From f475555d06edffeaebed3c9cd34608a9ba3aa84d Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Sat, 5 Apr 2025 21:44:13 -0400 Subject: [PATCH 001/384] docs: document that default GitHub app requires device flow (#17162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Issue Closes #16824 Document that the default GitHub authentication app provided by Coder requires device flow, and that this behavior cannot be overridden. ## Changes Made Claude updated the GitHub authentication documentation to: 1. Add a prominent warning in the Default Configuration section explaining that the default GitHub app requires device flow and ignores the `CODER_OAUTH2_GITHUB_DEVICE_FLOW` setting 2. Clarify the Device Flow section to indicate that: - Device flow is always enabled for the default GitHub app - Device flow is optional for custom GitHub OAuth apps - The `CODER_OAUTH2_GITHUB_DEVICE_FLOW` setting is ignored when using the default app [preview](https://coder.com/docs/@16824-github-device-flow/admin/users/github-auth) 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Co-authored-by: Claude Co-authored-by: M Atif Ali --- docs/admin/users/github-auth.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/admin/users/github-auth.md b/docs/admin/users/github-auth.md index 1be6f7a11d9ef..d895764c44f29 100644 --- a/docs/admin/users/github-auth.md +++ b/docs/admin/users/github-auth.md @@ -15,6 +15,11 @@ This access is necessary for the Coder server to complete the authentication process. To the best of our knowledge, Coder, the company, does not gain access to this data by administering the GitHub app. +> [!IMPORTANT] +> The default GitHub app requires [device flow](#device-flow) to authenticate. +> This is enabled by default when using the default GitHub app. If you disable +> device flow using `CODER_OAUTH2_GITHUB_DEVICE_FLOW=false`, it will be ignored. + By default, only the admin user can sign up. To allow additional users to sign up with GitHub, add the following environment variable: @@ -124,11 +129,16 @@ organizations. This can be enforced from the organization settings page in the Coder supports [device flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow) -for GitHub OAuth. To enable it, set: +for GitHub OAuth. This is enabled by default for the default GitHub app and cannot be disabled +for that app. For your own custom GitHub OAuth app, you can enable device flow by setting: ```env CODER_OAUTH2_GITHUB_DEVICE_FLOW=true ``` -This is optional. We recommend using the standard OAuth flow instead, as it is -more convenient for end users. +Device flow is optional for custom GitHub OAuth apps. We generally recommend using +the standard OAuth flow instead, as it is more convenient for end users. + +> [!NOTE] +> If you're using the default GitHub app, device flow is always enabled regardless of +> the `CODER_OAUTH2_GITHUB_DEVICE_FLOW` setting. From 8f665e364a6866de41b8bda9f5b643344100e9cd Mon Sep 17 00:00:00 2001 From: Vincent Vielle Date: Sun, 6 Apr 2025 23:50:18 +0200 Subject: [PATCH 002/384] chore: remove notifications beta label (#17263) Some notifications `beta` label were remaining after the previous PR - removing it. --- site/src/modules/management/DeploymentSidebarView.tsx | 1 - .../UserSettingsPage/NotificationsPage/NotificationsPage.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/site/src/modules/management/DeploymentSidebarView.tsx b/site/src/modules/management/DeploymentSidebarView.tsx index d3985391def16..21d5ca840cf56 100644 --- a/site/src/modules/management/DeploymentSidebarView.tsx +++ b/site/src/modules/management/DeploymentSidebarView.tsx @@ -87,7 +87,6 @@ export const DeploymentSidebarView: FC = ({
Notifications -
)} diff --git a/site/src/pages/UserSettingsPage/NotificationsPage/NotificationsPage.tsx b/site/src/pages/UserSettingsPage/NotificationsPage/NotificationsPage.tsx index 6e7b9ac8ab8e0..a7f9537b1e99d 100644 --- a/site/src/pages/UserSettingsPage/NotificationsPage/NotificationsPage.tsx +++ b/site/src/pages/UserSettingsPage/NotificationsPage/NotificationsPage.tsx @@ -99,7 +99,6 @@ export const NotificationsPage: FC = () => { title="Notifications" description="Control which notifications you receive." layout="fluid" - featureStage="beta" > {ready ? ( From 87d9ff09731fc5a0174e10ebb12a204db959b9a2 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Mon, 7 Apr 2025 11:35:47 +0400 Subject: [PATCH 003/384] feat: add CODER_WORKSPACE_HOSTNAME_SUFFIX (#17268) Adds deployment option `CODER_WORKSPACE_HOSTNAME_SUFFIX`. This will eventually replace `CODER_SSH_HOSTNAME_PREFIX`, but we will do this slowly and support both for `coder ssh` for some time. Note that the name is changed to "workspace" hostname, since this suffix will also be used for Coder Connect on Coder Desktop, which is not limited to SSH. --- cli/testdata/coder_server_--help.golden | 7 ++++++- cli/testdata/server-config.yaml.golden | 6 +++++- coderd/apidoc/docs.go | 3 +++ coderd/apidoc/swagger.json | 3 +++ codersdk/deployment.go | 14 +++++++++++++- docs/reference/api/general.md | 1 + docs/reference/api/schemas.md | 3 +++ docs/reference/cli/server.md | 11 +++++++++++ enterprise/cli/testdata/coder_server_--help.golden | 7 ++++++- site/src/api/typesGenerated.ts | 1 + 10 files changed, 52 insertions(+), 4 deletions(-) diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 80779201dc796..7fe70860e2e2a 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -78,7 +78,7 @@ OPTIONS: CLIENT OPTIONS: These options change the behavior of how clients interact with the Coder. -Clients include the coder cli, vs code extension, and the web UI. +Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI. --cli-upgrade-message string, $CODER_CLI_UPGRADE_MESSAGE The upgrade message to display to users when a client/server mismatch @@ -98,6 +98,11 @@ Clients include the coder cli, vs code extension, and the web UI. The renderer to use when opening a web terminal. Valid values are 'canvas', 'webgl', or 'dom'. + --workspace-hostname-suffix string, $CODER_WORKSPACE_HOSTNAME_SUFFIX (default: coder) + Workspace hostnames use this suffix in SSH config and Coder Connect on + Coder Desktop. By default it is coder, resulting in names like + myworkspace.coder. + CONFIG OPTIONS: Use a YAML configuration file when your server launch become unwieldy. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 39ed5eb2c047d..271593f753395 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -490,11 +490,15 @@ disablePathApps: false # (default: , type: bool) disableOwnerWorkspaceAccess: false # These options change the behavior of how clients interact with the Coder. -# Clients include the coder cli, vs code extension, and the web UI. +# Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI. client: # The SSH deployment prefix is used in the Host of the ssh config. # (default: coder., type: string) sshHostnamePrefix: coder. + # Workspace hostnames use this suffix in SSH config and Coder Connect on Coder + # Desktop. By default it is coder, resulting in names like myworkspace.coder. + # (default: coder, type: string) + workspaceHostnameSuffix: coder # These SSH config options will override the default SSH config options. Provide # options in "key=value" or "key value" format separated by commas.Using this # incorrectly can break SSH to your deployment, use cautiously. diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c93af6a64a41c..c31ff68c9b147 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -12019,6 +12019,9 @@ const docTemplate = `{ "wildcard_access_url": { "type": "string" }, + "workspace_hostname_suffix": { + "type": "string" + }, "write_config": { "type": "boolean" } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index da4d7a4fcf41c..982daead86e69 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10759,6 +10759,9 @@ "wildcard_access_url": { "type": "string" }, + "workspace_hostname_suffix": { + "type": "string" + }, "write_config": { "type": "boolean" } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index a67682489f81d..a3e690ed67b08 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -393,6 +393,7 @@ type DeploymentValues struct { TermsOfServiceURL serpent.String `json:"terms_of_service_url,omitempty" typescript:",notnull"` Notifications NotificationsConfig `json:"notifications,omitempty" typescript:",notnull"` AdditionalCSPPolicy serpent.StringArray `json:"additional_csp_policy,omitempty" typescript:",notnull"` + WorkspaceHostnameSuffix serpent.String `json:"workspace_hostname_suffix,omitempty" typescript:",notnull"` Config serpent.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"` WriteConfig serpent.Bool `json:"write_config,omitempty" typescript:",notnull"` @@ -944,7 +945,7 @@ func (c *DeploymentValues) Options() serpent.OptionSet { deploymentGroupClient = serpent.Group{ Name: "Client", Description: "These options change the behavior of how clients interact with the Coder. " + - "Clients include the coder cli, vs code extension, and the web UI.", + "Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI.", YAML: "client", } deploymentGroupConfig = serpent.Group{ @@ -2549,6 +2550,17 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Hidden: false, Default: "coder.", }, + { + Name: "Workspace Hostname Suffix", + Description: "Workspace hostnames use this suffix in SSH config and Coder Connect on Coder Desktop. By default it is coder, resulting in names like myworkspace.coder.", + Flag: "workspace-hostname-suffix", + Env: "CODER_WORKSPACE_HOSTNAME_SUFFIX", + YAML: "workspaceHostnameSuffix", + Group: &deploymentGroupClient, + Value: &c.WorkspaceHostnameSuffix, + Hidden: false, + Default: "coder", + }, { Name: "SSH Config Options", Description: "These SSH config options will override the default SSH config options. " + diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index c016ae5ddc8fe..d1c4e2d5970f7 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -515,6 +515,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "web_terminal_renderer": "string", "wgtunnel_host": "string", "wildcard_access_url": "string", + "workspace_hostname_suffix": "string", "write_config": true }, "options": [ diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 4791967b53c9e..a3b11bf0f9f26 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -2204,6 +2204,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "web_terminal_renderer": "string", "wgtunnel_host": "string", "wildcard_access_url": "string", + "workspace_hostname_suffix": "string", "write_config": true }, "options": [ @@ -2680,6 +2681,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "web_terminal_renderer": "string", "wgtunnel_host": "string", "wildcard_access_url": "string", + "workspace_hostname_suffix": "string", "write_config": true } ``` @@ -2748,6 +2750,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `web_terminal_renderer` | string | false | | | | `wgtunnel_host` | string | false | | | | `wildcard_access_url` | string | false | | | +| `workspace_hostname_suffix` | string | false | | | | `write_config` | boolean | false | | | ## codersdk.DisplayApp diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 888e569f9d5bc..f55165bb397da 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -1133,6 +1133,17 @@ Specify a YAML file to load configuration from. The SSH deployment prefix is used in the Host of the ssh config. +### --workspace-hostname-suffix + +| | | +|-------------|-----------------------------------------------| +| Type | string | +| Environment | $CODER_WORKSPACE_HOSTNAME_SUFFIX | +| YAML | client.workspaceHostnameSuffix | +| Default | coder | + +Workspace hostnames use this suffix in SSH config and Coder Connect on Coder Desktop. By default it is coder, resulting in names like myworkspace.coder. + ### --ssh-config-options | | | diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 8ad6839c7a635..8f383e145aa94 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -79,7 +79,7 @@ OPTIONS: CLIENT OPTIONS: These options change the behavior of how clients interact with the Coder. -Clients include the coder cli, vs code extension, and the web UI. +Clients include the Coder CLI, Coder Desktop, IDE extensions, and the web UI. --cli-upgrade-message string, $CODER_CLI_UPGRADE_MESSAGE The upgrade message to display to users when a client/server mismatch @@ -99,6 +99,11 @@ Clients include the coder cli, vs code extension, and the web UI. The renderer to use when opening a web terminal. Valid values are 'canvas', 'webgl', or 'dom'. + --workspace-hostname-suffix string, $CODER_WORKSPACE_HOSTNAME_SUFFIX (default: coder) + Workspace hostnames use this suffix in SSH config and Coder Connect on + Coder Desktop. By default it is coder, resulting in names like + myworkspace.coder. + CONFIG OPTIONS: Use a YAML configuration file when your server launch become unwieldy. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 2df1c351d9db1..1f5af620130d1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -684,6 +684,7 @@ export interface DeploymentValues { readonly terms_of_service_url?: string; readonly notifications?: NotificationsConfig; readonly additional_csp_policy?: string; + readonly workspace_hostname_suffix?: string; readonly config?: string; readonly write_config?: boolean; readonly address?: string; From 24248736acd44baaa27c6cdc29b6ab82d3fa09c0 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Mon, 7 Apr 2025 11:57:10 +0400 Subject: [PATCH 004/384] feat: add host suffix to /api/v2/deployment/ssh (#17269) Adds `HostnameSuffix` to ssh config API and deprecates `HostnamePrefix`. We will still support setting and using the prefix for some time. --- cli/server.go | 1 + coderd/apidoc/docs.go | 5 +++++ coderd/apidoc/swagger.json | 5 +++++ codersdk/deployment.go | 7 ++++++- docs/reference/api/general.md | 1 + docs/reference/api/schemas.md | 12 +++++++----- site/src/api/typesGenerated.ts | 1 + site/src/testHelpers/entities.ts | 1 + 8 files changed, 27 insertions(+), 6 deletions(-) diff --git a/cli/server.go b/cli/server.go index c0d7d6fcee13e..98a7739412afa 100644 --- a/cli/server.go +++ b/cli/server.go @@ -653,6 +653,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. SSHConfig: codersdk.SSHConfigResponse{ HostnamePrefix: vals.SSHConfig.DeploymentName.String(), SSHConfigOptions: configSSHOptions, + HostnameSuffix: vals.WorkspaceHostnameSuffix.String(), }, AllowWorkspaceRenames: vals.AllowWorkspaceRenames.Value(), Entitlements: entitlements.New(), diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index c31ff68c9b147..ae566ee62208e 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -14754,6 +14754,11 @@ const docTemplate = `{ "type": "object", "properties": { "hostname_prefix": { + "description": "HostnamePrefix is the prefix we append to workspace names for SSH hostnames.\nDeprecated: use HostnameSuffix instead.", + "type": "string" + }, + "hostname_suffix": { + "description": "HostnameSuffix is the suffix to append to workspace names for SSH hostnames.", "type": "string" }, "ssh_config_options": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 982daead86e69..897ff44187a63 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13384,6 +13384,11 @@ "type": "object", "properties": { "hostname_prefix": { + "description": "HostnamePrefix is the prefix we append to workspace names for SSH hostnames.\nDeprecated: use HostnameSuffix instead.", + "type": "string" + }, + "hostname_suffix": { + "description": "HostnameSuffix is the suffix to append to workspace names for SSH hostnames.", "type": "string" }, "ssh_config_options": { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index a3e690ed67b08..089bd11567ab7 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -3393,7 +3393,12 @@ type DeploymentStats struct { } type SSHConfigResponse struct { - HostnamePrefix string `json:"hostname_prefix"` + // HostnamePrefix is the prefix we append to workspace names for SSH hostnames. + // Deprecated: use HostnameSuffix instead. + HostnamePrefix string `json:"hostname_prefix"` + + // HostnameSuffix is the suffix to append to workspace names for SSH hostnames. + HostnameSuffix string `json:"hostname_suffix"` SSHConfigOptions map[string]string `json:"ssh_config_options"` } diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index d1c4e2d5970f7..20372423f12ad 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -582,6 +582,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/ssh \ ```json { "hostname_prefix": "string", + "hostname_suffix": "string", "ssh_config_options": { "property1": "string", "property2": "string" diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index a3b11bf0f9f26..0fbf87e8e5ff9 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -5748,6 +5748,7 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ```json { "hostname_prefix": "string", + "hostname_suffix": "string", "ssh_config_options": { "property1": "string", "property2": "string" @@ -5757,11 +5758,12 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith ### Properties -| Name | Type | Required | Restrictions | Description | -|----------------------|--------|----------|--------------|-------------| -| `hostname_prefix` | string | false | | | -| `ssh_config_options` | object | false | | | -| » `[any property]` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|----------------------|--------|----------|--------------|-----------------------------------------------------------------------------------------------------------------------| +| `hostname_prefix` | string | false | | Hostname prefix is the prefix we append to workspace names for SSH hostnames. Deprecated: use HostnameSuffix instead. | +| `hostname_suffix` | string | false | | Hostname suffix is the suffix to append to workspace names for SSH hostnames. | +| `ssh_config_options` | object | false | | | +| » `[any property]` | string | false | | | ## codersdk.ServerSentEvent diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 1f5af620130d1..eb14392ed408a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2219,6 +2219,7 @@ export interface SSHConfig { // From codersdk/deployment.go export interface SSHConfigResponse { readonly hostname_prefix: string; + readonly hostname_suffix: string; readonly ssh_config_options: Record; } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a298dea4ffd9d..f69b8f98db6a0 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3032,6 +3032,7 @@ export const MockDeploymentStats: TypesGen.DeploymentStats = { export const MockDeploymentSSH: TypesGen.SSHConfigResponse = { hostname_prefix: " coder.", ssh_config_options: {}, + hostname_suffix: "coder", }; export const MockWorkspaceAgentLogs: TypesGen.WorkspaceAgentLog[] = [ From 59c5bc9bd2dd66abec7675bb66f910a72b4b08cd Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Mon, 7 Apr 2025 12:11:04 +0400 Subject: [PATCH 005/384] feat: add hostname-suffix option to config-ssh (#17270) Adds `hostname-suffix` as a Config SSH option that we get from Coderd, and also accept via a CLI flag. It doesn't actually do anything with this value --- that's for PRs up the stack, since we need the `coder ssh` command to be updated to understand the suffix first. --- cli/configssh.go | 28 +++++++++++++++++++-- cli/configssh_test.go | 2 ++ cli/testdata/coder_config-ssh_--help.golden | 3 +++ docs/reference/cli/config-ssh.md | 9 +++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/cli/configssh.go b/cli/configssh.go index 952120c30b477..67fbd19ef3f69 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -45,8 +45,10 @@ const ( // sshConfigOptions represents options that can be stored and read // from the coder config in ~/.ssh/coder. type sshConfigOptions struct { - waitEnum string + waitEnum string + // Deprecated: moving away from prefix to hostnameSuffix userHostPrefix string + hostnameSuffix string sshOptions []string disableAutostart bool header []string @@ -97,7 +99,11 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool { if !slicesSortedEqual(o.header, other.header) { return false } - return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix && o.disableAutostart == other.disableAutostart && o.headerCommand == other.headerCommand + return o.waitEnum == other.waitEnum && + o.userHostPrefix == other.userHostPrefix && + o.disableAutostart == other.disableAutostart && + o.headerCommand == other.headerCommand && + o.hostnameSuffix == other.hostnameSuffix } // slicesSortedEqual compares two slices without side-effects or regard to order. @@ -119,6 +125,9 @@ func (o sshConfigOptions) asList() (list []string) { if o.userHostPrefix != "" { list = append(list, fmt.Sprintf("ssh-host-prefix: %s", o.userHostPrefix)) } + if o.hostnameSuffix != "" { + list = append(list, fmt.Sprintf("hostname-suffix: %s", o.hostnameSuffix)) + } if o.disableAutostart { list = append(list, fmt.Sprintf("disable-autostart: %v", o.disableAutostart)) } @@ -314,6 +323,10 @@ func (r *RootCmd) configSSH() *serpent.Command { // Override with user flag. coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix } + if sshConfigOpts.hostnameSuffix != "" { + // Override with user flag. + coderdConfig.HostnameSuffix = sshConfigOpts.hostnameSuffix + } // Write agent configuration. defaultOptions := []string{ @@ -518,6 +531,12 @@ func (r *RootCmd) configSSH() *serpent.Command { Description: "Override the default host prefix.", Value: serpent.StringOf(&sshConfigOpts.userHostPrefix), }, + { + Flag: "hostname-suffix", + Env: "CODER_CONFIGSSH_HOSTNAME_SUFFIX", + Description: "Override the default hostname suffix.", + Value: serpent.StringOf(&sshConfigOpts.hostnameSuffix), + }, { Flag: "wait", Env: "CODER_CONFIGSSH_WAIT", // Not to be mixed with CODER_SSH_WAIT. @@ -568,6 +587,9 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption if o.userHostPrefix != "" { _, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-host-prefix", o.userHostPrefix) } + if o.hostnameSuffix != "" { + _, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "hostname-suffix", o.hostnameSuffix) + } if o.disableAutostart { _, _ = fmt.Fprintf(&ow, "# :%s=%v\n", "disable-autostart", o.disableAutostart) } @@ -607,6 +629,8 @@ func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) { o.waitEnum = parts[1] case "ssh-host-prefix": o.userHostPrefix = parts[1] + case "hostname-suffix": + o.hostnameSuffix = parts[1] case "ssh-option": o.sshOptions = append(o.sshOptions, parts[1]) case "disable-autostart": diff --git a/cli/configssh_test.go b/cli/configssh_test.go index 3b88ab1e54db7..84399ddc67949 100644 --- a/cli/configssh_test.go +++ b/cli/configssh_test.go @@ -432,6 +432,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { "# Last config-ssh options:", "# :wait=yes", "# :ssh-host-prefix=coder-test.", + "# :hostname-suffix=coder-suffix", "# :header=X-Test-Header=foo", "# :header=X-Test-Header2=bar", "# :header-command=printf h1=v1 h2=\"v2\" h3='v3'", @@ -447,6 +448,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { "--yes", "--wait=yes", "--ssh-host-prefix", "coder-test.", + "--hostname-suffix", "coder-suffix", "--header", "X-Test-Header=foo", "--header", "X-Test-Header2=bar", "--header-command", "printf h1=v1 h2=\"v2\" h3='v3'", diff --git a/cli/testdata/coder_config-ssh_--help.golden b/cli/testdata/coder_config-ssh_--help.golden index ebbfb7a11676c..86f38db99e84a 100644 --- a/cli/testdata/coder_config-ssh_--help.golden +++ b/cli/testdata/coder_config-ssh_--help.golden @@ -33,6 +33,9 @@ OPTIONS: unix-like shell. This flag forces the use of unix file paths (the forward slash '/'). + --hostname-suffix string, $CODER_CONFIGSSH_HOSTNAME_SUFFIX + Override the default hostname suffix. + --ssh-config-file string, $CODER_SSH_CONFIG_FILE (default: ~/.ssh/config) Specifies the path to an SSH config. diff --git a/docs/reference/cli/config-ssh.md b/docs/reference/cli/config-ssh.md index 937bcd061bd05..c9250523b6c28 100644 --- a/docs/reference/cli/config-ssh.md +++ b/docs/reference/cli/config-ssh.md @@ -79,6 +79,15 @@ Specifies whether or not to keep options from previous run of config-ssh. Override the default host prefix. +### --hostname-suffix + +| | | +|-------------|-----------------------------------------------| +| Type | string | +| Environment | $CODER_CONFIGSSH_HOSTNAME_SUFFIX | + +Override the default hostname suffix. + ### --wait | | | From 074ec2887d45d5c63bb03355087c9f18a09ac40c Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Mon, 7 Apr 2025 11:32:37 +0300 Subject: [PATCH 006/384] test(agent/agentssh): fix test race and improve Windows compat (#17271) Fixes coder/internal#558 --- agent/agentssh/agentssh.go | 4 +++- agent/agentssh/agentssh_test.go | 13 +++++++++++-- agent/agentssh/exec_windows.go | 10 +++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/agent/agentssh/agentssh.go b/agent/agentssh/agentssh.go index f56497d149499..293dd4db169ac 100644 --- a/agent/agentssh/agentssh.go +++ b/agent/agentssh/agentssh.go @@ -1060,8 +1060,10 @@ func (s *Server) Close() error { // Guard against multiple calls to Close and // accepting new connections during close. if s.closing != nil { + closing := s.closing s.mu.Unlock() - return xerrors.New("server is closing") + <-closing + return xerrors.New("server is closed") } s.closing = make(chan struct{}) diff --git a/agent/agentssh/agentssh_test.go b/agent/agentssh/agentssh_test.go index 9a427fdd7d91e..69f92e0fd31a0 100644 --- a/agent/agentssh/agentssh_test.go +++ b/agent/agentssh/agentssh_test.go @@ -153,7 +153,9 @@ func TestNewServer_CloseActiveConnections(t *testing.T) { logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil) require.NoError(t, err) - defer s.Close() + t.Cleanup(func() { + _ = s.Close() + }) err = s.UpdateHostSigner(42) assert.NoError(t, err) @@ -190,10 +192,17 @@ func TestNewServer_CloseActiveConnections(t *testing.T) { } // The 60 seconds here is intended to be longer than the // test. The shutdown should propagate. - err = sess.Start("/bin/bash -c 'trap \"sleep 60\" SIGTERM; sleep 60'") + if runtime.GOOS == "windows" { + // Best effort to at least partially test this in Windows. + err = sess.Start("echo start\"ed\" && sleep 60") + } else { + err = sess.Start("/bin/bash -c 'trap \"sleep 60\" SIGTERM; echo start\"ed\"; sleep 60'") + } assert.NoError(t, err) + pty.ExpectMatchContext(ctx, "started") close(ch) + err = sess.Wait() assert.Error(t, err) }(waitConns[i]) diff --git a/agent/agentssh/exec_windows.go b/agent/agentssh/exec_windows.go index 0345ddd85e52e..39f0f97198479 100644 --- a/agent/agentssh/exec_windows.go +++ b/agent/agentssh/exec_windows.go @@ -2,7 +2,6 @@ package agentssh import ( "context" - "os" "os/exec" "syscall" @@ -15,7 +14,12 @@ func cmdSysProcAttr() *syscall.SysProcAttr { func cmdCancel(ctx context.Context, logger slog.Logger, cmd *exec.Cmd) func() error { return func() error { - logger.Debug(ctx, "cmdCancel: sending interrupt to process", slog.F("pid", cmd.Process.Pid)) - return cmd.Process.Signal(os.Interrupt) + logger.Debug(ctx, "cmdCancel: killing process", slog.F("pid", cmd.Process.Pid)) + // Windows doesn't support sending signals to process groups, so we + // have to kill the process directly. In the future, we may want to + // implement a more sophisticated solution for process groups on + // Windows, but for now, this is a simple way to ensure that the + // process is terminated when the context is cancelled. + return cmd.Process.Kill() } } From 0b2b643ce2c21a77c173522fd62ecc4fbee5fd75 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 7 Apr 2025 10:35:28 +0200 Subject: [PATCH 007/384] feat: persist prebuild definitions on template import (#16951) This PR allows provisioners to recognise and report prebuild definitions to the coder control plane. It also allows the coder control plane to then persist these to its store. closes https://github.com/coder/internal/issues/507 --------- Signed-off-by: Danny Kopping Co-authored-by: Danny Kopping Co-authored-by: evgeniy-scherbina --- coderd/database/dbauthz/dbauthz.go | 16 +- coderd/database/dbauthz/dbauthz_test.go | 220 +-- coderd/database/dbmem/dbmem.go | 21 +- coderd/database/dbmetrics/querymetrics.go | 7 + coderd/database/dbmock/dbmock.go | 15 + coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 37 + coderd/database/queries/presets.sql | 8 + coderd/presets_test.go | 10 +- .../provisionerdserver/provisionerdserver.go | 21 +- .../provisionerdserver_test.go | 88 +- enterprise/coderd/groups_test.go | 6 + enterprise/coderd/prebuilds/id.go | 1 + go.mod | 4 +- go.sum | 8 +- provisioner/terraform/resources.go | 15 + provisioner/terraform/resources_test.go | 3 + .../child-external-module/main.tf | 2 +- .../resources/presets/external-module/main.tf | 2 +- .../testdata/resources/presets/presets.tf | 8 +- .../resources/presets/presets.tfplan.json | 28 +- .../resources/presets/presets.tfstate.json | 14 +- provisionersdk/proto/provisioner.pb.go | 1477 +++++++++-------- provisionersdk/proto/provisioner.proto | 9 +- site/e2e/provisionerGenerated.ts | 25 + 25 files changed, 1205 insertions(+), 841 deletions(-) create mode 100644 enterprise/coderd/prebuilds/id.go diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 3815f713c0f4e..bb372aa4c9f48 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2187,14 +2187,24 @@ func (q *querier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceID u return q.db.GetPresetByWorkspaceBuildID(ctx, workspaceID) } -func (q *querier) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { +func (q *querier) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { // An actor can read template version presets if they can read the related template version. - _, err := q.GetTemplateVersionByID(ctx, templateVersionID) + _, err := q.GetPresetByID(ctx, presetID) + if err != nil { + return nil, err + } + + return q.db.GetPresetParametersByPresetID(ctx, presetID) +} + +func (q *querier) GetPresetParametersByTemplateVersionID(ctx context.Context, args uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { + // An actor can read template version presets if they can read the related template version. + _, err := q.GetTemplateVersionByID(ctx, args) if err != nil { return nil, err } - return q.db.GetPresetParametersByTemplateVersionID(ctx, templateVersionID) + return q.db.GetPresetParametersByTemplateVersionID(ctx, args) } func (q *querier) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]database.GetPresetsBackoffRow, error) { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 0fe17f886b1b2..7af3cace5112b 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -182,7 +182,6 @@ func TestDBAuthzRecursive(t *testing.T) { method.Name == "PGLocks" { continue } - // Log the name of the last method, so if there is a panic, it is // easy to know which method failed. // t.Log(method.Name) // Call the function. Any infinite recursion will stack overflow. @@ -969,8 +968,7 @@ func (s *MethodTestSuite) TestOrganization() { TemplateVersionID: workspaceBuild.TemplateVersionID, Name: "test", } - preset, err := db.InsertPreset(context.Background(), insertPresetParams) - require.NoError(s.T(), err) + preset := dbgen.Preset(s.T(), db, insertPresetParams) insertPresetParametersParams := database.InsertPresetParametersParams{ TemplateVersionPresetID: preset.ID, Names: []string{"test"}, @@ -1027,8 +1025,8 @@ func (s *MethodTestSuite) TestOrganization() { }) check.Args(database.OrganizationMembersParams{ - OrganizationID: uuid.UUID{}, - UserID: uuid.UUID{}, + OrganizationID: o.ID, + UserID: u.ID, }).Asserts( mem, policy.ActionRead, ) @@ -3906,96 +3904,6 @@ func (s *MethodTestSuite) TestSystemFunctions() { ErrorsWithInMemDB(sql.ErrNoRows). Returns([]database.ParameterSchema{}) })) - s.Run("GetPresetByWorkspaceBuildID", s.Subtest(func(db database.Store, check *expects) { - org := dbgen.Organization(s.T(), db, database.Organization{}) - user := dbgen.User(s.T(), db, database.User{}) - template := dbgen.Template(s.T(), db, database.Template{ - CreatedBy: user.ID, - OrganizationID: org.ID, - }) - templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - preset, err := db.InsertPreset(context.Background(), database.InsertPresetParams{ - TemplateVersionID: templateVersion.ID, - Name: "test", - }) - require.NoError(s.T(), err) - workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ - OrganizationID: org.ID, - OwnerID: user.ID, - TemplateID: template.ID, - }) - job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ - OrganizationID: org.ID, - }) - workspaceBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ - WorkspaceID: workspace.ID, - TemplateVersionID: templateVersion.ID, - TemplateVersionPresetID: uuid.NullUUID{UUID: preset.ID, Valid: true}, - InitiatorID: user.ID, - JobID: job.ID, - }) - _, err = db.GetPresetByWorkspaceBuildID(context.Background(), workspaceBuild.ID) - require.NoError(s.T(), err) - check.Args(workspaceBuild.ID).Asserts(rbac.ResourceTemplate, policy.ActionRead) - })) - s.Run("GetPresetParametersByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) { - ctx := context.Background() - org := dbgen.Organization(s.T(), db, database.Organization{}) - user := dbgen.User(s.T(), db, database.User{}) - template := dbgen.Template(s.T(), db, database.Template{ - CreatedBy: user.ID, - OrganizationID: org.ID, - }) - templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - preset, err := db.InsertPreset(ctx, database.InsertPresetParams{ - TemplateVersionID: templateVersion.ID, - Name: "test", - }) - require.NoError(s.T(), err) - _, err = db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{ - TemplateVersionPresetID: preset.ID, - Names: []string{"test"}, - Values: []string{"test"}, - }) - require.NoError(s.T(), err) - presetParameters, err := db.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID) - require.NoError(s.T(), err) - - check.Args(templateVersion.ID).Asserts(template.RBACObject(), policy.ActionRead).Returns(presetParameters) - })) - s.Run("GetPresetsByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) { - ctx := context.Background() - org := dbgen.Organization(s.T(), db, database.Organization{}) - user := dbgen.User(s.T(), db, database.User{}) - template := dbgen.Template(s.T(), db, database.Template{ - CreatedBy: user.ID, - OrganizationID: org.ID, - }) - templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ - TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - - _, err := db.InsertPreset(ctx, database.InsertPresetParams{ - TemplateVersionID: templateVersion.ID, - Name: "test", - }) - require.NoError(s.T(), err) - - presets, err := db.GetPresetsByTemplateVersionID(ctx, templateVersion.ID) - require.NoError(s.T(), err) - - check.Args(templateVersion.ID).Asserts(template.RBACObject(), policy.ActionRead).Returns(presets) - })) s.Run("GetWorkspaceAppsByAgentIDs", s.Subtest(func(db database.Store, check *expects) { dbtestutil.DisableForeignKeysAndTriggers(s.T(), db) aWs := dbgen.Workspace(s.T(), db, database.WorkspaceTable{}) @@ -4839,6 +4747,125 @@ func (s *MethodTestSuite) TestNotifications() { } func (s *MethodTestSuite) TestPrebuilds() { + s.Run("GetPresetByWorkspaceBuildID", s.Subtest(func(db database.Store, check *expects) { + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + preset, err := db.InsertPreset(context.Background(), database.InsertPresetParams{ + TemplateVersionID: templateVersion.ID, + Name: "test", + }) + require.NoError(s.T(), err) + workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ + OrganizationID: org.ID, + OwnerID: user.ID, + TemplateID: template.ID, + }) + job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{ + OrganizationID: org.ID, + }) + workspaceBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + TemplateVersionPresetID: uuid.NullUUID{UUID: preset.ID, Valid: true}, + InitiatorID: user.ID, + JobID: job.ID, + }) + _, err = db.GetPresetByWorkspaceBuildID(context.Background(), workspaceBuild.ID) + require.NoError(s.T(), err) + check.Args(workspaceBuild.ID).Asserts(rbac.ResourceTemplate, policy.ActionRead) + })) + s.Run("GetPresetParametersByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) { + ctx := context.Background() + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + preset, err := db.InsertPreset(ctx, database.InsertPresetParams{ + TemplateVersionID: templateVersion.ID, + Name: "test", + }) + require.NoError(s.T(), err) + insertedParameters, err := db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{ + TemplateVersionPresetID: preset.ID, + Names: []string{"test"}, + Values: []string{"test"}, + }) + require.NoError(s.T(), err) + check. + Args(templateVersion.ID). + Asserts(template.RBACObject(), policy.ActionRead). + Returns(insertedParameters) + })) + s.Run("GetPresetParametersByPresetID", s.Subtest(func(db database.Store, check *expects) { + ctx := context.Background() + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + preset, err := db.InsertPreset(ctx, database.InsertPresetParams{ + TemplateVersionID: templateVersion.ID, + Name: "test", + }) + require.NoError(s.T(), err) + insertedParameters, err := db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{ + TemplateVersionPresetID: preset.ID, + Names: []string{"test"}, + Values: []string{"test"}, + }) + require.NoError(s.T(), err) + check. + Args(preset.ID). + Asserts(template.RBACObject(), policy.ActionRead). + Returns(insertedParameters) + })) + s.Run("GetPresetsByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) { + ctx := context.Background() + org := dbgen.Organization(s.T(), db, database.Organization{}) + user := dbgen.User(s.T(), db, database.User{}) + template := dbgen.Template(s.T(), db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + + _, err := db.InsertPreset(ctx, database.InsertPresetParams{ + TemplateVersionID: templateVersion.ID, + Name: "test", + }) + require.NoError(s.T(), err) + + presets, err := db.GetPresetsByTemplateVersionID(ctx, templateVersion.ID) + require.NoError(s.T(), err) + + check.Args(templateVersion.ID).Asserts(template.RBACObject(), policy.ActionRead).Returns(presets) + })) s.Run("ClaimPrebuiltWorkspace", s.Subtest(func(db database.Store, check *expects) { org := dbgen.Organization(s.T(), db, database.Organization{}) user := dbgen.User(s.T(), db, database.User{}) @@ -4923,7 +4950,8 @@ func (s *MethodTestSuite) TestPrebuilds() { UUID: template.ID, Valid: true, }, - OrganizationID: org.ID, + InvalidateAfterSecs: preset.InvalidateAfterSecs, + OrganizationID: org.ID, }) })) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index bfae69fa68b98..9d2bdd7a1ad81 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -4275,6 +4275,21 @@ func (q *FakeQuerier) GetPresetByWorkspaceBuildID(_ context.Context, workspaceBu return database.TemplateVersionPreset{}, sql.ErrNoRows } +func (q *FakeQuerier) GetPresetParametersByPresetID(_ context.Context, presetID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + parameters := make([]database.TemplateVersionPresetParameter, 0) + for _, parameter := range q.presetParameters { + if parameter.TemplateVersionPresetID != presetID { + continue + } + parameters = append(parameters, parameter) + } + + return parameters, nil +} + func (q *FakeQuerier) GetPresetParametersByTemplateVersionID(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -4293,7 +4308,6 @@ func (q *FakeQuerier) GetPresetParametersByTemplateVersionID(_ context.Context, continue } parameters = append(parameters, parameter) - break } } @@ -8854,6 +8868,11 @@ func (q *FakeQuerier) InsertPreset(_ context.Context, arg database.InsertPresetP TemplateVersionID: arg.TemplateVersionID, Name: arg.Name, CreatedAt: arg.CreatedAt, + DesiredInstances: arg.DesiredInstances, + InvalidateAfterSecs: sql.NullInt32{ + Int32: 0, + Valid: true, + }, } q.presets = append(q.presets, preset) return preset, nil diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index b29d95752d195..a70b4842c7fb9 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1110,6 +1110,13 @@ func (m queryMetricsStore) GetPresetByWorkspaceBuildID(ctx context.Context, work return r0, r1 } +func (m queryMetricsStore) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { + start := time.Now() + r0, r1 := m.s.GetPresetParametersByPresetID(ctx, presetID) + m.queryLatencies.WithLabelValues("GetPresetParametersByPresetID").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { start := time.Now() r0, r1 := m.s.GetPresetParametersByTemplateVersionID(ctx, templateVersionID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index e30759c6bba42..8ebb37178182d 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -2269,6 +2269,21 @@ func (mr *MockStoreMockRecorder) GetPresetByWorkspaceBuildID(ctx, workspaceBuild return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetByWorkspaceBuildID", reflect.TypeOf((*MockStore)(nil).GetPresetByWorkspaceBuildID), ctx, workspaceBuildID) } +// GetPresetParametersByPresetID mocks base method. +func (m *MockStore) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPresetParametersByPresetID", ctx, presetID) + ret0, _ := ret[0].([]database.TemplateVersionPresetParameter) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPresetParametersByPresetID indicates an expected call of GetPresetParametersByPresetID. +func (mr *MockStoreMockRecorder) GetPresetParametersByPresetID(ctx, presetID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetParametersByPresetID", reflect.TypeOf((*MockStore)(nil).GetPresetParametersByPresetID), ctx, presetID) +} + // GetPresetParametersByTemplateVersionID mocks base method. func (m *MockStore) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 54483c2176f4e..880a5ce4a093d 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -237,6 +237,7 @@ type sqlcQuerier interface { GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetricsRow, error) GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error) + GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]TemplateVersionPresetParameter, error) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, error) // GetPresetsBackoff groups workspace builds by preset ID. // Each preset is associated with exactly one template version ID. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e1c7c3e65ab92..653d3d3136e63 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6389,6 +6389,43 @@ func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceB return i, err } +const getPresetParametersByPresetID = `-- name: GetPresetParametersByPresetID :many +SELECT + tvpp.id, tvpp.template_version_preset_id, tvpp.name, tvpp.value +FROM + template_version_preset_parameters tvpp +WHERE + tvpp.template_version_preset_id = $1 +` + +func (q *sqlQuerier) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]TemplateVersionPresetParameter, error) { + rows, err := q.db.QueryContext(ctx, getPresetParametersByPresetID, presetID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TemplateVersionPresetParameter + for rows.Next() { + var i TemplateVersionPresetParameter + if err := rows.Scan( + &i.ID, + &i.TemplateVersionPresetID, + &i.Name, + &i.Value, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getPresetParametersByTemplateVersionID = `-- name: GetPresetParametersByTemplateVersionID :many SELECT template_version_preset_parameters.id, template_version_preset_parameters.template_version_preset_id, template_version_preset_parameters.name, template_version_preset_parameters.value diff --git a/coderd/database/queries/presets.sql b/coderd/database/queries/presets.sql index 526d7d0a95c3c..15bcea0c28fb5 100644 --- a/coderd/database/queries/presets.sql +++ b/coderd/database/queries/presets.sql @@ -49,6 +49,14 @@ FROM WHERE template_version_presets.template_version_id = @template_version_id; +-- name: GetPresetParametersByPresetID :many +SELECT + tvpp.* +FROM + template_version_preset_parameters tvpp +WHERE + tvpp.template_version_preset_id = @preset_id; + -- name: GetPresetByID :one SELECT tvp.*, tv.template_id, tv.organization_id FROM template_version_presets tvp diff --git a/coderd/presets_test.go b/coderd/presets_test.go index 08ff7c76f24f5..dc47b10cfd36f 100644 --- a/coderd/presets_test.go +++ b/coderd/presets_test.go @@ -8,6 +8,7 @@ import ( "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" @@ -86,16 +87,12 @@ func TestTemplateVersionPresets(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - // nolint:gocritic // This is a test - provisionerCtx := dbauthz.AsProvisionerd(ctx) - // Insert all presets for this test case for _, givenPreset := range tc.presets { - dbPreset, err := db.InsertPreset(provisionerCtx, database.InsertPresetParams{ + dbPreset := dbgen.Preset(t, db, database.InsertPresetParams{ Name: givenPreset.Name, TemplateVersionID: version.ID, }) - require.NoError(t, err) if len(givenPreset.Parameters) > 0 { var presetParameterNames []string @@ -104,12 +101,11 @@ func TestTemplateVersionPresets(t *testing.T) { presetParameterNames = append(presetParameterNames, presetParameter.Name) presetParameterValues = append(presetParameterValues, presetParameter.Value) } - _, err = db.InsertPresetParameters(provisionerCtx, database.InsertPresetParametersParams{ + dbgen.PresetParameter(t, db, database.InsertPresetParametersParams{ TemplateVersionPresetID: dbPreset.ID, Names: presetParameterNames, Values: presetParameterValues, }) - require.NoError(t, err) } } diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index b9f303f95c319..6f8c3707f7279 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1855,12 +1855,22 @@ func InsertWorkspacePresetsAndParameters(ctx context.Context, logger slog.Logger func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, templateVersionID uuid.UUID, protoPreset *sdkproto.Preset, t time.Time) error { err := db.InTx(func(tx database.Store) error { + var desiredInstances sql.NullInt32 + if protoPreset != nil && protoPreset.Prebuild != nil { + desiredInstances = sql.NullInt32{ + Int32: protoPreset.Prebuild.Instances, + Valid: true, + } + } dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{ - TemplateVersionID: templateVersionID, - Name: protoPreset.Name, - CreatedAt: t, - DesiredInstances: sql.NullInt32{}, - InvalidateAfterSecs: sql.NullInt32{}, + TemplateVersionID: templateVersionID, + Name: protoPreset.Name, + CreatedAt: t, + DesiredInstances: desiredInstances, + InvalidateAfterSecs: sql.NullInt32{ + Int32: 0, + Valid: false, + }, // TODO: implement cache invalidation }) if err != nil { return xerrors.Errorf("insert preset: %w", err) @@ -1880,6 +1890,7 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, if err != nil { return xerrors.Errorf("insert preset parameters: %w", err) } + return nil }, nil) if err != nil { diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 3909c54aef843..698520d6f8d02 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -1733,6 +1733,34 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) { }, }, }, + { + name: "one preset, no parameters, requesting prebuilds", + givenPresets: []*sdkproto.Preset{ + { + Name: "preset1", + Prebuild: &sdkproto.Prebuild{ + Instances: 1, + }, + }, + }, + }, + { + name: "one preset with multiple parameters, requesting 0 prebuilds", + givenPresets: []*sdkproto.Preset{ + { + Name: "preset1", + Parameters: []*sdkproto.PresetParameter{ + { + Name: "param1", + Value: "value1", + }, + }, + Prebuild: &sdkproto.Prebuild{ + Instances: 0, + }, + }, + }, + }, { name: "one preset with multiple parameters", givenPresets: []*sdkproto.Preset{ @@ -1751,6 +1779,27 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) { }, }, }, + { + name: "one preset, multiple parameters, requesting prebuilds", + givenPresets: []*sdkproto.Preset{ + { + Name: "preset1", + Parameters: []*sdkproto.PresetParameter{ + { + Name: "param1", + Value: "value1", + }, + { + Name: "param2", + Value: "value2", + }, + }, + Prebuild: &sdkproto.Prebuild{ + Instances: 1, + }, + }, + }, + }, { name: "multiple presets with parameters", givenPresets: []*sdkproto.Preset{ @@ -1766,6 +1815,9 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) { Value: "value2", }, }, + Prebuild: &sdkproto.Prebuild{ + Instances: 1, + }, }, { Name: "preset2", @@ -1794,6 +1846,7 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) { db, ps := dbtestutil.NewDB(t) org := dbgen.Organization(t, db, database.Organization{}) user := dbgen.User(t, db, database.User{}) + job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ Type: database.ProvisionerJobTypeWorkspaceBuild, OrganizationID: org.ID, @@ -1820,42 +1873,37 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) { require.Len(t, gotPresets, len(c.givenPresets)) for _, givenPreset := range c.givenPresets { - foundMatch := false + var foundPreset *database.TemplateVersionPreset for _, gotPreset := range gotPresets { if givenPreset.Name == gotPreset.Name { - foundMatch = true + foundPreset = &gotPreset break } } - require.True(t, foundMatch, "preset %s not found in parameters", givenPreset.Name) - } + require.NotNil(t, foundPreset, "preset %s not found in parameters", givenPreset.Name) - gotPresetParameters, err := db.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID) - require.NoError(t, err) + gotPresetParameters, err := db.GetPresetParametersByPresetID(ctx, foundPreset.ID) + require.NoError(t, err) + require.Len(t, gotPresetParameters, len(givenPreset.Parameters)) - for _, givenPreset := range c.givenPresets { for _, givenParameter := range givenPreset.Parameters { foundMatch := false for _, gotParameter := range gotPresetParameters { nameMatches := givenParameter.Name == gotParameter.Name valueMatches := givenParameter.Value == gotParameter.Value - - // ensure that preset parameters are matched to the correct preset: - var gotPreset database.TemplateVersionPreset - for _, preset := range gotPresets { - if preset.ID == gotParameter.TemplateVersionPresetID { - gotPreset = preset - break - } - } - presetMatches := gotPreset.Name == givenPreset.Name - - if nameMatches && valueMatches && presetMatches { + if nameMatches && valueMatches { foundMatch = true break } } - require.True(t, foundMatch, "preset parameter %s not found in presets", givenParameter.Name) + require.True(t, foundMatch, "preset parameter %s not found in parameters", givenParameter.Name) + } + if givenPreset.Prebuild == nil { + require.False(t, foundPreset.DesiredInstances.Valid) + } + if givenPreset.Prebuild != nil { + require.True(t, foundPreset.DesiredInstances.Valid) + require.Equal(t, givenPreset.Prebuild.Instances, foundPreset.DesiredInstances.Int32) } } }) diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 690a476fcb1ba..028aa3328535f 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/google/uuid" "github.com/stretchr/testify/require" @@ -830,6 +832,9 @@ func TestGroup(t *testing.T) { _, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) ctx := testutil.Context(t, testutil.WaitLong) + // nolint:gocritic // "This client is operating as the owner user" is fine in this case. + prebuildsUser, err := client.User(ctx, prebuilds.SystemUserID.String()) + require.NoError(t, err) // The 'Everyone' group always has an ID that matches the organization ID. group, err := userAdminClient.Group(ctx, user.OrganizationID) require.NoError(t, err) @@ -838,6 +843,7 @@ func TestGroup(t *testing.T) { require.Equal(t, user.OrganizationID, group.OrganizationID) require.Contains(t, group.Members, user1.ReducedUser) require.Contains(t, group.Members, user2.ReducedUser) + require.NotContains(t, group.Members, prebuildsUser.ReducedUser) }) } diff --git a/enterprise/coderd/prebuilds/id.go b/enterprise/coderd/prebuilds/id.go new file mode 100644 index 0000000000000..b6513942447c2 --- /dev/null +++ b/enterprise/coderd/prebuilds/id.go @@ -0,0 +1 @@ +package prebuilds diff --git a/go.mod b/go.mod index 3ecb96a3e14f6..20d78c4ab9808 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( github.com/coder/quartz v0.1.2 github.com/coder/retry v1.5.1 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder/v2 v2.1.3 + github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e github.com/coder/websocket v1.8.12 github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 github.com/coreos/go-oidc/v3 v3.13.0 @@ -341,7 +341,7 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 // indirect github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect diff --git a/go.sum b/go.sum index 70c46ff5266da..7b94f620d7d0e 100644 --- a/go.sum +++ b/go.sum @@ -248,8 +248,8 @@ github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a h1:18TQ03KlYrkW8 github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= -github.com/coder/terraform-provider-coder/v2 v2.1.3 h1:zB7ObGsiOGBHcJUUMmcSauEPlTWRIYmMYieF05LxHSc= -github.com/coder/terraform-provider-coder/v2 v2.1.3/go.mod h1:RHGyb+ghiy8UpDAMJM8duRFuzd+1VqA3AtkRLh2P3Ug= +github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e h1:coy2k2X/d+bGys9wUqQn/TR/0xBibiOIX6vZzPSVGso= +github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e/go.mod h1:X28s3rz+aEM5PkBKvk3xcUrQFO2eNPjzRChUg9wb70U= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Auuw3YOiSyLNHkdMtyCZHPFBx7syN4rk= @@ -565,8 +565,8 @@ github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiy github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0 h1:7/iejAPyCRBhqAg3jOx+4UcAhY0A+Sg8B+0+d/GxSfM= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0/go.mod h1:TiQwXAjFrgBf5tg5rvBRz8/ubPULpU0HjSaVi5UoJf8= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1/go.mod h1:P6o64QS97plG44iFzSM6rAn6VJIC/Sy9a9IkEtl79K4= github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= diff --git a/provisioner/terraform/resources.go b/provisioner/terraform/resources.go index eaf6f9b5991bc..da86ab2f3d48e 100644 --- a/provisioner/terraform/resources.go +++ b/provisioner/terraform/resources.go @@ -3,6 +3,7 @@ package terraform import ( "context" "fmt" + "math" "strings" "github.com/awalterschulze/gographviz" @@ -883,10 +884,24 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s ) } + if len(preset.Prebuilds) != 1 { + logger.Warn( + ctx, + "coder_workspace_preset must have exactly one prebuild block", + ) + } + var prebuildInstances int32 + if len(preset.Prebuilds) > 0 { + prebuildInstances = int32(math.Min(math.MaxInt32, float64(preset.Prebuilds[0].Instances))) + } protoPreset := &proto.Preset{ Name: preset.Name, Parameters: presetParameters, + Prebuild: &proto.Prebuild{ + Instances: prebuildInstances, + }, } + if slice.Contains(duplicatedPresetNames, preset.Name) { duplicatedPresetNames = append(duplicatedPresetNames, preset.Name) } diff --git a/provisioner/terraform/resources_test.go b/provisioner/terraform/resources_test.go index 815bb7f8a6034..61c21ea532b53 100644 --- a/provisioner/terraform/resources_test.go +++ b/provisioner/terraform/resources_test.go @@ -828,6 +828,9 @@ func TestConvertResources(t *testing.T) { Name: "Sample", Value: "A1B2C3", }}, + Prebuild: &proto.Prebuild{ + Instances: 4, + }, }}, }, "devcontainer": { diff --git a/provisioner/terraform/testdata/resources/presets/external-module/child-external-module/main.tf b/provisioner/terraform/testdata/resources/presets/external-module/child-external-module/main.tf index 87a338be4e9ed..395f766d48c4c 100644 --- a/provisioner/terraform/testdata/resources/presets/external-module/child-external-module/main.tf +++ b/provisioner/terraform/testdata/resources/presets/external-module/child-external-module/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "2.1.3" + version = "2.3.0-pre2" } docker = { source = "kreuzwerker/docker" diff --git a/provisioner/terraform/testdata/resources/presets/external-module/main.tf b/provisioner/terraform/testdata/resources/presets/external-module/main.tf index 8bcb59c832ee9..bdfd29c301c06 100644 --- a/provisioner/terraform/testdata/resources/presets/external-module/main.tf +++ b/provisioner/terraform/testdata/resources/presets/external-module/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "2.1.3" + version = "2.3.0-pre2" } docker = { source = "kreuzwerker/docker" diff --git a/provisioner/terraform/testdata/resources/presets/presets.tf b/provisioner/terraform/testdata/resources/presets/presets.tf index 42471aa0f298a..cd5338bfd3ba4 100644 --- a/provisioner/terraform/testdata/resources/presets/presets.tf +++ b/provisioner/terraform/testdata/resources/presets/presets.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "2.1.3" + version = "2.3.0-pre2" } } } @@ -22,9 +22,9 @@ data "coder_workspace_preset" "MyFirstProject" { name = "My First Project" parameters = { (data.coder_parameter.sample.name) = "A1B2C3" - # TODO (sasswart): Add support for parameters from external modules - # (data.coder_parameter.first_parameter_from_module.name) = "A1B2C3" - # (data.coder_parameter.child_first_parameter_from_module.name) = "A1B2C3" + } + prebuilds { + instances = 4 } } diff --git a/provisioner/terraform/testdata/resources/presets/presets.tfplan.json b/provisioner/terraform/testdata/resources/presets/presets.tfplan.json index 4339a3df51569..57bdf0fe19188 100644 --- a/provisioner/terraform/testdata/resources/presets/presets.tfplan.json +++ b/provisioner/terraform/testdata/resources/presets/presets.tfplan.json @@ -21,6 +21,7 @@ "motd_file": null, "order": null, "os": "windows", + "resources_monitoring": [], "shutdown_script": null, "startup_script": null, "startup_script_behavior": "non-blocking", @@ -29,6 +30,7 @@ "sensitive_values": { "display_apps": [], "metadata": [], + "resources_monitoring": [], "token": true } }, @@ -69,6 +71,7 @@ "motd_file": null, "order": null, "os": "windows", + "resources_monitoring": [], "shutdown_script": null, "startup_script": null, "startup_script_behavior": "non-blocking", @@ -79,12 +82,14 @@ "id": true, "init_script": true, "metadata": [], + "resources_monitoring": [], "token": true }, "before_sensitive": false, "after_sensitive": { "display_apps": [], "metadata": [], + "resources_monitoring": [], "token": true } } @@ -156,10 +161,18 @@ "name": "My First Project", "parameters": { "Sample": "A1B2C3" - } + }, + "prebuilds": [ + { + "instances": 4 + } + ] }, "sensitive_values": { - "parameters": {} + "parameters": {}, + "prebuilds": [ + {} + ] } } ], @@ -293,7 +306,7 @@ "coder": { "name": "coder", "full_name": "registry.terraform.io/coder/coder", - "version_constraint": "2.1.3" + "version_constraint": "2.3.0-pre2" }, "module.this_is_external_module:docker": { "name": "docker", @@ -372,7 +385,14 @@ "data.coder_parameter.sample.name", "data.coder_parameter.sample" ] - } + }, + "prebuilds": [ + { + "instances": { + "constant_value": 4 + } + } + ] }, "schema_version": 0 } diff --git a/provisioner/terraform/testdata/resources/presets/presets.tfstate.json b/provisioner/terraform/testdata/resources/presets/presets.tfstate.json index 552cdef3ab8a6..1ae43c857fc69 100644 --- a/provisioner/terraform/testdata/resources/presets/presets.tfstate.json +++ b/provisioner/terraform/testdata/resources/presets/presets.tfstate.json @@ -43,10 +43,18 @@ "name": "My First Project", "parameters": { "Sample": "A1B2C3" - } + }, + "prebuilds": [ + { + "instances": 4 + } + ] }, "sensitive_values": { - "parameters": {} + "parameters": {}, + "prebuilds": [ + {} + ] } }, { @@ -77,6 +85,7 @@ "motd_file": null, "order": null, "os": "windows", + "resources_monitoring": [], "shutdown_script": null, "startup_script": null, "startup_script_behavior": "non-blocking", @@ -88,6 +97,7 @@ {} ], "metadata": [], + "resources_monitoring": [], "token": true } }, diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index d7c91319ddcf9..f258f79e36f94 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -699,6 +699,53 @@ func (x *RichParameterValue) GetValue() string { return "" } +type Prebuild struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Instances int32 `protobuf:"varint,1,opt,name=instances,proto3" json:"instances,omitempty"` +} + +func (x *Prebuild) Reset() { + *x = Prebuild{} + if protoimpl.UnsafeEnabled { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Prebuild) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Prebuild) ProtoMessage() {} + +func (x *Prebuild) ProtoReflect() protoreflect.Message { + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Prebuild.ProtoReflect.Descriptor instead. +func (*Prebuild) Descriptor() ([]byte, []int) { + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{5} +} + +func (x *Prebuild) GetInstances() int32 { + if x != nil { + return x.Instances + } + return 0 +} + // Preset represents a set of preset parameters for a template version. type Preset struct { state protoimpl.MessageState @@ -707,12 +754,13 @@ type Preset struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Parameters []*PresetParameter `protobuf:"bytes,2,rep,name=parameters,proto3" json:"parameters,omitempty"` + Prebuild *Prebuild `protobuf:"bytes,3,opt,name=prebuild,proto3" json:"prebuild,omitempty"` } func (x *Preset) Reset() { *x = Preset{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[5] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -725,7 +773,7 @@ func (x *Preset) String() string { func (*Preset) ProtoMessage() {} func (x *Preset) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[5] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -738,7 +786,7 @@ func (x *Preset) ProtoReflect() protoreflect.Message { // Deprecated: Use Preset.ProtoReflect.Descriptor instead. func (*Preset) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{5} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{6} } func (x *Preset) GetName() string { @@ -755,6 +803,13 @@ func (x *Preset) GetParameters() []*PresetParameter { return nil } +func (x *Preset) GetPrebuild() *Prebuild { + if x != nil { + return x.Prebuild + } + return nil +} + type PresetParameter struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -767,7 +822,7 @@ type PresetParameter struct { func (x *PresetParameter) Reset() { *x = PresetParameter{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -780,7 +835,7 @@ func (x *PresetParameter) String() string { func (*PresetParameter) ProtoMessage() {} func (x *PresetParameter) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -793,7 +848,7 @@ func (x *PresetParameter) ProtoReflect() protoreflect.Message { // Deprecated: Use PresetParameter.ProtoReflect.Descriptor instead. func (*PresetParameter) Descriptor() ([]byte, []int) { - return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{6} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{7} } func (x *PresetParameter) GetName() string { @@ -824,7 +879,7 @@ type VariableValue struct { func (x *VariableValue) Reset() { *x = VariableValue{} if protoimpl.UnsafeEnabled { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[7] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -837,7 +892,7 @@ func (x *VariableValue) String() string { func (*VariableValue) ProtoMessage() {} func (x *VariableValue) ProtoReflect() protoreflect.Message { - mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[7] + mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -850,7 +905,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{7} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{8} } func (x *VariableValue) GetName() string { @@ -887,7 +942,7 @@ type Log struct { func (x *Log) Reset() { *x = Log{} 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) } @@ -900,7 +955,7 @@ func (x *Log) String() string { func (*Log) ProtoMessage() {} func (x *Log) 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 { @@ -913,7 +968,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{8} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{9} } func (x *Log) GetLevel() LogLevel { @@ -941,7 +996,7 @@ type InstanceIdentityAuth struct { func (x *InstanceIdentityAuth) Reset() { *x = InstanceIdentityAuth{} 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) } @@ -954,7 +1009,7 @@ func (x *InstanceIdentityAuth) String() string { func (*InstanceIdentityAuth) ProtoMessage() {} func (x *InstanceIdentityAuth) 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 { @@ -967,7 +1022,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{9} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{10} } func (x *InstanceIdentityAuth) GetInstanceId() string { @@ -989,7 +1044,7 @@ type ExternalAuthProviderResource struct { func (x *ExternalAuthProviderResource) Reset() { *x = ExternalAuthProviderResource{} 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) } @@ -1002,7 +1057,7 @@ func (x *ExternalAuthProviderResource) String() string { func (*ExternalAuthProviderResource) ProtoMessage() {} func (x *ExternalAuthProviderResource) 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 { @@ -1015,7 +1070,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{10} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{11} } func (x *ExternalAuthProviderResource) GetId() string { @@ -1044,7 +1099,7 @@ type ExternalAuthProvider struct { func (x *ExternalAuthProvider) Reset() { *x = ExternalAuthProvider{} 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) } @@ -1057,7 +1112,7 @@ func (x *ExternalAuthProvider) String() string { func (*ExternalAuthProvider) ProtoMessage() {} func (x *ExternalAuthProvider) 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 { @@ -1070,7 +1125,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{11} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{12} } func (x *ExternalAuthProvider) GetId() string { @@ -1124,7 +1179,7 @@ type Agent struct { func (x *Agent) Reset() { *x = Agent{} 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) } @@ -1137,7 +1192,7 @@ func (x *Agent) String() string { func (*Agent) ProtoMessage() {} func (x *Agent) 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 { @@ -1150,7 +1205,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{12} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13} } func (x *Agent) GetId() string { @@ -1321,7 +1376,7 @@ type ResourcesMonitoring struct { func (x *ResourcesMonitoring) Reset() { *x = ResourcesMonitoring{} 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) } @@ -1334,7 +1389,7 @@ func (x *ResourcesMonitoring) String() string { func (*ResourcesMonitoring) ProtoMessage() {} func (x *ResourcesMonitoring) 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 { @@ -1347,7 +1402,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{13} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{14} } func (x *ResourcesMonitoring) GetMemory() *MemoryResourceMonitor { @@ -1376,7 +1431,7 @@ type MemoryResourceMonitor struct { func (x *MemoryResourceMonitor) Reset() { *x = MemoryResourceMonitor{} 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) } @@ -1389,7 +1444,7 @@ func (x *MemoryResourceMonitor) String() string { func (*MemoryResourceMonitor) ProtoMessage() {} func (x *MemoryResourceMonitor) 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 { @@ -1402,7 +1457,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{14} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{15} } func (x *MemoryResourceMonitor) GetEnabled() bool { @@ -1432,7 +1487,7 @@ type VolumeResourceMonitor struct { func (x *VolumeResourceMonitor) Reset() { *x = VolumeResourceMonitor{} 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) } @@ -1445,7 +1500,7 @@ func (x *VolumeResourceMonitor) String() string { func (*VolumeResourceMonitor) ProtoMessage() {} func (x *VolumeResourceMonitor) 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 { @@ -1458,7 +1513,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{15} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{16} } func (x *VolumeResourceMonitor) GetPath() string { @@ -1497,7 +1552,7 @@ type DisplayApps struct { func (x *DisplayApps) Reset() { *x = DisplayApps{} 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) } @@ -1510,7 +1565,7 @@ func (x *DisplayApps) String() string { func (*DisplayApps) ProtoMessage() {} func (x *DisplayApps) 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 { @@ -1523,7 +1578,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{16} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{17} } func (x *DisplayApps) GetVscode() bool { @@ -1573,7 +1628,7 @@ type Env struct { func (x *Env) Reset() { *x = Env{} 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) } @@ -1586,7 +1641,7 @@ func (x *Env) String() string { func (*Env) ProtoMessage() {} func (x *Env) 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 { @@ -1599,7 +1654,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{17} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{18} } func (x *Env) GetName() string { @@ -1636,7 +1691,7 @@ type Script struct { func (x *Script) Reset() { *x = Script{} 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) } @@ -1649,7 +1704,7 @@ func (x *Script) String() string { func (*Script) ProtoMessage() {} func (x *Script) 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 { @@ -1662,7 +1717,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{18} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{19} } func (x *Script) GetDisplayName() string { @@ -1741,7 +1796,7 @@ type Devcontainer struct { func (x *Devcontainer) Reset() { *x = Devcontainer{} 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) } @@ -1754,7 +1809,7 @@ func (x *Devcontainer) String() string { func (*Devcontainer) ProtoMessage() {} func (x *Devcontainer) 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 { @@ -1767,7 +1822,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{19} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{20} } func (x *Devcontainer) GetWorkspaceFolder() string { @@ -1816,7 +1871,7 @@ type App struct { func (x *App) Reset() { *x = App{} 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) } @@ -1829,7 +1884,7 @@ func (x *App) String() string { func (*App) ProtoMessage() {} func (x *App) 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 { @@ -1842,7 +1897,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{20} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{21} } func (x *App) GetSlug() string { @@ -1943,7 +1998,7 @@ type Healthcheck struct { func (x *Healthcheck) Reset() { *x = Healthcheck{} 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) } @@ -1956,7 +2011,7 @@ func (x *Healthcheck) String() string { func (*Healthcheck) ProtoMessage() {} func (x *Healthcheck) 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 { @@ -1969,7 +2024,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{21} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{22} } func (x *Healthcheck) GetUrl() string { @@ -2013,7 +2068,7 @@ type Resource struct { func (x *Resource) Reset() { *x = Resource{} 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) } @@ -2026,7 +2081,7 @@ func (x *Resource) String() string { func (*Resource) ProtoMessage() {} func (x *Resource) 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 { @@ -2039,7 +2094,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{22} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{23} } func (x *Resource) GetName() string { @@ -2118,7 +2173,7 @@ type Module struct { func (x *Module) Reset() { *x = Module{} 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) } @@ -2131,7 +2186,7 @@ func (x *Module) String() string { func (*Module) ProtoMessage() {} func (x *Module) 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 { @@ -2144,7 +2199,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{23} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{24} } func (x *Module) GetSource() string { @@ -2180,7 +2235,7 @@ type Role struct { func (x *Role) Reset() { *x = Role{} 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) } @@ -2193,7 +2248,7 @@ func (x *Role) String() string { func (*Role) ProtoMessage() {} func (x *Role) 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 { @@ -2206,7 +2261,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{24} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{25} } func (x *Role) GetName() string { @@ -2248,12 +2303,14 @@ type Metadata struct { 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"` } func (x *Metadata) Reset() { *x = Metadata{} 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) } @@ -2266,7 +2323,7 @@ func (x *Metadata) String() string { func (*Metadata) ProtoMessage() {} func (x *Metadata) 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 { @@ -2279,7 +2336,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{25} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{26} } func (x *Metadata) GetCoderUrl() string { @@ -2415,6 +2472,20 @@ func (x *Metadata) GetWorkspaceOwnerRbacRoles() []*Role { return nil } +func (x *Metadata) GetIsPrebuild() bool { + if x != nil { + return x.IsPrebuild + } + return false +} + +func (x *Metadata) GetRunningWorkspaceAgentToken() string { + if x != nil { + return x.RunningWorkspaceAgentToken + } + return "" +} + // Config represents execution configuration shared by all subsequent requests in the Session type Config struct { state protoimpl.MessageState @@ -2431,7 +2502,7 @@ type Config struct { func (x *Config) Reset() { *x = Config{} 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) } @@ -2444,7 +2515,7 @@ func (x *Config) String() string { func (*Config) ProtoMessage() {} func (x *Config) 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 { @@ -2457,7 +2528,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{26} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{27} } func (x *Config) GetTemplateSourceArchive() []byte { @@ -2491,7 +2562,7 @@ type ParseRequest struct { func (x *ParseRequest) Reset() { *x = ParseRequest{} 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) } @@ -2504,7 +2575,7 @@ func (x *ParseRequest) String() string { func (*ParseRequest) ProtoMessage() {} func (x *ParseRequest) 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 { @@ -2517,7 +2588,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{27} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{28} } // ParseComplete indicates a request to parse completed. @@ -2535,7 +2606,7 @@ type ParseComplete struct { func (x *ParseComplete) Reset() { *x = ParseComplete{} 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) } @@ -2548,7 +2619,7 @@ func (x *ParseComplete) String() string { func (*ParseComplete) ProtoMessage() {} func (x *ParseComplete) 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 { @@ -2561,7 +2632,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{28} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{29} } func (x *ParseComplete) GetError() string { @@ -2607,7 +2678,7 @@ type PlanRequest struct { func (x *PlanRequest) Reset() { *x = PlanRequest{} 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) } @@ -2620,7 +2691,7 @@ func (x *PlanRequest) String() string { func (*PlanRequest) ProtoMessage() {} func (x *PlanRequest) 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 { @@ -2633,7 +2704,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{29} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{30} } func (x *PlanRequest) GetMetadata() *Metadata { @@ -2683,7 +2754,7 @@ type PlanComplete struct { func (x *PlanComplete) Reset() { *x = PlanComplete{} 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) } @@ -2696,7 +2767,7 @@ func (x *PlanComplete) String() string { func (*PlanComplete) ProtoMessage() {} func (x *PlanComplete) 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 { @@ -2709,7 +2780,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{30} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{31} } func (x *PlanComplete) GetError() string { @@ -2781,7 +2852,7 @@ type ApplyRequest struct { func (x *ApplyRequest) Reset() { *x = ApplyRequest{} 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) } @@ -2794,7 +2865,7 @@ func (x *ApplyRequest) String() string { func (*ApplyRequest) ProtoMessage() {} func (x *ApplyRequest) 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 { @@ -2807,7 +2878,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{31} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{32} } func (x *ApplyRequest) GetMetadata() *Metadata { @@ -2834,7 +2905,7 @@ type ApplyComplete struct { func (x *ApplyComplete) Reset() { *x = ApplyComplete{} 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) } @@ -2847,7 +2918,7 @@ func (x *ApplyComplete) String() string { func (*ApplyComplete) ProtoMessage() {} func (x *ApplyComplete) 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 { @@ -2860,7 +2931,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{32} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{33} } func (x *ApplyComplete) GetState() []byte { @@ -2922,7 +2993,7 @@ type Timing struct { func (x *Timing) Reset() { *x = Timing{} 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) } @@ -2935,7 +3006,7 @@ func (x *Timing) String() string { func (*Timing) ProtoMessage() {} func (x *Timing) 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 { @@ -2948,7 +3019,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{33} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{34} } func (x *Timing) GetStart() *timestamppb.Timestamp { @@ -3010,7 +3081,7 @@ type CancelRequest struct { func (x *CancelRequest) Reset() { *x = CancelRequest{} 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) } @@ -3023,7 +3094,7 @@ func (x *CancelRequest) String() string { func (*CancelRequest) ProtoMessage() {} func (x *CancelRequest) 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 { @@ -3036,7 +3107,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{34} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{35} } type Request struct { @@ -3057,7 +3128,7 @@ type Request struct { func (x *Request) Reset() { *x = Request{} 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) } @@ -3070,7 +3141,7 @@ func (x *Request) String() string { func (*Request) ProtoMessage() {} func (x *Request) 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 { @@ -3083,7 +3154,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{35} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{36} } func (m *Request) GetType() isRequest_Type { @@ -3179,7 +3250,7 @@ type Response struct { func (x *Response) Reset() { *x = Response{} 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) } @@ -3192,7 +3263,7 @@ func (x *Response) String() string { func (*Response) ProtoMessage() {} func (x *Response) 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 { @@ -3205,7 +3276,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{36} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{37} } func (m *Response) GetType() isResponse_Type { @@ -3287,7 +3358,7 @@ type Agent_Metadata struct { func (x *Agent_Metadata) Reset() { *x = Agent_Metadata{} 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) } @@ -3300,7 +3371,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[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 { @@ -3313,7 +3384,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{12, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{13, 0} } func (x *Agent_Metadata) GetKey() string { @@ -3372,7 +3443,7 @@ type Resource_Metadata struct { func (x *Resource_Metadata) Reset() { *x = Resource_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) } @@ -3385,7 +3456,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[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 { @@ -3398,7 +3469,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{22, 0} + return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{23, 0} } func (x *Resource_Metadata) GetKey() string { @@ -3501,468 +3572,480 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x5a, 0x0a, 0x06, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, - 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x73, 0x22, 0x3b, 0x0a, 0x0f, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, - 0x65, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, - 0x0a, 0x0d, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, - 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, - 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 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, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x28, 0x0a, 0x08, 0x50, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x22, + 0x8d, 0x01, 0x0a, 0x06, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3c, + 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x2e, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x31, 0x0a, 0x08, + 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x65, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x52, 0x08, 0x70, 0x72, 0x65, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x22, + 0x3b, 0x0a, 0x0f, 0x50, 0x72, 0x65, 0x73, 0x65, 0x74, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, + 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x57, 0x0a, 0x0d, + 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, + 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, + 0x69, 0x74, 0x69, 0x76, 0x65, 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, 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, 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, + 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, + 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, 0x92, 0x03, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, - 0x12, 0x0a, 0x04, 0x68, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, - 0x69, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, - 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, - 0x64, 0x61, 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x09, 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, - 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x1a, 0x69, 0x0a, 0x08, - 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, - 0x0a, 0x07, 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x06, 0x69, 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0x4c, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, - 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x31, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x12, 0x0a, + 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, 0x15, 0x0a, 0x06, 0x6f, 0x72, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x6f, 0x72, 0x67, 0x49, 0x64, 0x22, 0xfc, 0x07, 0x0a, 0x08, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, - 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, - 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, - 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x65, 0x6d, 0x61, 0x69, - 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x23, 0x0a, 0x0d, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x65, 0x6d, - 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x21, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, - 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x65, 0x73, - 0x73, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x41, 0x0a, 0x1d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x65, 0x6d, - 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x30, 0x0a, 0x14, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x34, 0x0a, 0x16, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, - 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x73, 0x12, 0x42, 0x0a, 0x1e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x1a, 0x77, 0x6f, 0x72, 0x6b, - 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, 0x73, 0x68, 0x50, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x1f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x1b, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x53, - 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x12, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, - 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x1a, 0x77, 0x6f, - 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6c, 0x6f, - 0x67, 0x69, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, - 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 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, 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, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, + 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, + 0x04, 0x68, 0x69, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x68, 0x69, 0x64, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x69, 0x63, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x61, + 0x69, 0x6c, 0x79, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, + 0x64, 0x61, 0x69, 0x6c, 0x79, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x6f, 0x64, + 0x75, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, + 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x1a, 0x69, 0x0a, 0x08, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1c, + 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x17, 0x0a, 0x07, + 0x69, 0x73, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x69, + 0x73, 0x4e, 0x75, 0x6c, 0x6c, 0x22, 0x4c, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x22, 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, + 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, 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, 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, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x22, 0xbe, 0x02, 0x0a, 0x0d, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x3a, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, + 0x74, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x69, 0x63, 0x68, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x73, 0x12, 0x61, 0x0a, 0x17, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x15, + 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x2d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x73, + 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x74, 0x69, 0x6d, + 0x69, 0x6e, 0x67, 0x73, 0x22, 0xfa, 0x01, 0x0a, 0x06, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x12, + 0x30, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, + 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, + 0x74, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, + 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x54, + 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x22, 0x0f, 0x0a, 0x0d, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x8c, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, + 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x31, 0x0a, + 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, + 0x12, 0x2e, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, + 0x12, 0x31, 0x0a, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, + 0x70, 0x6c, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, + 0x70, 0x6c, 0x79, 0x12, 0x34, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, + 0x00, 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x22, 0xd1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, + 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, + 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x32, 0x0a, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, + 0x00, 0x52, 0x05, 0x70, 0x61, 0x72, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x48, 0x00, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x05, 0x61, 0x70, 0x70, + 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x43, 0x6f, 0x6d, 0x70, + 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x42, 0x06, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, + 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, + 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x3b, 0x0a, 0x0f, 0x41, 0x70, 0x70, 0x53, 0x68, 0x61, + 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x4f, 0x57, 0x4e, + 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x55, 0x54, 0x48, 0x45, 0x4e, 0x54, 0x49, + 0x43, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x50, 0x55, 0x42, 0x4c, 0x49, + 0x43, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x09, 0x41, 0x70, 0x70, 0x4f, 0x70, 0x65, 0x6e, 0x49, 0x6e, + 0x12, 0x0e, 0x0a, 0x06, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x10, 0x00, 0x1a, 0x02, 0x08, 0x01, + 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x4c, 0x49, 0x4d, 0x5f, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x10, + 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x41, 0x42, 0x10, 0x02, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, + 0x53, 0x54, 0x4f, 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, + 0x59, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x0b, 0x54, 0x69, 0x6d, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x0d, 0x0a, 0x09, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0a, + 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x32, 0x49, 0x0a, 0x0b, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x3a, 0x0a, 0x07, 0x53, 0x65, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, + 0x76, 0x32, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, + 0x6b, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3978,7 +4061,7 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { } var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 41) +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 @@ -3990,101 +4073,103 @@ var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ (*RichParameterOption)(nil), // 7: provisioner.RichParameterOption (*RichParameter)(nil), // 8: provisioner.RichParameter (*RichParameterValue)(nil), // 9: provisioner.RichParameterValue - (*Preset)(nil), // 10: provisioner.Preset - (*PresetParameter)(nil), // 11: provisioner.PresetParameter - (*VariableValue)(nil), // 12: provisioner.VariableValue - (*Log)(nil), // 13: provisioner.Log - (*InstanceIdentityAuth)(nil), // 14: provisioner.InstanceIdentityAuth - (*ExternalAuthProviderResource)(nil), // 15: provisioner.ExternalAuthProviderResource - (*ExternalAuthProvider)(nil), // 16: provisioner.ExternalAuthProvider - (*Agent)(nil), // 17: provisioner.Agent - (*ResourcesMonitoring)(nil), // 18: provisioner.ResourcesMonitoring - (*MemoryResourceMonitor)(nil), // 19: provisioner.MemoryResourceMonitor - (*VolumeResourceMonitor)(nil), // 20: provisioner.VolumeResourceMonitor - (*DisplayApps)(nil), // 21: provisioner.DisplayApps - (*Env)(nil), // 22: provisioner.Env - (*Script)(nil), // 23: provisioner.Script - (*Devcontainer)(nil), // 24: provisioner.Devcontainer - (*App)(nil), // 25: provisioner.App - (*Healthcheck)(nil), // 26: provisioner.Healthcheck - (*Resource)(nil), // 27: provisioner.Resource - (*Module)(nil), // 28: provisioner.Module - (*Role)(nil), // 29: provisioner.Role - (*Metadata)(nil), // 30: provisioner.Metadata - (*Config)(nil), // 31: provisioner.Config - (*ParseRequest)(nil), // 32: provisioner.ParseRequest - (*ParseComplete)(nil), // 33: provisioner.ParseComplete - (*PlanRequest)(nil), // 34: provisioner.PlanRequest - (*PlanComplete)(nil), // 35: provisioner.PlanComplete - (*ApplyRequest)(nil), // 36: provisioner.ApplyRequest - (*ApplyComplete)(nil), // 37: provisioner.ApplyComplete - (*Timing)(nil), // 38: provisioner.Timing - (*CancelRequest)(nil), // 39: provisioner.CancelRequest - (*Request)(nil), // 40: provisioner.Request - (*Response)(nil), // 41: provisioner.Response - (*Agent_Metadata)(nil), // 42: provisioner.Agent.Metadata - nil, // 43: provisioner.Agent.EnvEntry - (*Resource_Metadata)(nil), // 44: provisioner.Resource.Metadata - nil, // 45: provisioner.ParseComplete.WorkspaceTagsEntry - (*timestamppb.Timestamp)(nil), // 46: google.protobuf.Timestamp + (*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 } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 7, // 0: provisioner.RichParameter.options:type_name -> provisioner.RichParameterOption - 11, // 1: provisioner.Preset.parameters:type_name -> provisioner.PresetParameter - 0, // 2: provisioner.Log.level:type_name -> provisioner.LogLevel - 43, // 3: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry - 25, // 4: provisioner.Agent.apps:type_name -> provisioner.App - 42, // 5: provisioner.Agent.metadata:type_name -> provisioner.Agent.Metadata - 21, // 6: provisioner.Agent.display_apps:type_name -> provisioner.DisplayApps - 23, // 7: provisioner.Agent.scripts:type_name -> provisioner.Script - 22, // 8: provisioner.Agent.extra_envs:type_name -> provisioner.Env - 18, // 9: provisioner.Agent.resources_monitoring:type_name -> provisioner.ResourcesMonitoring - 24, // 10: provisioner.Agent.devcontainers:type_name -> provisioner.Devcontainer - 19, // 11: provisioner.ResourcesMonitoring.memory:type_name -> provisioner.MemoryResourceMonitor - 20, // 12: provisioner.ResourcesMonitoring.volumes:type_name -> provisioner.VolumeResourceMonitor - 26, // 13: provisioner.App.healthcheck:type_name -> provisioner.Healthcheck - 1, // 14: provisioner.App.sharing_level:type_name -> provisioner.AppSharingLevel - 2, // 15: provisioner.App.open_in:type_name -> provisioner.AppOpenIn - 17, // 16: provisioner.Resource.agents:type_name -> provisioner.Agent - 44, // 17: provisioner.Resource.metadata:type_name -> provisioner.Resource.Metadata - 3, // 18: provisioner.Metadata.workspace_transition:type_name -> provisioner.WorkspaceTransition - 29, // 19: provisioner.Metadata.workspace_owner_rbac_roles:type_name -> provisioner.Role - 6, // 20: provisioner.ParseComplete.template_variables:type_name -> provisioner.TemplateVariable - 45, // 21: provisioner.ParseComplete.workspace_tags:type_name -> provisioner.ParseComplete.WorkspaceTagsEntry - 30, // 22: provisioner.PlanRequest.metadata:type_name -> provisioner.Metadata - 9, // 23: provisioner.PlanRequest.rich_parameter_values:type_name -> provisioner.RichParameterValue - 12, // 24: provisioner.PlanRequest.variable_values:type_name -> provisioner.VariableValue - 16, // 25: provisioner.PlanRequest.external_auth_providers:type_name -> provisioner.ExternalAuthProvider - 27, // 26: provisioner.PlanComplete.resources:type_name -> provisioner.Resource - 8, // 27: provisioner.PlanComplete.parameters:type_name -> provisioner.RichParameter - 15, // 28: provisioner.PlanComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 38, // 29: provisioner.PlanComplete.timings:type_name -> provisioner.Timing - 28, // 30: provisioner.PlanComplete.modules:type_name -> provisioner.Module - 10, // 31: provisioner.PlanComplete.presets:type_name -> provisioner.Preset - 30, // 32: provisioner.ApplyRequest.metadata:type_name -> provisioner.Metadata - 27, // 33: provisioner.ApplyComplete.resources:type_name -> provisioner.Resource - 8, // 34: provisioner.ApplyComplete.parameters:type_name -> provisioner.RichParameter - 15, // 35: provisioner.ApplyComplete.external_auth_providers:type_name -> provisioner.ExternalAuthProviderResource - 38, // 36: provisioner.ApplyComplete.timings:type_name -> provisioner.Timing - 46, // 37: provisioner.Timing.start:type_name -> google.protobuf.Timestamp - 46, // 38: provisioner.Timing.end:type_name -> google.protobuf.Timestamp - 4, // 39: provisioner.Timing.state:type_name -> provisioner.TimingState - 31, // 40: provisioner.Request.config:type_name -> provisioner.Config - 32, // 41: provisioner.Request.parse:type_name -> provisioner.ParseRequest - 34, // 42: provisioner.Request.plan:type_name -> provisioner.PlanRequest - 36, // 43: provisioner.Request.apply:type_name -> provisioner.ApplyRequest - 39, // 44: provisioner.Request.cancel:type_name -> provisioner.CancelRequest - 13, // 45: provisioner.Response.log:type_name -> provisioner.Log - 33, // 46: provisioner.Response.parse:type_name -> provisioner.ParseComplete - 35, // 47: provisioner.Response.plan:type_name -> provisioner.PlanComplete - 37, // 48: provisioner.Response.apply:type_name -> provisioner.ApplyComplete - 40, // 49: provisioner.Provisioner.Session:input_type -> provisioner.Request - 41, // 50: provisioner.Provisioner.Session:output_type -> provisioner.Response - 50, // [50:51] is the sub-list for method output_type - 49, // [49:50] is the sub-list for method input_type - 49, // [49:49] is the sub-list for extension type_name - 49, // [49:49] is the sub-list for extension extendee - 0, // [0:49] is the sub-list for field type_name + 12, // 1: provisioner.Preset.parameters:type_name -> provisioner.PresetParameter + 10, // 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 + 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 + 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 } func init() { file_provisionersdk_proto_provisioner_proto_init() } @@ -4154,7 +4239,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Preset); i { + switch v := v.(*Prebuild); i { case 0: return &v.state case 1: @@ -4166,7 +4251,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PresetParameter); i { + switch v := v.(*Preset); i { case 0: return &v.state case 1: @@ -4178,7 +4263,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VariableValue); i { + switch v := v.(*PresetParameter); i { case 0: return &v.state case 1: @@ -4190,7 +4275,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.(*Log); i { + switch v := v.(*VariableValue); i { case 0: return &v.state case 1: @@ -4202,7 +4287,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.(*InstanceIdentityAuth); i { + switch v := v.(*Log); i { case 0: return &v.state case 1: @@ -4214,7 +4299,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.(*ExternalAuthProviderResource); i { + switch v := v.(*InstanceIdentityAuth); i { case 0: return &v.state case 1: @@ -4226,7 +4311,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.(*ExternalAuthProvider); i { + switch v := v.(*ExternalAuthProviderResource); i { case 0: return &v.state case 1: @@ -4238,7 +4323,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.(*Agent); i { + switch v := v.(*ExternalAuthProvider); i { case 0: return &v.state case 1: @@ -4250,7 +4335,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.(*ResourcesMonitoring); i { + switch v := v.(*Agent); i { case 0: return &v.state case 1: @@ -4262,7 +4347,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.(*MemoryResourceMonitor); i { + switch v := v.(*ResourcesMonitoring); i { case 0: return &v.state case 1: @@ -4274,7 +4359,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.(*VolumeResourceMonitor); i { + switch v := v.(*MemoryResourceMonitor); i { case 0: return &v.state case 1: @@ -4286,7 +4371,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.(*DisplayApps); i { + switch v := v.(*VolumeResourceMonitor); i { case 0: return &v.state case 1: @@ -4298,7 +4383,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.(*Env); i { + switch v := v.(*DisplayApps); i { case 0: return &v.state case 1: @@ -4310,7 +4395,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.(*Script); i { + switch v := v.(*Env); i { case 0: return &v.state case 1: @@ -4322,7 +4407,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.(*Devcontainer); i { + switch v := v.(*Script); i { case 0: return &v.state case 1: @@ -4334,7 +4419,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.(*App); i { + switch v := v.(*Devcontainer); i { case 0: return &v.state case 1: @@ -4346,7 +4431,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.(*Healthcheck); i { + switch v := v.(*App); i { case 0: return &v.state case 1: @@ -4358,7 +4443,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.(*Resource); i { + switch v := v.(*Healthcheck); i { case 0: return &v.state case 1: @@ -4370,7 +4455,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.(*Module); i { + switch v := v.(*Resource); i { case 0: return &v.state case 1: @@ -4382,7 +4467,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.(*Role); i { + switch v := v.(*Module); i { case 0: return &v.state case 1: @@ -4394,7 +4479,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.(*Metadata); i { + switch v := v.(*Role); i { case 0: return &v.state case 1: @@ -4406,7 +4491,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.(*Config); i { + switch v := v.(*Metadata); i { case 0: return &v.state case 1: @@ -4418,7 +4503,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.(*ParseRequest); i { + switch v := v.(*Config); i { case 0: return &v.state case 1: @@ -4430,7 +4515,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.(*ParseComplete); i { + switch v := v.(*ParseRequest); i { case 0: return &v.state case 1: @@ -4442,7 +4527,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.(*PlanRequest); i { + switch v := v.(*ParseComplete); i { case 0: return &v.state case 1: @@ -4454,7 +4539,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.(*PlanComplete); i { + switch v := v.(*PlanRequest); i { case 0: return &v.state case 1: @@ -4466,7 +4551,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.(*ApplyRequest); i { + switch v := v.(*PlanComplete); i { case 0: return &v.state case 1: @@ -4478,7 +4563,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.(*ApplyComplete); i { + switch v := v.(*ApplyRequest); i { case 0: return &v.state case 1: @@ -4490,7 +4575,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.(*Timing); i { + switch v := v.(*ApplyComplete); i { case 0: return &v.state case 1: @@ -4502,7 +4587,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.(*CancelRequest); i { + switch v := v.(*Timing); i { case 0: return &v.state case 1: @@ -4514,7 +4599,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.(*Request); i { + switch v := v.(*CancelRequest); i { case 0: return &v.state case 1: @@ -4526,7 +4611,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.(*Response); i { + switch v := v.(*Request); i { case 0: return &v.state case 1: @@ -4538,6 +4623,18 @@ 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 { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_provisionersdk_proto_provisioner_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Agent_Metadata); i { case 0: return &v.state @@ -4549,7 +4646,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { return nil } } - file_provisionersdk_proto_provisioner_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + file_provisionersdk_proto_provisioner_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Resource_Metadata); i { case 0: return &v.state @@ -4563,18 +4660,18 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[3].OneofWrappers = []interface{}{} - file_provisionersdk_proto_provisioner_proto_msgTypes[12].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[13].OneofWrappers = []interface{}{ (*Agent_Token)(nil), (*Agent_InstanceId)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[35].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[36].OneofWrappers = []interface{}{ (*Request_Config)(nil), (*Request_Parse)(nil), (*Request_Plan)(nil), (*Request_Apply)(nil), (*Request_Cancel)(nil), } - file_provisionersdk_proto_provisioner_proto_msgTypes[36].OneofWrappers = []interface{}{ + file_provisionersdk_proto_provisioner_proto_msgTypes[37].OneofWrappers = []interface{}{ (*Response_Log)(nil), (*Response_Parse)(nil), (*Response_Plan)(nil), @@ -4586,7 +4683,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_provisionersdk_proto_provisioner_proto_rawDesc, NumEnums: 5, - NumMessages: 41, + NumMessages: 42, NumExtensions: 0, NumServices: 1, }, diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 446bee7fc6108..3e6841fb24450 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -57,10 +57,15 @@ message RichParameterValue { string value = 2; } +message Prebuild { + int32 instances = 1; +} + // Preset represents a set of preset parameters for a template version. message Preset { string name = 1; repeated PresetParameter parameters = 2; + Prebuild prebuild = 3; } message PresetParameter { @@ -287,7 +292,9 @@ message Metadata { string workspace_owner_ssh_private_key = 16; string workspace_build_id = 17; string workspace_owner_login_type = 18; - repeated Role workspace_owner_rbac_roles = 19; + repeated Role workspace_owner_rbac_roles = 19; + bool is_prebuild = 20; + string running_workspace_agent_token = 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 8623c20bcf24c..cea6f9cb364af 100644 --- a/site/e2e/provisionerGenerated.ts +++ b/site/e2e/provisionerGenerated.ts @@ -94,10 +94,15 @@ export interface RichParameterValue { value: string; } +export interface Prebuild { + instances: number; +} + /** Preset represents a set of preset parameters for a template version. */ export interface Preset { name: string; parameters: PresetParameter[]; + prebuild: Prebuild | undefined; } export interface PresetParameter { @@ -302,6 +307,8 @@ export interface Metadata { workspaceBuildId: string; workspaceOwnerLoginType: string; workspaceOwnerRbacRoles: Role[]; + isPrebuild: boolean; + runningWorkspaceAgentToken: string; } /** Config represents execution configuration shared by all subsequent requests in the Session */ @@ -511,6 +518,15 @@ export const RichParameterValue = { }, }; +export const Prebuild = { + encode(message: Prebuild, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.instances !== 0) { + writer.uint32(8).int32(message.instances); + } + return writer; + }, +}; + export const Preset = { encode(message: Preset, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { if (message.name !== "") { @@ -519,6 +535,9 @@ export const Preset = { for (const v of message.parameters) { PresetParameter.encode(v!, writer.uint32(18).fork()).ldelim(); } + if (message.prebuild !== undefined) { + Prebuild.encode(message.prebuild, writer.uint32(26).fork()).ldelim(); + } return writer; }, }; @@ -1008,6 +1027,12 @@ 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.runningWorkspaceAgentToken !== "") { + writer.uint32(170).string(message.runningWorkspaceAgentToken); + } return writer; }, }; From 4615d2969822663823006ce16e882264c1e0a0b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:59:56 +0000 Subject: [PATCH 008/384] chore: bump the x group across 1 directory with 6 updates (#17272) Bumps the x group with 3 updates in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/net](https://github.com/golang/net) and [golang.org/x/oauth2](https://github.com/golang/oauth2). Updates `golang.org/x/crypto` from 0.36.0 to 0.37.0
Commits
  • 959f8f3 go.mod: update golang.org/x dependencies
  • 769bcd6 ssh: use the configured rand in kex init
  • d0a798f cryptobyte: fix typo 'octects' into 'octets' for asn1.go
  • acbcbef acme: remove unnecessary []byte conversion
  • 376eb14 x509roots: support constrained roots
  • b369b72 crypto/internal/poly1305: implement function update in assembly on loong64
  • 6b853fb ssh/knownhosts: check more than one key
  • See full diff in compare view

Updates `golang.org/x/net` from 0.37.0 to 0.38.0
Commits
  • e1fcd82 html: properly handle trailing solidus in unquoted attribute value in foreign...
  • ebed060 internal/http3: fix build of tests with GOEXPERIMENT=nosynctest
  • 1f1fa29 publicsuffix: regenerate table
  • 1215081 http2: improve error when server sends HTTP/1
  • 312450e html: ensure <search> tag closes <p> and update tests
  • 09731f9 http2: improve handling of lost PING in Server
  • 55989e2 http2/h2c: use ResponseController for hijacking connections
  • 2914f46 websocket: re-recommend gorilla/websocket
  • See full diff in compare view

Updates `golang.org/x/oauth2` from 0.28.0 to 0.29.0
Commits

Updates `golang.org/x/sync` from 0.12.0 to 0.13.0
Commits

Updates `golang.org/x/sys` from 0.31.0 to 0.32.0
Commits
  • 01aaa83 all: simplify code by using modern Go constructs
  • 1b2bd6b windows: replace all StringToUTF16 calls with UTF16FromString
  • 1c3b72f unix: update Linux kernel to 6.14
  • c175b6b windows: add cmsghdr and pktinfo structures
  • 3330b5e unix: support Readv, Preadv, Writev and Pwritev for darwin
  • 7401cce cpu: replace specific instructions with WORD in the function get_cpucfg on lo...
  • b8f7da6 cpu: add support for detecting cpu features on loong64
  • f2ce62c windows: add constants for PMTUD socket options
  • See full diff in compare view

Updates `golang.org/x/term` from 0.30.0 to 0.31.0
Commits
  • 5d2308b go.mod: update golang.org/x dependencies
  • e770ddd x/term: disabling auto-completion around GetPassword()
  • See full diff in compare view

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> --- go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 20d78c4ab9808..d17e29200de9a 100644 --- a/go.mod +++ b/go.mod @@ -189,15 +189,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.36.0 + golang.org/x/crypto v0.37.0 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa golang.org/x/mod v0.24.0 - golang.org/x/net v0.37.0 - golang.org/x/oauth2 v0.28.0 - golang.org/x/sync v0.12.0 - golang.org/x/sys v0.31.0 - golang.org/x/term v0.30.0 - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.38.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/tools v0.31.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da google.golang.org/api v0.228.0 diff --git a/go.sum b/go.sum index 7b94f620d7d0e..30f91e435be27 100644 --- a/go.sum +++ b/go.sum @@ -1076,8 +1076,8 @@ golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +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/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= @@ -1106,10 +1106,10 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1121,8 +1121,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -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/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1165,8 +1165,8 @@ 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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.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= @@ -1179,8 +1179,8 @@ golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1194,8 +1194,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.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 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -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/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From a2314ad53c22c610026305ba2b2043463ec1608c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:03:43 +0000 Subject: [PATCH 009/384] chore: bump github.com/ory/dockertest/v3 from 3.11.0 to 3.12.0 (#17275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/ory/dockertest/v3](https://github.com/ory/dockertest) from 3.11.0 to 3.12.0.
Release notes

Sourced from github.com/ory/dockertest/v3's releases.

v3.12.0

What's Changed

New Contributors

Full Changelog: https://github.com/ory/dockertest/compare/v3.11.0...v3.12.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/ory/dockertest/v3&package-manager=go_modules&previous-version=3.11.0&new-version=3.12.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 | 15 +++++++++------ go.sum | 26 ++++++++++++++------------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index d17e29200de9a..5af9e57e99559 100644 --- a/go.mod +++ b/go.mod @@ -153,7 +153,7 @@ require ( github.com/muesli/termenv v0.16.0 github.com/natefinch/atomic v1.0.1 github.com/open-policy-agent/opa v1.1.0 - github.com/ory/dockertest/v3 v3.11.0 + github.com/ory/dockertest/v3 v3.12.0 github.com/pion/udp v0.1.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e @@ -276,10 +276,10 @@ require ( github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/continuity v0.4.4 // indirect + github.com/containerd/continuity v0.4.5 // indirect github.com/coreos/go-iptables v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/docker/cli v27.1.1+incompatible // indirect + github.com/docker/cli v27.4.1+incompatible // indirect github.com/docker/docker v27.2.0+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -305,7 +305,7 @@ require ( github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-test/deep v1.1.0 // indirect github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.1.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect @@ -384,7 +384,7 @@ require ( github.com/oklog/run v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runc v1.1.14 // indirect + 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.3 // indirect github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect @@ -483,4 +483,7 @@ require ( require github.com/mark3labs/mcp-go v0.17.0 -require github.com/yosida95/uritemplate/v3 v3.0.2 // indirect +require ( + github.com/moby/sys/user v0.3.0 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect +) diff --git a/go.sum b/go.sum index 30f91e435be27..f658ab14fc7da 100644 --- a/go.sum +++ b/go.sum @@ -212,8 +212,8 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= -github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= -github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= +github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= +github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE= github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= @@ -256,8 +256,8 @@ github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Au github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0/go.mod h1:qANbdpqyAGlo2bg+4gQKPj24H1ZWa3bQU2Q5/bV5B3Y= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818 h1:bNhUTaKl3q0bFn78bBRq7iIwo72kNTvUD9Ll5TTzDDk= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818/go.mod h1:fAlLM6hUgnf4Sagxn2Uy5Us0PBgOYWz+63HwHUVGEbw= -github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= -github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8= @@ -294,8 +294,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= -github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= +github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -418,8 +418,8 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -713,6 +713,8 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/moby v28.0.0+incompatible h1:D+F1Z56b/DS8J5pUkTG/stemqrvHBQ006hUqJxjV9P0= github.com/moby/moby v28.0.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/mocktools/go-smtp-mock/v2 v2.4.0 h1:u0ky0iyNW/LEMKAFRTsDivHyP8dHYxe/cV3FZC3rRjo= @@ -757,14 +759,14 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= -github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= +github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= +github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= -github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= +github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= +github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE= github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0= github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= From e6dc6fb8c148beb65395c15f57c680dd3379a777 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:17:11 +0000 Subject: [PATCH 010/384] chore: bump github.com/open-policy-agent/opa from 1.1.0 to 1.3.0 (#17170) 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.1.0 to 1.3.0.
Release notes

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

v1.3.0

This release contains a mix of features, bugfixes, and dependency updates.

New Buffer Option for Decision Logs (#5724)

A new, optional, buffering mechanism has been added to decision logging. The default buffer is designed around making precise memory footprint guarantees, which can produce lock contention at high loads, negatively impacting query performance. The new event-based buffer is designed to reduce lock contention and improve performance at high loads, but sacrifices the memory footprint guarantees of the default buffer.

The new event-based buffer is enabled by setting the decision_logs.reporting.buffer_type configuration option to event.

For more details, see the decision log plugin README.

Reported by @​mjungsbluth, authored by @​sspaink

OpenTelemetry: HTTP Support and Expanded Batch Span Configuration (#7412)

Distributed tracing through OpenTelemetry has been extended to support HTTP collectors (enabled by setting the distributed_tracing.type configuration option to http). Additionally, configuration has been expanded with fine-grained batch span processor options.

Authored and reported by @​sqyang94

Runtime, Tooling, SDK

Docs, Website, Ecosystem

Miscellaneous

  • Enable unused-receiver linter (revive) (#7448) authored by @​anderseknert
  • Dependency updates; notably:
    • build(deps): bump github.com/containerd/containerd from 1.7.26 to 1.7.27
    • build(deps): bump github.com/dgraph-io/badger/v4 from 4.5.1 to 4.6.0
    • build(deps): bump github.com/opencontainers/image-spec from 1.1.0 to 1.1.1

... (truncated)

Changelog

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

1.3.0

This release contains a mix of features, bugfixes, and dependency updates.

New Buffer Option for Decision Logs (#5724)

A new, optional, buffering mechanism has been added to decision logging. The default buffer is designed around making precise memory footprint guarantees, which can produce lock contention at high loads, negatively impacting query performance. The new event-based buffer is designed to reduce lock contention and improve performance at high loads, but sacrifices the memory footprint guarantees of the default buffer.

The new event-based buffer is enabled by setting the decision_logs.reporting.buffer_type configuration option to event.

For more details, see the decision log plugin README.

Reported by @​mjungsbluth, authored by @​sspaink

OpenTelemetry: HTTP Support and Expanded Batch Span Configuration (#7412)

Distributed tracing through OpenTelemetry has been extended to support HTTP collectors (enabled by setting the distributed_tracing.type configuration option to http). Additionally, configuration has been expanded with fine-grained batch span processor options.

Authored and reported by @​sqyang94

Runtime, Tooling, SDK

Docs, Website, Ecosystem

Miscellaneous

  • Enable unused-receiver linter (revive) (#7448) authored by @​anderseknert
  • Dependency updates; notably:
    • build(deps): bump github.com/containerd/containerd from 1.7.26 to 1.7.27
    • build(deps): bump github.com/dgraph-io/badger/v4 from 4.5.1 to 4.6.0

... (truncated)

Commits
  • 89f4835 Prepare v1.3.0 release (#7467)
  • ee38d83 docs/envoy-tutorial-standalone: simplify 'kind' usage instruction (#7465)
  • 3d3b45f Delete reference to license key in envoy-tutorial-standalone-envoy.md (#7466)
  • 004af4c docs/envoy-tutorial-standalone: fix typo (#7464)
  • cd66fa3 feat: new event-based decisions log buffer implementation (#7446)
  • c8febc8 feat: add more distributed tracing options (#7421)
  • b3b87ff fmt: allow one liner rule grouping (#7453)
  • 92ae9a0 build(deps): bump github.com/containerd/containerd from 1.7.26 to 1.7.27 (#7451)
  • f3de100 docs: Update slack inviter link (#7450)
  • bd5ceb5 Enable unused-receiver linter (revive) (#7448)
  • 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.1.0&new-version=1.3.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 | 29 +++++++++++------------ go.sum | 74 ++++++++++++++++++++++++++++------------------------------ 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/go.mod b/go.mod index 5af9e57e99559..29e3eb769aa5a 100644 --- a/go.mod +++ b/go.mod @@ -152,14 +152,14 @@ 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.1.0 + github.com/open-policy-agent/opa v1.3.0 github.com/ory/dockertest/v3 v3.12.0 github.com/pion/udp v0.1.4 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/pkg/sftp v1.13.7 github.com/prometheus-community/pro-bing v0.6.0 - github.com/prometheus/client_golang v1.21.0 + github.com/prometheus/client_golang v1.21.1 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.63.0 github.com/quasilyte/go-ruleguard/dsl v0.3.21 @@ -167,7 +167,7 @@ require ( github.com/shirou/gopsutil/v4 v4.25.2 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/spf13/afero v1.14.0 - github.com/spf13/pflag v1.0.5 + github.com/spf13/pflag v1.0.6 github.com/sqlc-dev/pqtype v0.3.0 github.com/stretchr/testify v1.10.0 github.com/swaggo/http-swagger/v2 v2.0.1 @@ -180,11 +180,11 @@ require ( github.com/zclconf/go-cty-yaml v1.1.0 go.mozilla.org/pkcs7 v0.9.0 go.nhat.io/otelsql v0.15.0 - go.opentelemetry.io/otel v1.34.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 - go.opentelemetry.io/otel/sdk v1.34.0 - go.opentelemetry.io/otel/trace v1.34.0 + go.opentelemetry.io/otel v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 + go.opentelemetry.io/otel/sdk v1.35.0 + go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29 go.uber.org/mock v0.5.0 @@ -238,10 +238,9 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/agnivade/levenshtein v1.2.0 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect github.com/akutz/memconn v0.1.0 // indirect github.com/alecthomas/chroma/v2 v2.15.0 // indirect github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect @@ -325,7 +324,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect @@ -383,7 +382,7 @@ require ( github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/oklog/run v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect 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.3 // indirect @@ -448,8 +447,8 @@ require ( go.opentelemetry.io/collector/pdata/pprofile v0.104.0 // indirect go.opentelemetry.io/collector/semconv v0.104.0 // indirect go.opentelemetry.io/contrib v1.19.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect @@ -460,7 +459,7 @@ require ( golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index f658ab14fc7da..27d3a9587e6f8 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= -github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= @@ -74,8 +72,8 @@ github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= -github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= @@ -277,8 +275,8 @@ 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.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= -github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +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/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -471,8 +469,8 @@ github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 h1:4txT5G2kqVA github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= -github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -509,8 +507,8 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo= github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -632,8 +630,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3 h1:Z9/bo5PSeMutpdiKYNt/TTSfGM1Ll0naj3QzYX9VxTc= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= -github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA= -github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e h1:OP0ZMFeZkUnOzTFRfpuK3m7Kp4fNvC6qN+exwj7aI4M= github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -753,12 +749,12 @@ 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.1.0 h1:HMz2evdEMTyNqtdLjmu3Vyx06BmhNYAx67Yz3Ll9q2s= -github.com/open-policy-agent/opa v1.1.0/go.mod h1:T1pASQ1/vwfTa+e2fYcfpLCvWgYtqtiUv+IuA/dLPQs= +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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runc v1.2.3 h1:fxE7amCzfZflJO2lHXf4y/y8M1BoAqp+FVmG19oYB80= github.com/opencontainers/runc v1.2.3/go.mod h1:nSxcWUydXrsBZVYNSkTjoQ/N6rcyTtn+1SD5D4+kRIM= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= @@ -805,8 +801,8 @@ github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkB github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus-community/pro-bing v0.6.0 h1:04SZ/092gONTE1XUFzYFWqgB4mKwcdkqNChLMFedwhg= github.com/prometheus-community/pro-bing v0.6.0/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ= -github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA= -github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= @@ -859,8 +855,8 @@ github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1024,31 +1020,33 @@ go.opentelemetry.io/contrib v1.19.0 h1:rnYI7OEPMWFeM4QCqWQ3InMJ0arWMR1i0Cx9A5hcj go.opentelemetry.io/contrib v1.19.0/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= +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= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -1230,8 +1228,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= -google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= -google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= +google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= From 37ef4d8cb94e64d4ce1d28e55a5364c3c07b6793 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:18:05 +0000 Subject: [PATCH 011/384] chore: bump github.com/coreos/go-oidc/v3 from 3.13.0 to 3.14.1 (#17276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/coreos/go-oidc/v3](https://github.com/coreos/go-oidc) from 3.13.0 to 3.14.1.
Release notes

Sourced from github.com/coreos/go-oidc/v3's releases.

v3.14.1

What's Changed

Full Changelog: https://github.com/coreos/go-oidc/compare/v3.14.0...v3.14.1

v3.14.0

What's Changed

Full Changelog: https://github.com/coreos/go-oidc/compare/v3.13.0...v3.14.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/coreos/go-oidc/v3&package-manager=go_modules&previous-version=3.13.0&new-version=3.14.1)](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 29e3eb769aa5a..94c3a24959310 100644 --- a/go.mod +++ b/go.mod @@ -97,7 +97,7 @@ require ( github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e github.com/coder/websocket v1.8.12 github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 - github.com/coreos/go-oidc/v3 v3.13.0 + github.com/coreos/go-oidc/v3 v3.14.1 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/creack/pty v1.1.21 github.com/dave/dst v0.27.2 diff --git a/go.sum b/go.sum index 27d3a9587e6f8..7eea353180742 100644 --- a/go.sum +++ b/go.sum @@ -258,8 +258,8 @@ github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8= -github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= From 30f41cdd42ff35d1247607125d2e83afa02b6247 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:18:23 +0000 Subject: [PATCH 012/384] chore: bump github.com/valyala/fasthttp from 1.59.0 to 1.60.0 (#17274) 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.59.0 to 1.60.0.
Release notes

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

v1.60.0

What's Changed

New Contributors

Full Changelog: https://github.com/valyala/fasthttp/compare/v1.59.0...v1.60.0

Commits
  • 752b0e7 Remove idleConns mutex for every request (#1986)
  • bf3f552 chore(deps): bump golang.org/x/net from 0.37.0 to 0.38.0 (#1983)
  • 4891fc5 Update golangci-lint to v2 (#1980)
  • 30b09be Fix untyped int constant 4294967295
  • 4269e2d chore(deps): bump golang.org/x/net from 0.36.0 to 0.37.0 (#1971)
  • 1353ca5 chore(deps): bump securego/gosec from 2.22.1 to 2.22.2 (#1972)
  • 6c07c2f chore(deps): bump golang.org/x/net from 0.35.0 to 0.36.0 (#1968)
  • 69dc7b1 Update the supported version to the same as Go itself (#1967)
  • b8969ed Fix normalizeHeaderValue (#1963)
  • 31e34c5 add related project for opentelemetry-go-auto-instrumentation (#1962)
  • Additional commits viewable 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.59.0&new-version=1.60.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 94c3a24959310..42dde8033dc67 100644 --- a/go.mod +++ b/go.mod @@ -175,7 +175,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.59.0 + github.com/valyala/fasthttp v1.60.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 diff --git a/go.sum b/go.sum index 7eea353180742..4d09c0ece78b8 100644 --- a/go.sum +++ b/go.sum @@ -934,8 +934,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.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI= -github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU= +github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw= +github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuHgTO4FXCvc= 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= From 743d308eb3c8d3daaf9a7da42a14e8d24f1cec2d Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Mon, 7 Apr 2025 14:30:10 +0200 Subject: [PATCH 013/384] feat: support multiple terminal fonts (#17257) Fixes: https://github.com/coder/coder/issues/15024 --- coderd/apidoc/docs.go | 20 +++ coderd/apidoc/swagger.json | 17 ++- coderd/database/dbauthz/dbauthz.go | 66 ++++++--- coderd/database/dbauthz/dbauthz_test.go | 29 +++- coderd/database/dbmem/dbmem.go | 123 ++++++++++------ coderd/database/dbmetrics/querymetrics.go | 42 ++++-- coderd/database/dbmock/dbmock.go | 90 ++++++++---- coderd/database/querier.go | 6 +- coderd/database/queries.sql.go | 132 ++++++++++++------ coderd/database/queries/users.sql | 27 +++- coderd/users.go | 47 ++++++- coderd/users_test.go | 80 +++++++++++ codersdk/users.go | 39 +++++- docs/reference/api/schemas.md | 32 ++++- docs/reference/api/users.md | 3 + site/package.json | 1 + site/pnpm-lock.yaml | 8 ++ site/site.go | 13 +- site/src/api/queries/users.ts | 1 + site/src/api/typesGenerated.ts | 11 ++ .../TerminalPage/TerminalPage.stories.tsx | 34 +++++ site/src/pages/TerminalPage/TerminalPage.tsx | 20 ++- .../AppearancePage/AppearanceForm.stories.tsx | 2 +- .../AppearancePage/AppearanceForm.tsx | 119 +++++++++++++--- .../AppearancePage/AppearancePage.test.tsx | 27 +++- .../AppearancePage/AppearancePage.tsx | 32 ++--- site/src/testHelpers/entities.ts | 1 + site/src/theme/constants.ts | 16 +++ site/src/theme/globalFonts.ts | 3 + 29 files changed, 813 insertions(+), 228 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index ae566ee62208e..acd93fc7180cf 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15603,6 +15603,19 @@ const docTemplate = `{ "TemplateVersionWarningUnsupportedWorkspaces" ] }, + "codersdk.TerminalFontName": { + "type": "string", + "enum": [ + "", + "ibm-plex-mono", + "fira-code" + ], + "x-enum-varnames": [ + "TerminalFontUnknown", + "TerminalFontIBMPlexMono", + "TerminalFontFiraCode" + ] + }, "codersdk.TimingStage": { "type": "string", "enum": [ @@ -15776,9 +15789,13 @@ const docTemplate = `{ "codersdk.UpdateUserAppearanceSettingsRequest": { "type": "object", "required": [ + "terminal_font", "theme_preference" ], "properties": { + "terminal_font": { + "$ref": "#/definitions/codersdk.TerminalFontName" + }, "theme_preference": { "type": "string" } @@ -16070,6 +16087,9 @@ const docTemplate = `{ "codersdk.UserAppearanceSettings": { "type": "object", "properties": { + "terminal_font": { + "$ref": "#/definitions/codersdk.TerminalFontName" + }, "theme_preference": { "type": "string" } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 897ff44187a63..622c3865e0a6e 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -14188,6 +14188,15 @@ "enum": ["UNSUPPORTED_WORKSPACES"], "x-enum-varnames": ["TemplateVersionWarningUnsupportedWorkspaces"] }, + "codersdk.TerminalFontName": { + "type": "string", + "enum": ["", "ibm-plex-mono", "fira-code"], + "x-enum-varnames": [ + "TerminalFontUnknown", + "TerminalFontIBMPlexMono", + "TerminalFontFiraCode" + ] + }, "codersdk.TimingStage": { "type": "string", "enum": [ @@ -14358,8 +14367,11 @@ }, "codersdk.UpdateUserAppearanceSettingsRequest": { "type": "object", - "required": ["theme_preference"], + "required": ["terminal_font", "theme_preference"], "properties": { + "terminal_font": { + "$ref": "#/definitions/codersdk.TerminalFontName" + }, "theme_preference": { "type": "string" } @@ -14625,6 +14637,9 @@ "codersdk.UserAppearanceSettings": { "type": "object", "properties": { + "terminal_font": { + "$ref": "#/definitions/codersdk.TerminalFontName" + }, "theme_preference": { "type": "string" } diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index bb372aa4c9f48..980e7fd9c1941 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2721,17 +2721,6 @@ func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetU return q.db.GetUserActivityInsights(ctx, arg) } -func (q *querier) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) { - u, err := q.db.GetUserByID(ctx, userID) - if err != nil { - return "", err - } - if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { - return "", err - } - return q.db.GetUserAppearanceSettings(ctx, userID) -} - func (q *querier) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { return fetch(q.log, q.auth, q.db.GetUserByEmailOrUsername)(ctx, arg) } @@ -2804,6 +2793,28 @@ func (q *querier) GetUserStatusCounts(ctx context.Context, arg database.GetUserS return q.db.GetUserStatusCounts(ctx, arg) } +func (q *querier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + u, err := q.db.GetUserByID(ctx, userID) + if err != nil { + return "", err + } + if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { + return "", err + } + return q.db.GetUserTerminalFont(ctx, userID) +} + +func (q *querier) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) { + u, err := q.db.GetUserByID(ctx, userID) + if err != nil { + return "", err + } + if err := q.authorizeContext(ctx, policy.ActionReadPersonal, u); err != nil { + return "", err + } + return q.db.GetUserThemePreference(ctx, userID) +} + func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { u, err := q.db.GetUserByID(ctx, params.OwnerID) if err != nil { @@ -4321,17 +4332,6 @@ func (q *querier) UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg da return fetchAndExec(q.log, q.auth, policy.ActionUpdate, fetch, q.db.UpdateTemplateWorkspacesLastUsedAt)(ctx, arg) } -func (q *querier) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) { - u, err := q.db.GetUserByID(ctx, arg.UserID) - if err != nil { - return database.UserConfig{}, err - } - if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { - return database.UserConfig{}, err - } - return q.db.UpdateUserAppearanceSettings(ctx, arg) -} - func (q *querier) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { return deleteQ(q.log, q.auth, q.db.GetUserByID, q.db.UpdateUserDeletedByID)(ctx, id) } @@ -4469,6 +4469,28 @@ func (q *querier) UpdateUserStatus(ctx context.Context, arg database.UpdateUserS return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateUserStatus)(ctx, arg) } +func (q *querier) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) { + u, err := q.db.GetUserByID(ctx, arg.UserID) + if err != nil { + return database.UserConfig{}, err + } + if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { + return database.UserConfig{}, err + } + return q.db.UpdateUserTerminalFont(ctx, arg) +} + +func (q *querier) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) { + u, err := q.db.GetUserByID(ctx, arg.UserID) + if err != nil { + return database.UserConfig{}, err + } + if err := q.authorizeContext(ctx, policy.ActionUpdatePersonal, u); err != nil { + return database.UserConfig{}, err + } + return q.db.UpdateUserThemePreference(ctx, arg) +} + func (q *querier) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 7af3cace5112b..8cf58f1a360c4 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -1628,27 +1628,48 @@ func (s *MethodTestSuite) TestUser() { []database.GetUserWorkspaceBuildParametersRow{}, ) })) - s.Run("GetUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) { + s.Run("GetUserThemePreference", s.Subtest(func(db database.Store, check *expects) { ctx := context.Background() u := dbgen.User(s.T(), db, database.User{}) - db.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{ + db.UpdateUserThemePreference(ctx, database.UpdateUserThemePreferenceParams{ UserID: u.ID, ThemePreference: "light", }) check.Args(u.ID).Asserts(u, policy.ActionReadPersonal).Returns("light") })) - s.Run("UpdateUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) { + s.Run("UpdateUserThemePreference", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) uc := database.UserConfig{ UserID: u.ID, Key: "theme_preference", Value: "dark", } - check.Args(database.UpdateUserAppearanceSettingsParams{ + check.Args(database.UpdateUserThemePreferenceParams{ UserID: u.ID, ThemePreference: uc.Value, }).Asserts(u, policy.ActionUpdatePersonal).Returns(uc) })) + s.Run("GetUserTerminalFont", s.Subtest(func(db database.Store, check *expects) { + ctx := context.Background() + u := dbgen.User(s.T(), db, database.User{}) + db.UpdateUserTerminalFont(ctx, database.UpdateUserTerminalFontParams{ + UserID: u.ID, + TerminalFont: "ibm-plex-mono", + }) + check.Args(u.ID).Asserts(u, policy.ActionReadPersonal).Returns("ibm-plex-mono") + })) + s.Run("UpdateUserTerminalFont", s.Subtest(func(db database.Store, check *expects) { + u := dbgen.User(s.T(), db, database.User{}) + uc := database.UserConfig{ + UserID: u.ID, + Key: "terminal_font", + Value: "ibm-plex-mono", + } + check.Args(database.UpdateUserTerminalFontParams{ + UserID: u.ID, + TerminalFont: uc.Value, + }).Asserts(u, policy.ActionUpdatePersonal).Returns(uc) + })) s.Run("UpdateUserStatus", s.Subtest(func(db database.Store, check *expects) { u := dbgen.User(s.T(), db, database.User{}) check.Args(database.UpdateUserStatusParams{ diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 9d2bdd7a1ad81..d21da315ffa85 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6448,20 +6448,6 @@ func (q *FakeQuerier) GetUserActivityInsights(_ context.Context, arg database.Ge return rows, nil } -func (q *FakeQuerier) GetUserAppearanceSettings(_ context.Context, userID uuid.UUID) (string, error) { - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, uc := range q.userConfigs { - if uc.UserID != userID || uc.Key != "theme_preference" { - continue - } - return uc.Value, nil - } - - return "", sql.ErrNoRows -} - func (q *FakeQuerier) GetUserByEmailOrUsername(_ context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { if err := validateDatabaseType(arg); err != nil { return database.User{}, err @@ -6674,6 +6660,34 @@ func (q *FakeQuerier) GetUserStatusCounts(_ context.Context, arg database.GetUse return result, nil } +func (q *FakeQuerier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, uc := range q.userConfigs { + if uc.UserID != userID || uc.Key != "terminal_font" { + continue + } + return uc.Value, nil + } + + return "", sql.ErrNoRows +} + +func (q *FakeQuerier) GetUserThemePreference(_ context.Context, userID uuid.UUID) (string, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + for _, uc := range q.userConfigs { + if uc.UserID != userID || uc.Key != "theme_preference" { + continue + } + return uc.Value, nil + } + + return "", sql.ErrNoRows +} + func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -11015,33 +11029,6 @@ func (q *FakeQuerier) UpdateTemplateWorkspacesLastUsedAt(_ context.Context, arg return nil } -func (q *FakeQuerier) UpdateUserAppearanceSettings(_ context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) { - err := validateDatabaseType(arg) - if err != nil { - return database.UserConfig{}, err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for i, uc := range q.userConfigs { - if uc.UserID != arg.UserID || uc.Key != "theme_preference" { - continue - } - uc.Value = arg.ThemePreference - q.userConfigs[i] = uc - return uc, nil - } - - uc := database.UserConfig{ - UserID: arg.UserID, - Key: "theme_preference", - Value: arg.ThemePreference, - } - q.userConfigs = append(q.userConfigs, uc) - return uc, nil -} - func (q *FakeQuerier) UpdateUserDeletedByID(_ context.Context, id uuid.UUID) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -11367,6 +11354,60 @@ func (q *FakeQuerier) UpdateUserStatus(_ context.Context, arg database.UpdateUse return database.User{}, sql.ErrNoRows } +func (q *FakeQuerier) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.UserConfig{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, uc := range q.userConfigs { + if uc.UserID != arg.UserID || uc.Key != "terminal_font" { + continue + } + uc.Value = arg.TerminalFont + q.userConfigs[i] = uc + return uc, nil + } + + uc := database.UserConfig{ + UserID: arg.UserID, + Key: "terminal_font", + Value: arg.TerminalFont, + } + q.userConfigs = append(q.userConfigs, uc) + return uc, nil +} + +func (q *FakeQuerier) UpdateUserThemePreference(_ context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.UserConfig{}, err + } + + q.mutex.Lock() + defer q.mutex.Unlock() + + for i, uc := range q.userConfigs { + if uc.UserID != arg.UserID || uc.Key != "theme_preference" { + continue + } + uc.Value = arg.ThemePreference + q.userConfigs[i] = uc + return uc, nil + } + + uc := database.UserConfig{ + UserID: arg.UserID, + Key: "theme_preference", + Value: arg.ThemePreference, + } + q.userConfigs = append(q.userConfigs, uc) + return uc, nil +} + func (q *FakeQuerier) UpdateVolumeResourceMonitor(_ context.Context, arg database.UpdateVolumeResourceMonitorParams) error { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index a70b4842c7fb9..c90d083fa20c7 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -1509,13 +1509,6 @@ func (m queryMetricsStore) GetUserActivityInsights(ctx context.Context, arg data return r0, r1 } -func (m queryMetricsStore) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) { - start := time.Now() - r0, r1 := m.s.GetUserAppearanceSettings(ctx, userID) - m.queryLatencies.WithLabelValues("GetUserAppearanceSettings").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { start := time.Now() user, err := m.s.GetUserByEmailOrUsername(ctx, arg) @@ -1579,6 +1572,20 @@ func (m queryMetricsStore) GetUserStatusCounts(ctx context.Context, arg database return r0, r1 } +func (m queryMetricsStore) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + start := time.Now() + r0, r1 := m.s.GetUserTerminalFont(ctx, userID) + m.queryLatencies.WithLabelValues("GetUserTerminalFont").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) { + start := time.Now() + r0, r1 := m.s.GetUserThemePreference(ctx, userID) + m.queryLatencies.WithLabelValues("GetUserThemePreference").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) GetUserWorkspaceBuildParameters(ctx context.Context, ownerID database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { start := time.Now() r0, r1 := m.s.GetUserWorkspaceBuildParameters(ctx, ownerID) @@ -2734,13 +2741,6 @@ func (m queryMetricsStore) UpdateTemplateWorkspacesLastUsedAt(ctx context.Contex return r0 } -func (m queryMetricsStore) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) { - start := time.Now() - r0, r1 := m.s.UpdateUserAppearanceSettings(ctx, arg) - m.queryLatencies.WithLabelValues("UpdateUserAppearanceSettings").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { start := time.Now() r0 := m.s.UpdateUserDeletedByID(ctx, id) @@ -2832,6 +2832,20 @@ func (m queryMetricsStore) UpdateUserStatus(ctx context.Context, arg database.Up return user, err } +func (m queryMetricsStore) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) { + start := time.Now() + r0, r1 := m.s.UpdateUserTerminalFont(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateUserTerminalFont").Observe(time.Since(start).Seconds()) + return r0, r1 +} + +func (m queryMetricsStore) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) { + start := time.Now() + r0, r1 := m.s.UpdateUserThemePreference(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateUserThemePreference").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { start := time.Now() r0 := m.s.UpdateVolumeResourceMonitor(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 8ebb37178182d..e015a72094aa9 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3154,21 +3154,6 @@ func (mr *MockStoreMockRecorder) GetUserActivityInsights(ctx, arg any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserActivityInsights", reflect.TypeOf((*MockStore)(nil).GetUserActivityInsights), ctx, arg) } -// GetUserAppearanceSettings mocks base method. -func (m *MockStore) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUserAppearanceSettings", ctx, userID) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetUserAppearanceSettings indicates an expected call of GetUserAppearanceSettings. -func (mr *MockStoreMockRecorder) GetUserAppearanceSettings(ctx, userID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserAppearanceSettings", reflect.TypeOf((*MockStore)(nil).GetUserAppearanceSettings), ctx, userID) -} - // GetUserByEmailOrUsername mocks base method. func (m *MockStore) GetUserByEmailOrUsername(ctx context.Context, arg database.GetUserByEmailOrUsernameParams) (database.User, error) { m.ctrl.T.Helper() @@ -3304,6 +3289,36 @@ func (mr *MockStoreMockRecorder) GetUserStatusCounts(ctx, arg any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserStatusCounts", reflect.TypeOf((*MockStore)(nil).GetUserStatusCounts), ctx, arg) } +// GetUserTerminalFont mocks base method. +func (m *MockStore) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserTerminalFont", ctx, userID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserTerminalFont indicates an expected call of GetUserTerminalFont. +func (mr *MockStoreMockRecorder) GetUserTerminalFont(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserTerminalFont", reflect.TypeOf((*MockStore)(nil).GetUserTerminalFont), ctx, userID) +} + +// GetUserThemePreference mocks base method. +func (m *MockStore) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserThemePreference", ctx, userID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserThemePreference indicates an expected call of GetUserThemePreference. +func (mr *MockStoreMockRecorder) GetUserThemePreference(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserThemePreference", reflect.TypeOf((*MockStore)(nil).GetUserThemePreference), ctx, userID) +} + // GetUserWorkspaceBuildParameters mocks base method. func (m *MockStore) GetUserWorkspaceBuildParameters(ctx context.Context, arg database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { m.ctrl.T.Helper() @@ -5783,21 +5798,6 @@ func (mr *MockStoreMockRecorder) UpdateTemplateWorkspacesLastUsedAt(ctx, arg any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTemplateWorkspacesLastUsedAt", reflect.TypeOf((*MockStore)(nil).UpdateTemplateWorkspacesLastUsedAt), ctx, arg) } -// UpdateUserAppearanceSettings mocks base method. -func (m *MockStore) UpdateUserAppearanceSettings(ctx context.Context, arg database.UpdateUserAppearanceSettingsParams) (database.UserConfig, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUserAppearanceSettings", ctx, arg) - ret0, _ := ret[0].(database.UserConfig) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpdateUserAppearanceSettings indicates an expected call of UpdateUserAppearanceSettings. -func (mr *MockStoreMockRecorder) UpdateUserAppearanceSettings(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserAppearanceSettings", reflect.TypeOf((*MockStore)(nil).UpdateUserAppearanceSettings), ctx, arg) -} - // UpdateUserDeletedByID mocks base method. func (m *MockStore) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error { m.ctrl.T.Helper() @@ -5989,6 +5989,36 @@ func (mr *MockStoreMockRecorder) UpdateUserStatus(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserStatus", reflect.TypeOf((*MockStore)(nil).UpdateUserStatus), ctx, arg) } +// UpdateUserTerminalFont mocks base method. +func (m *MockStore) UpdateUserTerminalFont(ctx context.Context, arg database.UpdateUserTerminalFontParams) (database.UserConfig, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserTerminalFont", ctx, arg) + ret0, _ := ret[0].(database.UserConfig) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUserTerminalFont indicates an expected call of UpdateUserTerminalFont. +func (mr *MockStoreMockRecorder) UpdateUserTerminalFont(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserTerminalFont", reflect.TypeOf((*MockStore)(nil).UpdateUserTerminalFont), ctx, arg) +} + +// UpdateUserThemePreference mocks base method. +func (m *MockStore) UpdateUserThemePreference(ctx context.Context, arg database.UpdateUserThemePreferenceParams) (database.UserConfig, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserThemePreference", ctx, arg) + ret0, _ := ret[0].(database.UserConfig) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateUserThemePreference indicates an expected call of UpdateUserThemePreference. +func (mr *MockStoreMockRecorder) UpdateUserThemePreference(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserThemePreference", reflect.TypeOf((*MockStore)(nil).UpdateUserThemePreference), ctx, arg) +} + // UpdateVolumeResourceMonitor mocks base method. func (m *MockStore) UpdateVolumeResourceMonitor(ctx context.Context, arg database.UpdateVolumeResourceMonitorParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 880a5ce4a093d..7494cbc04b770 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -344,7 +344,6 @@ type sqlcQuerier interface { // produces a bloated value if a user has used multiple templates // simultaneously. GetUserActivityInsights(ctx context.Context, arg GetUserActivityInsightsParams) ([]GetUserActivityInsightsRow, error) - GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) GetUserCount(ctx context.Context, includeSystem bool) (int64, error) @@ -370,6 +369,8 @@ type sqlcQuerier interface { // We do not start counting from 0 at the start_time. We check the last status change before the start_time for each user. As such, // the result shows the total number of users in each status on any particular day. GetUserStatusCounts(ctx context.Context, arg GetUserStatusCountsParams) ([]GetUserStatusCountsRow, error) + GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) + GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error) // This will never return deleted users. GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error) @@ -571,7 +572,6 @@ type sqlcQuerier interface { UpdateTemplateVersionDescriptionByJobID(ctx context.Context, arg UpdateTemplateVersionDescriptionByJobIDParams) error UpdateTemplateVersionExternalAuthProvidersByJobID(ctx context.Context, arg UpdateTemplateVersionExternalAuthProvidersByJobIDParams) error UpdateTemplateWorkspacesLastUsedAt(ctx context.Context, arg UpdateTemplateWorkspacesLastUsedAtParams) error - UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (UserConfig, error) UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error UpdateUserHashedOneTimePasscode(ctx context.Context, arg UpdateUserHashedOneTimePasscodeParams) error @@ -585,6 +585,8 @@ type sqlcQuerier interface { UpdateUserQuietHoursSchedule(ctx context.Context, arg UpdateUserQuietHoursScheduleParams) (User, error) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error) + UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) + UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) UpdateVolumeResourceMonitor(ctx context.Context, arg UpdateVolumeResourceMonitorParams) error UpdateWorkspace(ctx context.Context, arg UpdateWorkspaceParams) (WorkspaceTable, error) UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 653d3d3136e63..55a3bd27e5e3f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12196,23 +12196,6 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid. return i, err } -const getUserAppearanceSettings = `-- name: GetUserAppearanceSettings :one -SELECT - value as theme_preference -FROM - user_configs -WHERE - user_id = $1 - AND key = 'theme_preference' -` - -func (q *sqlQuerier) GetUserAppearanceSettings(ctx context.Context, userID uuid.UUID) (string, error) { - row := q.db.QueryRowContext(ctx, getUserAppearanceSettings, userID) - var theme_preference string - err := row.Scan(&theme_preference) - return theme_preference, err -} - const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system @@ -12310,6 +12293,40 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context, includeSystem bool) (int6 return count, err } +const getUserTerminalFont = `-- name: GetUserTerminalFont :one +SELECT + value as terminal_font +FROM + user_configs +WHERE + user_id = $1 + AND key = 'terminal_font' +` + +func (q *sqlQuerier) GetUserTerminalFont(ctx context.Context, userID uuid.UUID) (string, error) { + row := q.db.QueryRowContext(ctx, getUserTerminalFont, userID) + var terminal_font string + err := row.Scan(&terminal_font) + return terminal_font, err +} + +const getUserThemePreference = `-- name: GetUserThemePreference :one +SELECT + value as theme_preference +FROM + user_configs +WHERE + user_id = $1 + AND key = 'theme_preference' +` + +func (q *sqlQuerier) GetUserThemePreference(ctx context.Context, userID uuid.UUID) (string, error) { + row := q.db.QueryRowContext(ctx, getUserThemePreference, userID) + var theme_preference string + err := row.Scan(&theme_preference) + return theme_preference, err +} + const getUsers = `-- name: GetUsers :many SELECT id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, login_type, avatar_url, deleted, last_seen_at, quiet_hours_schedule, name, github_com_user_id, hashed_one_time_passcode, one_time_passcode_expires_at, is_system, COUNT(*) OVER() AS count @@ -12673,33 +12690,6 @@ func (q *sqlQuerier) UpdateInactiveUsersToDormant(ctx context.Context, arg Updat return items, nil } -const updateUserAppearanceSettings = `-- name: UpdateUserAppearanceSettings :one -INSERT INTO - user_configs (user_id, key, value) -VALUES - ($1, 'theme_preference', $2) -ON CONFLICT - ON CONSTRAINT user_configs_pkey -DO UPDATE -SET - value = $2 -WHERE user_configs.user_id = $1 - AND user_configs.key = 'theme_preference' -RETURNING user_id, key, value -` - -type UpdateUserAppearanceSettingsParams struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` - ThemePreference string `db:"theme_preference" json:"theme_preference"` -} - -func (q *sqlQuerier) UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (UserConfig, error) { - row := q.db.QueryRowContext(ctx, updateUserAppearanceSettings, arg.UserID, arg.ThemePreference) - var i UserConfig - err := row.Scan(&i.UserID, &i.Key, &i.Value) - return i, err -} - const updateUserDeletedByID = `-- name: UpdateUserDeletedByID :exec UPDATE users @@ -13047,6 +13037,60 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP return i, err } +const updateUserTerminalFont = `-- name: UpdateUserTerminalFont :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + ($1, 'terminal_font', $2) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = $2 +WHERE user_configs.user_id = $1 + AND user_configs.key = 'terminal_font' +RETURNING user_id, key, value +` + +type UpdateUserTerminalFontParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + TerminalFont string `db:"terminal_font" json:"terminal_font"` +} + +func (q *sqlQuerier) UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error) { + row := q.db.QueryRowContext(ctx, updateUserTerminalFont, arg.UserID, arg.TerminalFont) + var i UserConfig + err := row.Scan(&i.UserID, &i.Key, &i.Value) + return i, err +} + +const updateUserThemePreference = `-- name: UpdateUserThemePreference :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + ($1, 'theme_preference', $2) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = $2 +WHERE user_configs.user_id = $1 + AND user_configs.key = 'theme_preference' +RETURNING user_id, key, value +` + +type UpdateUserThemePreferenceParams struct { + UserID uuid.UUID `db:"user_id" json:"user_id"` + ThemePreference string `db:"theme_preference" json:"theme_preference"` +} + +func (q *sqlQuerier) UpdateUserThemePreference(ctx context.Context, arg UpdateUserThemePreferenceParams) (UserConfig, error) { + row := q.db.QueryRowContext(ctx, updateUserThemePreference, arg.UserID, arg.ThemePreference) + var i UserConfig + err := row.Scan(&i.UserID, &i.Key, &i.Value) + return i, err +} + const getWorkspaceAgentDevcontainersByAgentID = `-- name: GetWorkspaceAgentDevcontainersByAgentID :many SELECT id, workspace_agent_id, created_at, workspace_folder, config_path, name diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index c4304cfc3e60e..0bac76c8df14a 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -102,7 +102,7 @@ SET WHERE id = $1; --- name: GetUserAppearanceSettings :one +-- name: GetUserThemePreference :one SELECT value as theme_preference FROM @@ -111,7 +111,7 @@ WHERE user_id = @user_id AND key = 'theme_preference'; --- name: UpdateUserAppearanceSettings :one +-- name: UpdateUserThemePreference :one INSERT INTO user_configs (user_id, key, value) VALUES @@ -125,6 +125,29 @@ WHERE user_configs.user_id = @user_id AND user_configs.key = 'theme_preference' RETURNING *; +-- name: GetUserTerminalFont :one +SELECT + value as terminal_font +FROM + user_configs +WHERE + user_id = @user_id + AND key = 'terminal_font'; + +-- name: UpdateUserTerminalFont :one +INSERT INTO + user_configs (user_id, key, value) +VALUES + (@user_id, 'terminal_font', @terminal_font) +ON CONFLICT + ON CONSTRAINT user_configs_pkey +DO UPDATE +SET + value = @terminal_font +WHERE user_configs.user_id = @user_id + AND user_configs.key = 'terminal_font' +RETURNING *; + -- name: UpdateUserRoles :one UPDATE users diff --git a/coderd/users.go b/coderd/users.go index 069e1fc240302..03f900c01ddeb 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "slices" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -976,7 +977,7 @@ func (api *API) userAppearanceSettings(rw http.ResponseWriter, r *http.Request) user = httpmw.UserParam(r) ) - themePreference, err := api.Database.GetUserAppearanceSettings(ctx, user.ID) + themePreference, err := api.Database.GetUserThemePreference(ctx, user.ID) if err != nil { if !errors.Is(err, sql.ErrNoRows) { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -989,8 +990,22 @@ func (api *API) userAppearanceSettings(rw http.ResponseWriter, r *http.Request) themePreference = "" } + terminalFont, err := api.Database.GetUserTerminalFont(ctx, user.ID) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Error reading user settings.", + Detail: err.Error(), + }) + return + } + + terminalFont = "" + } + httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{ ThemePreference: themePreference, + TerminalFont: codersdk.TerminalFontName(terminalFont), }) } @@ -1015,23 +1030,47 @@ func (api *API) putUserAppearanceSettings(rw http.ResponseWriter, r *http.Reques return } - updatedSettings, err := api.Database.UpdateUserAppearanceSettings(ctx, database.UpdateUserAppearanceSettingsParams{ + if !isValidFontName(params.TerminalFont) { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Unsupported font family.", + }) + return + } + + updatedThemePreference, err := api.Database.UpdateUserThemePreference(ctx, database.UpdateUserThemePreferenceParams{ UserID: user.ID, ThemePreference: params.ThemePreference, }) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error updating user.", + Message: "Internal error updating user theme preference.", + Detail: err.Error(), + }) + return + } + + updatedTerminalFont, err := api.Database.UpdateUserTerminalFont(ctx, database.UpdateUserTerminalFontParams{ + UserID: user.ID, + TerminalFont: string(params.TerminalFont), + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error updating user terminal font.", Detail: err.Error(), }) return } httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserAppearanceSettings{ - ThemePreference: updatedSettings.Value, + ThemePreference: updatedThemePreference.Value, + TerminalFont: codersdk.TerminalFontName(updatedTerminalFont.Value), }) } +func isValidFontName(font codersdk.TerminalFontName) bool { + return slices.Contains(codersdk.TerminalFontNames, font) +} + // @Summary Update user password // @ID update-user-password // @Security CoderSessionToken diff --git a/coderd/users_test.go b/coderd/users_test.go index c21eca85a5ee7..fdaad21a826a9 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1972,6 +1972,86 @@ func TestPostTokens(t *testing.T) { require.NoError(t, err) } +func TestUserTerminalFont(t *testing.T) { + t.Parallel() + + t.Run("valid font", func(t *testing.T) { + t.Parallel() + + adminClient := coderdtest.New(t, nil) + firstUser := coderdtest.CreateFirstUser(t, adminClient) + client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // given + initial, err := client.GetUserAppearanceSettings(ctx, "me") + require.NoError(t, err) + require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont) + + // when + updated, err := client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{ + ThemePreference: "light", + TerminalFont: "fira-code", + }) + require.NoError(t, err) + + // then + require.Equal(t, codersdk.TerminalFontFiraCode, updated.TerminalFont) + }) + + t.Run("unsupported font", func(t *testing.T) { + t.Parallel() + + adminClient := coderdtest.New(t, nil) + firstUser := coderdtest.CreateFirstUser(t, adminClient) + client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // given + initial, err := client.GetUserAppearanceSettings(ctx, "me") + require.NoError(t, err) + require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont) + + // when + _, err = client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{ + ThemePreference: "light", + TerminalFont: "foobar", + }) + + // then + require.Error(t, err) + }) + + t.Run("undefined font is not ok", func(t *testing.T) { + t.Parallel() + + adminClient := coderdtest.New(t, nil) + firstUser := coderdtest.CreateFirstUser(t, adminClient) + client, _ := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // given + initial, err := client.GetUserAppearanceSettings(ctx, "me") + require.NoError(t, err) + require.Equal(t, codersdk.TerminalFontName(""), initial.TerminalFont) + + // when + _, err = client.UpdateUserAppearanceSettings(ctx, "me", codersdk.UpdateUserAppearanceSettingsRequest{ + ThemePreference: "light", + TerminalFont: "", + }) + + // then + require.Error(t, err) + }) +} + func TestWorkspacesByUser(t *testing.T) { t.Parallel() t.Run("Empty", func(t *testing.T) { diff --git a/codersdk/users.go b/codersdk/users.go index 31854731a0ae1..bdc9b521367f0 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -189,12 +189,25 @@ type ValidateUserPasswordResponse struct { Details string `json:"details"` } +// TerminalFontName is the name of supported terminal font +type TerminalFontName string + +var TerminalFontNames = []TerminalFontName{TerminalFontUnknown, TerminalFontIBMPlexMono, TerminalFontFiraCode} + +const ( + TerminalFontUnknown TerminalFontName = "" + TerminalFontIBMPlexMono TerminalFontName = "ibm-plex-mono" + TerminalFontFiraCode TerminalFontName = "fira-code" +) + type UserAppearanceSettings struct { - ThemePreference string `json:"theme_preference"` + ThemePreference string `json:"theme_preference"` + TerminalFont TerminalFontName `json:"terminal_font"` } type UpdateUserAppearanceSettingsRequest struct { - ThemePreference string `json:"theme_preference" validate:"required"` + ThemePreference string `json:"theme_preference" validate:"required"` + TerminalFont TerminalFontName `json:"terminal_font" validate:"required"` } type UpdateUserPasswordRequest struct { @@ -466,17 +479,31 @@ func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserS return resp, json.NewDecoder(res.Body).Decode(&resp) } +// GetUserAppearanceSettings fetches the appearance settings for a user. +func (c *Client) GetUserAppearanceSettings(ctx context.Context, user string) (UserAppearanceSettings, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/appearance", user), nil) + if err != nil { + return UserAppearanceSettings{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return UserAppearanceSettings{}, ReadBodyAsError(res) + } + var resp UserAppearanceSettings + return resp, json.NewDecoder(res.Body).Decode(&resp) +} + // UpdateUserAppearanceSettings updates the appearance settings for a user. -func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, req UpdateUserAppearanceSettingsRequest) (User, error) { +func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, req UpdateUserAppearanceSettingsRequest) (UserAppearanceSettings, error) { res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/appearance", user), req) if err != nil { - return User{}, err + return UserAppearanceSettings{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return User{}, ReadBodyAsError(res) + return UserAppearanceSettings{}, ReadBodyAsError(res) } - var resp User + var resp UserAppearanceSettings return resp, json.NewDecoder(res.Body).Decode(&resp) } diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 0fbf87e8e5ff9..fa9604cff6c9b 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -6717,6 +6717,22 @@ Restarts will only happen on weekdays in this list on weeks which line up with W |--------------------------| | `UNSUPPORTED_WORKSPACES` | +## codersdk.TerminalFontName + +```json +"" +``` + +### Properties + +#### Enumerated Values + +| Value | +|-----------------| +| `` | +| `ibm-plex-mono` | +| `fira-code` | + ## codersdk.TimingStage ```json @@ -6914,15 +6930,17 @@ Restarts will only happen on weekdays in this list on weeks which line up with W ```json { + "terminal_font": "", "theme_preference": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|--------|----------|--------------|-------------| -| `theme_preference` | string | true | | | +| Name | Type | Required | Restrictions | Description | +|--------------------|--------------------------------------------------------|----------|--------------|-------------| +| `terminal_font` | [codersdk.TerminalFontName](#codersdkterminalfontname) | true | | | +| `theme_preference` | string | true | | | ## codersdk.UpdateUserNotificationPreferences @@ -7265,15 +7283,17 @@ If the schedule is empty, the user will be updated to use the default schedule.| ```json { + "terminal_font": "", "theme_preference": "string" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|--------------------|--------|----------|--------------|-------------| -| `theme_preference` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|--------------------|--------------------------------------------------------|----------|--------------|-------------| +| `terminal_font` | [codersdk.TerminalFontName](#codersdkterminalfontname) | false | | | +| `theme_preference` | string | false | | | ## codersdk.UserLatency diff --git a/docs/reference/api/users.md b/docs/reference/api/users.md index 3f0c38571f7c4..43842fde6539b 100644 --- a/docs/reference/api/users.md +++ b/docs/reference/api/users.md @@ -501,6 +501,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/appearance \ ```json { + "terminal_font": "", "theme_preference": "string" } ``` @@ -531,6 +532,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \ ```json { + "terminal_font": "", "theme_preference": "string" } ``` @@ -548,6 +550,7 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \ ```json { + "terminal_font": "", "theme_preference": "string" } ``` diff --git a/site/package.json b/site/package.json index 750b2e482f36c..2b5104ddcb283 100644 --- a/site/package.json +++ b/site/package.json @@ -42,6 +42,7 @@ "@emotion/styled": "11.14.0", "@fastly/performance-observer-polyfill": "2.0.0", "@fontsource-variable/inter": "5.1.1", + "@fontsource/fira-code": "5.2.5", "@fontsource/ibm-plex-mono": "5.1.1", "@monaco-editor/react": "4.6.0", "@mui/icons-material": "5.16.14", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 8c1bfd1e5b06e..7a6dac0d026b6 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -40,6 +40,9 @@ importers: '@fontsource-variable/inter': specifier: 5.1.1 version: 5.1.1 + '@fontsource/fira-code': + specifier: 5.2.5 + version: 5.2.5 '@fontsource/ibm-plex-mono': specifier: 5.1.1 version: 5.1.1 @@ -1040,6 +1043,9 @@ packages: '@fontsource-variable/inter@5.1.1': resolution: {integrity: sha512-OpXFTmiH6tHkYijMvQTycFKBLK4X+SRV6tet1m4YOUH7SzIIlMqDja+ocDtiCA72UthBH/vF+3ZtlMr2rN/wIw==, tarball: https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.1.1.tgz} + '@fontsource/fira-code@5.2.5': + resolution: {integrity: sha512-Rn9PJoyfRr5D6ukEhZpzhpD+rbX2rtoz9QjkOuGxqFxrL69fQvhadMUBxQIOuTF4sTTkPRSKlAEpPjTKaI12QA==, tarball: https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.2.5.tgz} + '@fontsource/ibm-plex-mono@5.1.1': resolution: {integrity: sha512-1aayqPe/ZkD3MlvqpmOHecfA3f2B8g+fAEkgvcCd3lkPP0pS1T0xG5Zmn2EsJQqr1JURtugPUH+5NqvKyfFZMQ==, tarball: https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.1.1.tgz} @@ -7012,6 +7018,8 @@ snapshots: '@fontsource-variable/inter@5.1.1': {} + '@fontsource/fira-code@5.2.5': {} + '@fontsource/ibm-plex-mono@5.1.1': {} '@humanwhocodes/config-array@0.11.14': diff --git a/site/site.go b/site/site.go index f4d5509479db5..e47e15848cda0 100644 --- a/site/site.go +++ b/site/site.go @@ -428,6 +428,7 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht var eg errgroup.Group var user database.User var themePreference string + var terminalFont string orgIDs := []uuid.UUID{} eg.Go(func() error { var err error @@ -436,13 +437,22 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht }) eg.Go(func() error { var err error - themePreference, err = h.opts.Database.GetUserAppearanceSettings(ctx, apiKey.UserID) + themePreference, err = h.opts.Database.GetUserThemePreference(ctx, apiKey.UserID) if errors.Is(err, sql.ErrNoRows) { themePreference = "" return nil } return err }) + eg.Go(func() error { + var err error + terminalFont, err = h.opts.Database.GetUserTerminalFont(ctx, apiKey.UserID) + if errors.Is(err, sql.ErrNoRows) { + terminalFont = "" + return nil + } + return err + }) eg.Go(func() error { memberIDs, err := h.opts.Database.GetOrganizationIDsByMemberIDs(ctx, []uuid.UUID{apiKey.UserID}) if errors.Is(err, sql.ErrNoRows) || len(memberIDs) == 0 { @@ -471,6 +481,7 @@ func (h *Handler) renderHTMLWithState(r *http.Request, filePath string, state ht defer wg.Done() userAppearance, err := json.Marshal(codersdk.UserAppearanceSettings{ ThemePreference: themePreference, + TerminalFont: codersdk.TerminalFontName(terminalFont), }) if err == nil { state.UserAppearance = html.EscapeString(string(userAppearance)) diff --git a/site/src/api/queries/users.ts b/site/src/api/queries/users.ts index 5de828b6eac22..82b10213b4409 100644 --- a/site/src/api/queries/users.ts +++ b/site/src/api/queries/users.ts @@ -251,6 +251,7 @@ export const updateAppearanceSettings = ( // more responsive. queryClient.setQueryData(myAppearanceKey, { theme_preference: patch.theme_preference, + terminal_font: patch.terminal_font, }); }, onSuccess: async () => diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index eb14392ed408a..1197d6b6e109e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2657,6 +2657,15 @@ export interface TemplateVersionsByTemplateRequest extends Pagination { readonly include_archived: boolean; } +// From codersdk/users.go +export type TerminalFontName = "fira-code" | "ibm-plex-mono" | ""; + +export const TerminalFontNames: TerminalFontName[] = [ + "fira-code", + "ibm-plex-mono", + "", +]; + // From codersdk/workspacebuilds.go export type TimingStage = | "apply" @@ -2790,6 +2799,7 @@ export interface UpdateTemplateMeta { // From codersdk/users.go export interface UpdateUserAppearanceSettingsRequest { readonly theme_preference: string; + readonly terminal_font: TerminalFontName; } // From codersdk/notifications.go @@ -2906,6 +2916,7 @@ export interface UserActivityInsightsResponse { // From codersdk/users.go export interface UserAppearanceSettings { readonly theme_preference: string; + readonly terminal_font: TerminalFontName; } // From codersdk/insights.go diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index 4cf052668bb06..aa24485353894 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -17,6 +17,7 @@ import { MockEntitlements, MockExperiments, MockUser, + MockUserAppearanceSettings, MockWorkspace, MockWorkspaceAgent, } from "testHelpers/entities"; @@ -76,6 +77,7 @@ const meta = { key: getAuthorizationKey({ checks: permissionChecks }), data: { editWorkspaceProxies: true }, }, + { key: ["me", "appearance"], data: MockUserAppearanceSettings }, ], chromatic: { delay: 300 }, }, @@ -106,6 +108,38 @@ export const Starting: Story = { }, }; +export const FontFiraCode: Story = { + decorators: [withWebSocket], + parameters: { + ...meta.parameters, + webSocket: [ + { + event: "message", + // Copied and pasted this from browser + data: "➜ codergit:(bq/refactor-web-term-notifications) ✗", + }, + ], + queries: [ + ...meta.parameters.queries.filter( + (q) => + !( + Array.isArray(q.key) && + q.key[0] === "me" && + q.key[1] === "appearance" + ), + ), + { + key: ["me", "appearance"], + data: { + ...MockUserAppearanceSettings, + terminal_font: "fira-code", + }, + }, + createWorkspaceWithAgent("ready"), + ], + }, +}; + export const Ready: Story = { decorators: [withWebSocket], parameters: { diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index c86a3f9ed5396..9740e239233a4 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -7,18 +7,20 @@ import { WebLinksAddon } from "@xterm/addon-web-links"; import { WebglAddon } from "@xterm/addon-webgl"; import { Terminal } from "@xterm/xterm"; import { deploymentConfig } from "api/queries/deployment"; +import { appearanceSettings } from "api/queries/users"; import { workspaceByOwnerAndName, workspaceUsage, } from "api/queries/workspaces"; import { useProxy } from "contexts/ProxyContext"; import { ThemeOverride } from "contexts/ThemeProvider"; +import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { type FC, useCallback, useEffect, useRef, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useQuery } from "react-query"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import themes from "theme"; -import { MONOSPACE_FONT_FAMILY } from "theme/constants"; +import { DEFAULT_TERMINAL_FONT, terminalFonts } from "theme/constants"; import { pageTitle } from "utils/page"; import { openMaybePortForwardedURL } from "utils/portForward"; import { terminalWebsocketUrl } from "utils/terminal"; @@ -100,6 +102,13 @@ const TerminalPage: FC = () => { handleWebLinkRef.current = handleWebLink; }, [handleWebLink]); + const { metadata } = useEmbeddedMetadata(); + const appearanceSettingsQuery = useQuery( + appearanceSettings(metadata.userAppearance), + ); + const currentTerminalFont = + appearanceSettingsQuery.data?.terminal_font || DEFAULT_TERMINAL_FONT; + // Create the terminal! const fitAddonRef = useRef(); useEffect(() => { @@ -110,7 +119,7 @@ const TerminalPage: FC = () => { allowProposedApi: true, allowTransparency: true, disableStdin: false, - fontFamily: MONOSPACE_FONT_FAMILY, + fontFamily: terminalFonts[currentTerminalFont], fontSize: 16, theme: { background: theme.palette.background.default, @@ -150,7 +159,12 @@ const TerminalPage: FC = () => { window.removeEventListener("resize", listener); terminal.dispose(); }; - }, [config.isLoading, renderer, theme.palette.background.default]); + }, [ + config.isLoading, + renderer, + theme.palette.background.default, + currentTerminalFont, + ]); // Updates the reconnection token into the URL if necessary. useEffect(() => { diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.stories.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.stories.tsx index 4f2c5965dc957..436e2e7e38c2d 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.stories.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.stories.tsx @@ -18,6 +18,6 @@ type Story = StoryObj; export const Example: Story = { args: { - initialValues: { theme_preference: "" }, + initialValues: { theme_preference: "", terminal_font: "" }, }, }; diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx index 3468685a246cb..9ecee2dfac83a 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx @@ -1,12 +1,23 @@ import type { Interpolation } from "@emotion/react"; +import CircularProgress from "@mui/material/CircularProgress"; +import FormControl from "@mui/material/FormControl"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; import { visuallyHidden } from "@mui/utils"; -import type { UpdateUserAppearanceSettingsRequest } from "api/typesGenerated"; +import { + type TerminalFontName, + TerminalFontNames, + type UpdateUserAppearanceSettingsRequest, +} from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { PreviewBadge } from "components/Badges/Badges"; import { Stack } from "components/Stack/Stack"; import { ThemeOverride } from "contexts/ThemeProvider"; import type { FC } from "react"; import themes, { DEFAULT_THEME, type Theme } from "theme"; +import { DEFAULT_TERMINAL_FONT, terminalFontLabels } from "theme/constants"; +import { Section } from "../Section"; export interface AppearanceFormProps { isUpdating?: boolean; @@ -22,43 +33,107 @@ export const AppearanceForm: FC = ({ initialValues, }) => { const currentTheme = initialValues.theme_preference || DEFAULT_THEME; + const currentTerminalFont = + initialValues.terminal_font || DEFAULT_TERMINAL_FONT; const onChangeTheme = async (theme: string) => { if (isUpdating) { return; } + await onSubmit({ + theme_preference: theme, + terminal_font: currentTerminalFont, + }); + }; - await onSubmit({ theme_preference: theme }); + const onChangeTerminalFont = async (terminalFont: TerminalFontName) => { + if (isUpdating) { + return; + } + await onSubmit({ + theme_preference: currentTheme, + terminal_font: terminalFont, + }); }; return (
{Boolean(error) && } - - onChangeTheme("auto")} - /> - onChangeTheme("dark")} - /> - onChangeTheme("light")} - /> - +
+ Theme + {isUpdating && } + + } + layout="fluid" + > + + onChangeTheme("auto")} + /> + onChangeTheme("dark")} + /> + onChangeTheme("light")} + /> + +
+
+
+ Terminal Font + {isUpdating && } + + } + layout="fluid" + > + + + onChangeTerminalFont(toTerminalFontName(value)) + } + > + {TerminalFontNames.filter((name) => name !== "").map((name) => ( + } + label={ +
+ {terminalFontLabels[name]} +
+ } + /> + ))} +
+
+
); }; +export function toTerminalFontName(value: string): TerminalFontName { + return TerminalFontNames.includes(value as TerminalFontName) + ? (value as TerminalFontName) + : ""; +} + interface AutoThemePreviewButtonProps extends Omit { themes: [Theme, Theme]; onSelect?: () => void; diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx index c48c265460a4e..59dc62980b9f0 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx @@ -12,13 +12,14 @@ describe("appearance page", () => { jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, theme_preference: "dark", + terminal_font: "fira-code", }); const dark = await screen.findByText("Dark"); await userEvent.click(dark); // Check if the API was called correctly - expect(API.updateAppearanceSettings).toBeCalledTimes(0); + expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(0); }); it("changes theme to light", async () => { @@ -26,6 +27,7 @@ describe("appearance page", () => { jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ ...MockUser, + terminal_font: "ibm-plex-mono", theme_preference: "light", }); @@ -33,9 +35,30 @@ describe("appearance page", () => { await userEvent.click(light); // Check if the API was called correctly - expect(API.updateAppearanceSettings).toBeCalledTimes(1); + expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(1); expect(API.updateAppearanceSettings).toHaveBeenCalledWith({ + terminal_font: "ibm-plex-mono", theme_preference: "light", }); }); + + it("changes font to fira code", async () => { + renderWithAuth(); + + jest.spyOn(API, "updateAppearanceSettings").mockResolvedValueOnce({ + ...MockUser, + terminal_font: "fira-code", + theme_preference: "dark", + }); + + const ibmPlex = await screen.findByText("Fira Code"); + await userEvent.click(ibmPlex); + + // Check if the API was called correctly + expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(1); + expect(API.updateAppearanceSettings).toHaveBeenCalledWith({ + terminal_font: "fira-code", + theme_preference: "dark", + }); + }); }); diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx index 1379e42d0e909..679ad6aeef3bd 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx @@ -1,13 +1,10 @@ -import CircularProgress from "@mui/material/CircularProgress"; import { updateAppearanceSettings } from "api/queries/users"; import { appearanceSettings } from "api/queries/users"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; -import { Stack } from "components/Stack/Stack"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import type { FC } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { Section } from "../Section"; import { AppearanceForm } from "./AppearanceForm"; export const AppearancePage: FC = () => { @@ -31,26 +28,15 @@ export const AppearancePage: FC = () => { return ( <> -
- Theme - {updateAppearanceSettingsMutation.isLoading && ( - - )} - - } - layout="fluid" - > - -
+ ); }; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index f69b8f98db6a0..804291df30729 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -536,6 +536,7 @@ export const SuspendedMockUser: TypesGen.User = { export const MockUserAppearanceSettings: TypesGen.UserAppearanceSettings = { theme_preference: "dark", + terminal_font: "", }; export const MockOrganizationMember: TypesGen.OrganizationMemberWithUserData = { diff --git a/site/src/theme/constants.ts b/site/src/theme/constants.ts index b95998640efde..162e67310749c 100644 --- a/site/src/theme/constants.ts +++ b/site/src/theme/constants.ts @@ -1,7 +1,23 @@ +import type { TerminalFontName } from "api/typesGenerated"; + export const borderRadius = 8; export const MONOSPACE_FONT_FAMILY = "'IBM Plex Mono', 'Lucida Console', 'Lucida Sans Typewriter', 'Liberation Mono', 'Monaco', 'Courier New', Courier, monospace"; export const BODY_FONT_FAMILY = `"Inter Variable", system-ui, sans-serif`; + +export const terminalFonts: Record = { + "fira-code": MONOSPACE_FONT_FAMILY.replace("IBM Plex Mono", "Fira Code"), + "ibm-plex-mono": MONOSPACE_FONT_FAMILY, + + "": MONOSPACE_FONT_FAMILY, +}; +export const terminalFontLabels: Record = { + "fira-code": "Fira Code", + "ibm-plex-mono": "IBM Plex Mono", + "": "", // needed for enum completeness, otherwise fails the build +}; +export const DEFAULT_TERMINAL_FONT = "ibm-plex-mono"; + export const navHeight = 62; export const containerWidth = 1380; export const containerWidthMedium = 1080; diff --git a/site/src/theme/globalFonts.ts b/site/src/theme/globalFonts.ts index 24371dd57568e..db8089f9db266 100644 --- a/site/src/theme/globalFonts.ts +++ b/site/src/theme/globalFonts.ts @@ -3,3 +3,6 @@ import "@fontsource/ibm-plex-mono/400.css"; import "@fontsource/ibm-plex-mono/600.css"; // Main body copy font import "@fontsource-variable/inter"; +// Alternative font for Terminal +import "@fontsource/fira-code/400.css"; +import "@fontsource/fira-code/600.css"; From fc471eb384643ad304f9c16dcd429faa07900a6f Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Mon, 7 Apr 2025 10:06:58 -0400 Subject: [PATCH 014/384] fix: handle vscodessh style workspace names in coder ssh (#17154) Fixes an issue where old ssh configs that use the `owner--workspace--agent` format will fail to properly use the `coder ssh` command since we migrated off the `coder vscodessh` command. --- cli/ssh.go | 34 ++++++++++++++++++++++++++++++---- cli/ssh_test.go | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/cli/ssh.go b/cli/ssh.go index 6baaa2eff01a4..d9c98cd0b48f1 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -13,6 +13,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "slices" "strconv" "strings" @@ -57,6 +58,7 @@ var ( autostopNotifyCountdown = []time.Duration{30 * time.Minute} // gracefulShutdownTimeout is the timeout, per item in the stack of things to close gracefulShutdownTimeout = 2 * time.Second + workspaceNameRe = regexp.MustCompile(`[/.]+|--`) ) func (r *RootCmd) ssh() *serpent.Command { @@ -200,10 +202,9 @@ func (r *RootCmd) ssh() *serpent.Command { parsedEnv = append(parsedEnv, [2]string{k, v}) } - namedWorkspace := strings.TrimPrefix(inv.Args[0], hostPrefix) - // Support "--" as a delimiter between owner and workspace name - namedWorkspace = strings.Replace(namedWorkspace, "--", "/", 1) - + workspaceInput := strings.TrimPrefix(inv.Args[0], hostPrefix) + // convert workspace name format into owner/workspace.agent + namedWorkspace := normalizeWorkspaceInput(workspaceInput) workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, namedWorkspace) if err != nil { return err @@ -1413,3 +1414,28 @@ func collectNetworkStats(ctx context.Context, agentConn *workspacesdk.AgentConn, DownloadBytesSec: int64(downloadSecs), }, nil } + +// Converts workspace name input to owner/workspace.agent format +// Possible valid input formats: +// workspace +// owner/workspace +// owner--workspace +// owner/workspace--agent +// owner/workspace.agent +// owner--workspace--agent +// owner--workspace.agent +func normalizeWorkspaceInput(input string) string { + // Split on "/", "--", and "." + parts := workspaceNameRe.Split(input, -1) + + switch len(parts) { + case 1: + return input // "workspace" + case 2: + return fmt.Sprintf("%s/%s", parts[0], parts[1]) // "owner/workspace" + case 3: + return fmt.Sprintf("%s/%s.%s", parts[0], parts[1], parts[2]) // "owner/workspace.agent" + default: + return input // Fallback + } +} diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 4bd7682067f94..75ad88601e9ae 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -63,8 +63,11 @@ func setupWorkspaceForAgent(t *testing.T, mutations ...func([]*proto.Agent) []*p client, store := coderdtest.NewWithDatabase(t, nil) client.SetLogger(testutil.Logger(t).Named("client")) first := coderdtest.CreateFirstUser(t, client) - userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) + userClient, user := coderdtest.CreateAnotherUserMutators(t, client, first.OrganizationID, nil, func(r *codersdk.CreateUserRequestWithOrgs) { + r.Username = "myuser" + }) r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ + Name: "myworkspace", OrganizationID: first.OrganizationID, OwnerID: user.ID, }).WithAgent(mutations...).Do() @@ -98,6 +101,46 @@ func TestSSH(t *testing.T) { pty.WriteLine("exit") <-cmdDone }) + t.Run("WorkspaceNameInput", func(t *testing.T) { + t.Parallel() + + cases := []string{ + "myworkspace", + "myuser/myworkspace", + "myuser--myworkspace", + "myuser/myworkspace--dev", + "myuser/myworkspace.dev", + "myuser--myworkspace--dev", + "myuser--myworkspace.dev", + } + + for _, tc := range cases { + t.Run(tc, func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + client, workspace, agentToken := setupWorkspaceForAgent(t) + + inv, root := clitest.New(t, "ssh", tc) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + + cmdDone := tGo(t, func() { + err := inv.WithContext(ctx).Run() + assert.NoError(t, err) + }) + pty.ExpectMatch("Waiting") + + _ = agenttest.New(t, client.URL, agentToken) + coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID) + + // Shells on Mac, Windows, and Linux all exit shells with the "exit" command. + pty.WriteLine("exit") + <-cmdDone + }) + } + }) t.Run("StartStoppedWorkspace", func(t *testing.T) { t.Parallel() From f48a24c18e494fe34161ce9f7d514af60eac2fdc Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Mon, 7 Apr 2025 17:54:05 +0200 Subject: [PATCH 015/384] feat: add SBOM generation and attestation to GitHub workflow (#17277) Move SBOM generation and attestation to GitHub workflow This PR moves the SBOM generation and attestation process from the `build_docker.sh` script to the GitHub workflow. The change: 1. Removes SBOM generation and attestation from the `build_docker.sh` script 2. Adds a new "SBOM Generation and Attestation" step in the GitHub workflow 3. Generates and attests SBOMs for both multi-arch images and latest tags when applicable This approach ensures SBOM generation happens once for the final multi-architecture image rather than for each architecture separately. Change-Id: I2e15d7322ddec933bbc9bd7880abba9b0842719f Signed-off-by: Thomas Kosiewski --- .github/workflows/ci.yaml | 27 ++++++++++++++ .github/workflows/release.yaml | 67 ++++++++++++++++++++++++++++++---- scripts/build_docker.sh | 13 +------ 3 files changed, 88 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d1d5bf9c2959c..d25cb84173326 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1180,6 +1180,33 @@ jobs: done fi + - name: SBOM Generation and Attestation + if: github.ref == 'refs/heads/main' + env: + COSIGN_EXPERIMENTAL: 1 + run: | + set -euxo pipefail + + # Define image base and tags + IMAGE_BASE="ghcr.io/coder/coder-preview" + TAGS=("${{ steps.build-docker.outputs.tag }}" "main" "latest") + + # Generate and attest SBOM for each tag + for tag in "${TAGS[@]}"; do + IMAGE="${IMAGE_BASE}:${tag}" + SBOM_FILE="coder_sbom_${tag//[:\/]/_}.spdx.json" + + echo "Generating SBOM for image: ${IMAGE}" + syft "${IMAGE}" -o spdx-json > "${SBOM_FILE}" + + echo "Attesting SBOM to image: ${IMAGE}" + cosign clean "${IMAGE}" + cosign attest --type spdxjson \ + --predicate "${SBOM_FILE}" \ + --yes \ + "${IMAGE}" + done + # GitHub attestation provides SLSA provenance for the Docker images, establishing a verifiable # record that these images were built in GitHub Actions with specific inputs and environment. # This complements our existing cosign attestations which focus on SBOMs. diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 07a57b8ad939b..eb3983dac807f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -496,6 +496,39 @@ jobs: env: CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }} + - name: SBOM Generation and Attestation + if: ${{ !inputs.dry_run }} + env: + COSIGN_EXPERIMENTAL: "1" + run: | + set -euxo pipefail + + # Generate SBOM for multi-arch image with version in filename + echo "Generating SBOM for multi-arch image: ${{ steps.build_docker.outputs.multiarch_image }}" + syft "${{ steps.build_docker.outputs.multiarch_image }}" -o spdx-json > coder_${{ steps.version.outputs.version }}_sbom.spdx.json + + # Attest SBOM to multi-arch image + echo "Attesting SBOM to multi-arch image: ${{ steps.build_docker.outputs.multiarch_image }}" + cosign clean "${{ steps.build_docker.outputs.multiarch_image }}" + cosign attest --type spdxjson \ + --predicate coder_${{ steps.version.outputs.version }}_sbom.spdx.json \ + --yes \ + "${{ steps.build_docker.outputs.multiarch_image }}" + + # If latest tag was created, also attest it + if [[ "${{ steps.build_docker.outputs.created_latest_tag }}" == "true" ]]; then + latest_tag="$(./scripts/image_tag.sh --version latest)" + echo "Generating SBOM for latest image: ${latest_tag}" + syft "${latest_tag}" -o spdx-json > coder_latest_sbom.spdx.json + + echo "Attesting SBOM to latest image: ${latest_tag}" + cosign clean "${latest_tag}" + cosign attest --type spdxjson \ + --predicate coder_latest_sbom.spdx.json \ + --yes \ + "${latest_tag}" + fi + - name: GitHub Attestation for Docker image id: attest_main if: ${{ !inputs.dry_run }} @@ -612,16 +645,27 @@ jobs: fi declare -p publish_args + # Build the list of files to publish + files=( + ./build/*_installer.exe + ./build/*.zip + ./build/*.tar.gz + ./build/*.tgz + ./build/*.apk + ./build/*.deb + ./build/*.rpm + ./coder_${{ steps.version.outputs.version }}_sbom.spdx.json + ) + + # Only include the latest SBOM file if it was created + if [[ "${{ steps.build_docker.outputs.created_latest_tag }}" == "true" ]]; then + files+=(./coder_latest_sbom.spdx.json) + fi + ./scripts/release/publish.sh \ "${publish_args[@]}" \ --release-notes-file "$CODER_RELEASE_NOTES_FILE" \ - ./build/*_installer.exe \ - ./build/*.zip \ - ./build/*.tar.gz \ - ./build/*.tgz \ - ./build/*.apk \ - ./build/*.deb \ - ./build/*.rpm + "${files[@]}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }} @@ -663,6 +707,15 @@ jobs: ./build/*.apk ./build/*.deb ./build/*.rpm + ./coder_${{ steps.version.outputs.version }}_sbom.spdx.json + retention-days: 7 + + - name: Upload latest sbom artifact to actions (if dry-run) + if: inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: latest-sbom-artifact + path: ./coder_latest_sbom.spdx.json retention-days: 7 - name: Send repository-dispatch event diff --git a/scripts/build_docker.sh b/scripts/build_docker.sh index 7f1ba93840403..14d45d0913b6b 100755 --- a/scripts/build_docker.sh +++ b/scripts/build_docker.sh @@ -153,17 +153,6 @@ if [[ "$push" == 1 ]]; then docker push "$image_tag" 1>&2 fi -log "--- Generating SBOM for Docker image ($image_tag)" -syft "$image_tag" -o spdx-json >"${image_tag//[:\/]/_}.spdx.json" - -if [[ "$push" == 1 ]]; then - log "--- Attesting SBOM to Docker image for $arch ($image_tag)" - COSIGN_EXPERIMENTAL=1 cosign clean "$image_tag" - - COSIGN_EXPERIMENTAL=1 cosign attest --type spdxjson \ - --predicate "${image_tag//[:\/]/_}.spdx.json" \ - --yes \ - "$image_tag" -fi +# SBOM generation and attestation moved to the GitHub workflow echo "$image_tag" From aa0a63a29565709149e95f6fcfa56de3771a9741 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 7 Apr 2025 09:32:52 -0700 Subject: [PATCH 016/384] fix(agent): log correct error variable in createTailnet (#17283) --- agent/agent.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/agent.go b/agent/agent.go index 3c6a3c19610e3..cf784a2702bfe 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -1408,7 +1408,7 @@ func (a *agent) createTailnet( if rPTYServeErr != nil && a.gracefulCtx.Err() == nil && !strings.Contains(rPTYServeErr.Error(), "use of closed network connection") { - a.logger.Error(ctx, "error serving reconnecting PTY", slog.Error(err)) + a.logger.Error(ctx, "error serving reconnecting PTY", slog.Error(rPTYServeErr)) } }); err != nil { return nil, err From d312e82a5143772f5b72381be3cd0ef898bb0b0d Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Mon, 7 Apr 2025 21:33:33 +0400 Subject: [PATCH 017/384] feat: support --hostname-suffix flag on coder ssh (#17279) Adds `hostname-suffix` flag to `coder ssh` command for use in SSH Config ProxyCommands. Also enforces that Coder server doesn't start the suffix with a dot. part of: #16828 --- cli/server.go | 9 ++ cli/ssh.go | 43 +++++++++- cli/ssh_test.go | 120 +++++++++++++++------------ cli/testdata/coder_ssh_--help.golden | 5 ++ docs/reference/cli/ssh.md | 9 ++ 5 files changed, 131 insertions(+), 55 deletions(-) diff --git a/cli/server.go b/cli/server.go index 98a7739412afa..ea6f4d665f4de 100644 --- a/cli/server.go +++ b/cli/server.go @@ -620,6 +620,15 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. return xerrors.Errorf("parse ssh config options %q: %w", vals.SSHConfig.SSHConfigOptions.String(), err) } + // The workspace hostname suffix is always interpreted as implicitly beginning with a single dot, so it is + // a config error to explicitly include the dot. This ensures that we always interpret the suffix as a + // separate DNS label, and not just an ordinary string suffix. E.g. a suffix of 'coder' will match + // 'en.coder' but not 'encoder'. + if strings.HasPrefix(vals.WorkspaceHostnameSuffix.String(), ".") { + return xerrors.Errorf("you must omit any leading . in workspace hostname suffix: %s", + vals.WorkspaceHostnameSuffix.String()) + } + options := &coderd.Options{ AccessURL: vals.AccessURL.Value(), AppHostname: appHostname, diff --git a/cli/ssh.go b/cli/ssh.go index d9c98cd0b48f1..e02443e7032c6 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -65,6 +65,7 @@ func (r *RootCmd) ssh() *serpent.Command { var ( stdio bool hostPrefix string + hostnameSuffix string forwardAgent bool forwardGPG bool identityAgent string @@ -202,10 +203,14 @@ func (r *RootCmd) ssh() *serpent.Command { parsedEnv = append(parsedEnv, [2]string{k, v}) } - workspaceInput := strings.TrimPrefix(inv.Args[0], hostPrefix) - // convert workspace name format into owner/workspace.agent - namedWorkspace := normalizeWorkspaceInput(workspaceInput) - workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, namedWorkspace) + deploymentSSHConfig := codersdk.SSHConfigResponse{ + HostnamePrefix: hostPrefix, + HostnameSuffix: hostnameSuffix, + } + + workspace, workspaceAgent, err := findWorkspaceAndAgentByHostname( + ctx, inv, client, + inv.Args[0], deploymentSSHConfig, disableAutostart) if err != nil { return err } @@ -564,6 +569,12 @@ func (r *RootCmd) ssh() *serpent.Command { Description: "Strip this prefix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command.", Value: serpent.StringOf(&hostPrefix), }, + { + Flag: "hostname-suffix", + Env: "CODER_SSH_HOSTNAME_SUFFIX", + Description: "Strip this suffix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command. The suffix must be specified without a leading . character.", + Value: serpent.StringOf(&hostnameSuffix), + }, { Flag: "forward-agent", FlagShorthand: "A", @@ -656,6 +667,30 @@ func (r *RootCmd) ssh() *serpent.Command { return cmd } +// findWorkspaceAndAgentByHostname parses the hostname from the commandline and finds the workspace and agent it +// corresponds to, taking into account any name prefixes or suffixes configured (e.g. myworkspace.coder, or +// vscode-coder--myusername--myworkspace). +func findWorkspaceAndAgentByHostname( + ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, + hostname string, config codersdk.SSHConfigResponse, disableAutostart bool, +) ( + codersdk.Workspace, codersdk.WorkspaceAgent, error, +) { + // for suffixes, we don't explicitly get the . and must add it. This is to ensure that the suffix is always + // interpreted as a dotted label in DNS names, not just any string suffix. That is, a suffix of 'coder' will + // match a hostname like 'en.coder', but not 'encoder'. + qualifiedSuffix := "." + config.HostnameSuffix + + switch { + case config.HostnamePrefix != "" && strings.HasPrefix(hostname, config.HostnamePrefix): + hostname = strings.TrimPrefix(hostname, config.HostnamePrefix) + case config.HostnameSuffix != "" && strings.HasSuffix(hostname, qualifiedSuffix): + hostname = strings.TrimSuffix(hostname, qualifiedSuffix) + } + hostname = normalizeWorkspaceInput(hostname) + return getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, hostname) +} + // watchAndClose ensures closer is called if the context is canceled or // the workspace reaches the stopped state. // diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 75ad88601e9ae..332fbbe219c46 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -1690,67 +1690,85 @@ func TestSSH(t *testing.T) { } }) - t.Run("SSHHostPrefix", func(t *testing.T) { + t.Run("SSHHost", func(t *testing.T) { t.Parallel() - client, workspace, agentToken := setupWorkspaceForAgent(t) - _, _ = tGoContext(t, func(ctx context.Context) { - // Run this async so the SSH command has to wait for - // the build and agent to connect! - _ = agenttest.New(t, client.URL, agentToken) - <-ctx.Done() - }) - clientOutput, clientInput := io.Pipe() - serverOutput, serverInput := io.Pipe() - defer func() { - for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} { - _ = c.Close() - } - }() + testCases := []struct { + name, hostnameFormat string + flags []string + }{ + {"Prefix", "coder.dummy.com--%s--%s", []string{"--ssh-host-prefix", "coder.dummy.com--"}}, + {"Suffix", "%s--%s.coder", []string{"--hostname-suffix", "coder"}}, + {"Both", "%s--%s.coder", []string{"--hostname-suffix", "coder", "--ssh-host-prefix", "coder.dummy.com--"}}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + client, workspace, agentToken := setupWorkspaceForAgent(t) + _, _ = tGoContext(t, func(ctx context.Context) { + // Run this async so the SSH command has to wait for + // the build and agent to connect! + _ = agenttest.New(t, client.URL, agentToken) + <-ctx.Done() + }) - user, err := client.User(ctx, codersdk.Me) - require.NoError(t, err) + clientOutput, clientInput := io.Pipe() + serverOutput, serverInput := io.Pipe() + defer func() { + for _, c := range []io.Closer{clientOutput, clientInput, serverOutput, serverInput} { + _ = c.Close() + } + }() - inv, root := clitest.New(t, "ssh", "--stdio", "--ssh-host-prefix", "coder.dummy.com--", fmt.Sprintf("coder.dummy.com--%s--%s", user.Username, workspace.Name)) - clitest.SetupConfig(t, client, root) - inv.Stdin = clientOutput - inv.Stdout = serverInput - inv.Stderr = io.Discard + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() - cmdDone := tGo(t, func() { - err := inv.WithContext(ctx).Run() - assert.NoError(t, err) - }) + user, err := client.User(ctx, codersdk.Me) + require.NoError(t, err) - conn, channels, requests, err := ssh.NewClientConn(&stdioConn{ - Reader: serverOutput, - Writer: clientInput, - }, "", &ssh.ClientConfig{ - // #nosec - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - }) - require.NoError(t, err) - defer conn.Close() + args := []string{"ssh", "--stdio"} + args = append(args, tc.flags...) + args = append(args, fmt.Sprintf(tc.hostnameFormat, user.Username, workspace.Name)) + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + inv.Stdin = clientOutput + inv.Stdout = serverInput + inv.Stderr = io.Discard - sshClient := ssh.NewClient(conn, channels, requests) - session, err := sshClient.NewSession() - require.NoError(t, err) - defer session.Close() + cmdDone := tGo(t, func() { + err := inv.WithContext(ctx).Run() + assert.NoError(t, err) + }) - command := "sh -c exit" - if runtime.GOOS == "windows" { - command = "cmd.exe /c exit" - } - err = session.Run(command) - require.NoError(t, err) - err = sshClient.Close() - require.NoError(t, err) - _ = clientOutput.Close() + conn, channels, requests, err := ssh.NewClientConn(&stdioConn{ + Reader: serverOutput, + Writer: clientInput, + }, "", &ssh.ClientConfig{ + // #nosec + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + }) + require.NoError(t, err) + defer conn.Close() - <-cmdDone + sshClient := ssh.NewClient(conn, channels, requests) + session, err := sshClient.NewSession() + require.NoError(t, err) + defer session.Close() + + command := "sh -c exit" + if runtime.GOOS == "windows" { + command = "cmd.exe /c exit" + } + err = session.Run(command) + require.NoError(t, err) + err = sshClient.Close() + require.NoError(t, err) + _ = clientOutput.Close() + + <-cmdDone + }) + } }) } diff --git a/cli/testdata/coder_ssh_--help.golden b/cli/testdata/coder_ssh_--help.golden index 3d2f584727cd9..1f7122dd655a2 100644 --- a/cli/testdata/coder_ssh_--help.golden +++ b/cli/testdata/coder_ssh_--help.golden @@ -23,6 +23,11 @@ OPTIONS: locally and will not be started for you. If a GPG agent is already running in the workspace, it will be attempted to be killed. + --hostname-suffix string, $CODER_SSH_HOSTNAME_SUFFIX + Strip this suffix from the provided hostname to determine the + workspace name. This is useful when used as part of an OpenSSH proxy + command. The suffix must be specified without a leading . character. + --identity-agent string, $CODER_SSH_IDENTITY_AGENT Specifies which identity agent to use (overrides $SSH_AUTH_SOCK), forward agent must also be enabled. diff --git a/docs/reference/cli/ssh.md b/docs/reference/cli/ssh.md index 72d63a1f003af..c5bae755c8419 100644 --- a/docs/reference/cli/ssh.md +++ b/docs/reference/cli/ssh.md @@ -29,6 +29,15 @@ Specifies whether to emit SSH output over stdin/stdout. Strip this prefix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command. +### --hostname-suffix + +| | | +|-------------|-----------------------------------------| +| Type | string | +| Environment | $CODER_SSH_HOSTNAME_SUFFIX | + +Strip this suffix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command. The suffix must be specified without a leading . character. + ### -A, --forward-agent | | | From 2f6682a46f121874fa103ca31e937cf32bdaf94f Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Mon, 7 Apr 2025 14:00:43 -0400 Subject: [PATCH 018/384] docs: add zed code_app to extending-templates doc (#17281) continuation of #17236 (thanks @sharkymark ) adds zed as a coder_app to [preview](https://coder.com/docs/@17236-zed-app/admin/templates/extending-templates#coder-app-examples) --------- Co-authored-by: sharkymark Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- .../templates/extending-templates/index.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/admin/templates/extending-templates/index.md b/docs/admin/templates/extending-templates/index.md index c27c1da709253..2e274e11effe7 100644 --- a/docs/admin/templates/extending-templates/index.md +++ b/docs/admin/templates/extending-templates/index.md @@ -87,6 +87,55 @@ and can be hidden directly in the resource. You can arrange the display orientation of Coder apps in your template using [resource ordering](./resource-ordering.md). +### Coder app examples + +
+ +You can use these examples to add new Coder apps: + +## code-server + +```hcl +resource "coder_app" "code-server" { + agent_id = coder_agent.main.id + slug = "code-server" + display_name = "code-server" + url = "http://localhost:13337/?folder=/home/${local.username}" + icon = "/icon/code.svg" + subdomain = false + share = "owner" +} +``` + +## Filebrowser + +```hcl +resource "coder_app" "filebrowser" { + agent_id = coder_agent.main.id + display_name = "file browser" + slug = "filebrowser" + url = "http://localhost:13339" + icon = "/icon/database.svg" + subdomain = true + share = "owner" +} +``` + +## Zed + +```hcl +resource "coder_app" "zed" { + agent_id = coder_agent.main.id + slug = "slug" + display_name = "Zed" + external = true + url = "zed://ssh/coder.${data.coder_workspace.me.name}" + icon = "/icon/zed.svg" +} +``` + +
+ Check out our [module registry](https://registry.coder.com/modules) for additional Coder apps from the team and our OSS community. From d0aff04aef294596757b2b7d1080f0bc46a08304 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Mon, 7 Apr 2025 14:19:45 -0400 Subject: [PATCH 019/384] docs: remove blank inbox doc (#17285) [preview](https://coder.com/docs/@hotfix-inbox-doc) Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/images/icons/inbox-in.svg | 6 ------ docs/manifest.json | 6 ------ docs/user-guides/inbox/index.md | 1 - 3 files changed, 13 deletions(-) delete mode 100644 docs/images/icons/inbox-in.svg delete mode 100644 docs/user-guides/inbox/index.md diff --git a/docs/images/icons/inbox-in.svg b/docs/images/icons/inbox-in.svg deleted file mode 100644 index aee03ba870f95..0000000000000 --- a/docs/images/icons/inbox-in.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/docs/manifest.json b/docs/manifest.json index e6507bc42f44b..df535a1687807 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -194,12 +194,6 @@ "path": "./user-guides/workspace-management.md", "icon_path": "./images/icons/generic.svg" }, - { - "title": "Workspace Notifications", - "description": "Manage workspace notifications", - "path": "./user-guides/inbox/index.md", - "icon_path": "./images/icons/inbox-in.svg" - }, { "title": "Workspace Scheduling", "description": "Cost control with workspace schedules", diff --git a/docs/user-guides/inbox/index.md b/docs/user-guides/inbox/index.md deleted file mode 100644 index 393273020c2a0..0000000000000 --- a/docs/user-guides/inbox/index.md +++ /dev/null @@ -1 +0,0 @@ -# Workspace notifications From 114ba4593b2a82dfd41cdcb7fd6eb70d866e7b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Mon, 7 Apr 2025 12:48:58 -0700 Subject: [PATCH 020/384] chore: fix swagger type of AuditLog AdditionalFields (#17286) --- coderd/apidoc/docs.go | 5 +---- coderd/apidoc/swagger.json | 5 +---- codersdk/audit.go | 2 +- docs/reference/api/audit.md | 4 +--- docs/reference/api/schemas.md | 10 +++------- 5 files changed, 7 insertions(+), 19 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index acd93fc7180cf..d4dfb80cd13b5 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10734,10 +10734,7 @@ const docTemplate = `{ "$ref": "#/definitions/codersdk.AuditAction" }, "additional_fields": { - "type": "array", - "items": { - "type": "integer" - } + "type": "object" }, "description": { "type": "string" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 622c3865e0a6e..7e28bf764d9e7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9543,10 +9543,7 @@ "$ref": "#/definitions/codersdk.AuditAction" }, "additional_fields": { - "type": "array", - "items": { - "type": "integer" - } + "type": "object" }, "description": { "type": "string" diff --git a/codersdk/audit.go b/codersdk/audit.go index 1df5bd2d10e2c..12a35904a8af4 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -171,7 +171,7 @@ type AuditLog struct { Action AuditAction `json:"action"` Diff AuditDiff `json:"diff"` StatusCode int32 `json:"status_code"` - AdditionalFields json.RawMessage `json:"additional_fields"` + AdditionalFields json.RawMessage `json:"additional_fields" swaggertype:"object"` Description string `json:"description"` ResourceLink string `json:"resource_link"` IsDeleted bool `json:"is_deleted"` diff --git a/docs/reference/api/audit.md b/docs/reference/api/audit.md index 3fc6e746f17c8..c717a75d51e54 100644 --- a/docs/reference/api/audit.md +++ b/docs/reference/api/audit.md @@ -30,9 +30,7 @@ curl -X GET http://coder-server:8080/api/v2/audit?limit=0 \ "audit_logs": [ { "action": "create", - "additional_fields": [ - 0 - ], + "additional_fields": {}, "description": "string", "diff": { "property1": { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index fa9604cff6c9b..35f9f61f7c640 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -629,9 +629,7 @@ ```json { "action": "create", - "additional_fields": [ - 0 - ], + "additional_fields": {}, "description": "string", "diff": { "property1": { @@ -695,7 +693,7 @@ | Name | Type | Required | Restrictions | Description | |---------------------|--------------------------------------------------------------|----------|--------------|----------------------------------------------| | `action` | [codersdk.AuditAction](#codersdkauditaction) | false | | | -| `additional_fields` | array of integer | false | | | +| `additional_fields` | object | false | | | | `description` | string | false | | | | `diff` | [codersdk.AuditDiff](#codersdkauditdiff) | false | | | | `id` | string | false | | | @@ -721,9 +719,7 @@ "audit_logs": [ { "action": "create", - "additional_fields": [ - 0 - ], + "additional_fields": {}, "description": "string", "diff": { "property1": { From 9eeb506ae5520d1a665c177f76101c2493cc0d3a Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Tue, 8 Apr 2025 11:48:18 +0400 Subject: [PATCH 021/384] feat: modify config-ssh to set the host suffix (#17280) Wires up `config-ssh` command to use a hostname suffix if configured. part of: #16828 e.g. `coder config-ssh --hostname-suffix spiketest` gives: ``` # ------------START-CODER----------- # This section is managed by coder. DO NOT EDIT. # # You should not hand-edit this section unless you are removing it, all # changes will be lost when running "coder config-ssh". # # Last config-ssh options: # :hostname-suffix=spiketest # Host coder.* *.spiketest ConnectTimeout=0 StrictHostKeyChecking=no UserKnownHostsFile=/dev/null LogLevel ERROR ProxyCommand /home/coder/repos/coder/build/coder_config_ssh --global-config /home/coder/.config/coderv2 ssh --stdio --ssh-host-prefix coder. --hostname-suffix spiketest %h # ------------END-CODER------------ ``` --- cli/configssh.go | 28 +++++++++++++++++++++++++--- cli/configssh_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/cli/configssh.go b/cli/configssh.go index 67fbd19ef3f69..6a0f41c2a2fbc 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -356,9 +356,15 @@ func (r *RootCmd) configSSH() *serpent.Command { if sshConfigOpts.disableAutostart { flags += " --disable-autostart=true" } + if coderdConfig.HostnamePrefix != "" { + flags += " --ssh-host-prefix " + coderdConfig.HostnamePrefix + } + if coderdConfig.HostnameSuffix != "" { + flags += " --hostname-suffix " + coderdConfig.HostnameSuffix + } defaultOptions = append(defaultOptions, fmt.Sprintf( - "ProxyCommand %s %s ssh --stdio%s --ssh-host-prefix %s %%h", - escapedCoderBinary, rootFlags, flags, coderdConfig.HostnamePrefix, + "ProxyCommand %s %s ssh --stdio%s %%h", + escapedCoderBinary, rootFlags, flags, )) } @@ -391,7 +397,7 @@ func (r *RootCmd) configSSH() *serpent.Command { } hostBlock := []string{ - "Host " + coderdConfig.HostnamePrefix + "*", + sshConfigHostLinePatterns(coderdConfig), } // Prefix with '\t' for _, v := range configOptions.sshOptions { @@ -837,3 +843,19 @@ func diffBytes(name string, b1, b2 []byte, color bool) ([]byte, error) { } return b, nil } + +func sshConfigHostLinePatterns(config codersdk.SSHConfigResponse) string { + builder := strings.Builder{} + // by inspection, WriteString always returns nil error + _, _ = builder.WriteString("Host") + if config.HostnamePrefix != "" { + _, _ = builder.WriteString(" ") + _, _ = builder.WriteString(config.HostnamePrefix) + _, _ = builder.WriteString("*") + } + if config.HostnameSuffix != "" { + _, _ = builder.WriteString(" *.") + _, _ = builder.WriteString(config.HostnameSuffix) + } + return builder.String() +} diff --git a/cli/configssh_test.go b/cli/configssh_test.go index 84399ddc67949..638e38a3fee1b 100644 --- a/cli/configssh_test.go +++ b/cli/configssh_test.go @@ -611,6 +611,33 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { regexMatch: "RemoteForward 2222 192.168.11.1:2222.*\n.*RemoteForward 2223 192.168.11.1:2223", }, }, + { + name: "Hostname Suffix", + args: []string{ + "--yes", + "--hostname-suffix", "testy", + }, + wantErr: false, + hasAgent: true, + wantConfig: wantConfig{ + ssh: []string{"Host coder.* *.testy"}, + regexMatch: `ProxyCommand .* ssh .* --hostname-suffix testy %h`, + }, + }, + { + name: "Hostname Prefix and Suffix", + args: []string{ + "--yes", + "--ssh-host-prefix", "presto.", + "--hostname-suffix", "testy", + }, + wantErr: false, + hasAgent: true, + wantConfig: wantConfig{ + ssh: []string{"Host presto.* *.testy"}, + regexMatch: `ProxyCommand .* ssh .* --ssh-host-prefix presto\. --hostname-suffix testy %h`, + }, + }, } for _, tt := range tests { tt := tt From 12e5718b99bbf427801244d270552799cece62e4 Mon Sep 17 00:00:00 2001 From: coryb Date: Tue, 8 Apr 2025 00:58:28 -0700 Subject: [PATCH 022/384] feat(provisioner): propagate trace info (#17166) If tracing is enabled, propagate the trace information to the terraform provisioner via environment variables. This sets the `TRACEPARENT` environment variable using the default W3C trace propagators. Users can choose to continue the trace by adding new spans in the provisioner by reading from the environment like: ctx := env.ContextWithRemoteSpanContext(context.Background(), os.Environ()) --------- Co-authored-by: Spike Curtis --- provisioner/terraform/otelenv.go | 88 +++++++++++++++++++ .../terraform/otelenv_internal_test.go | 85 ++++++++++++++++++ provisioner/terraform/provision.go | 2 + 3 files changed, 175 insertions(+) create mode 100644 provisioner/terraform/otelenv.go create mode 100644 provisioner/terraform/otelenv_internal_test.go diff --git a/provisioner/terraform/otelenv.go b/provisioner/terraform/otelenv.go new file mode 100644 index 0000000000000..681df25490854 --- /dev/null +++ b/provisioner/terraform/otelenv.go @@ -0,0 +1,88 @@ +package terraform + +import ( + "context" + "fmt" + "slices" + "strings" + "unicode" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" +) + +// TODO: replace this with the upstream OTEL env propagation when it is +// released. + +// envCarrier is a propagation.TextMapCarrier that is used to extract or +// inject tracing environment variables. This is used with a +// propagation.TextMapPropagator +type envCarrier struct { + Env []string +} + +var _ propagation.TextMapCarrier = (*envCarrier)(nil) + +func toKey(key string) string { + key = strings.ToUpper(key) + key = strings.ReplaceAll(key, "-", "_") + return strings.Map(func(r rune) rune { + if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' { + return r + } + return -1 + }, key) +} + +func (c *envCarrier) Set(key, value string) { + if c == nil { + return + } + key = toKey(key) + for i, e := range c.Env { + if strings.HasPrefix(e, key+"=") { + // don't directly update the slice so we don't modify the slice + // passed in + c.Env = slices.Clone(c.Env) + c.Env[i] = fmt.Sprintf("%s=%s", key, value) + return + } + } + c.Env = append(c.Env, fmt.Sprintf("%s=%s", key, value)) +} + +func (c *envCarrier) Get(key string) string { + if c == nil { + return "" + } + key = toKey(key) + for _, e := range c.Env { + if strings.HasPrefix(e, key+"=") { + return strings.TrimPrefix(e, key+"=") + } + } + return "" +} + +func (c *envCarrier) Keys() []string { + if c == nil { + return nil + } + keys := make([]string, len(c.Env)) + for i, e := range c.Env { + k, _, _ := strings.Cut(e, "=") + keys[i] = k + } + return keys +} + +// otelEnvInject will add add any necessary environment variables for the span +// found in the Context. If environment variables are already present +// in `environ` then they will be updated. If no variables are found the +// new ones will be appended. The new environment will be returned, `environ` +// will never be modified. +func otelEnvInject(ctx context.Context, environ []string) []string { + c := &envCarrier{Env: environ} + otel.GetTextMapPropagator().Inject(ctx, c) + return c.Env +} diff --git a/provisioner/terraform/otelenv_internal_test.go b/provisioner/terraform/otelenv_internal_test.go new file mode 100644 index 0000000000000..57be6e4cd0cc6 --- /dev/null +++ b/provisioner/terraform/otelenv_internal_test.go @@ -0,0 +1,85 @@ +package terraform + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +type testIDGenerator struct{} + +var _ sdktrace.IDGenerator = (*testIDGenerator)(nil) + +func (testIDGenerator) NewIDs(_ context.Context) (trace.TraceID, trace.SpanID) { + traceID, _ := trace.TraceIDFromHex("60d19e9e9abf2197c1d6d8f93e28ee2a") + spanID, _ := trace.SpanIDFromHex("a028bd951229a46f") + return traceID, spanID +} + +func (testIDGenerator) NewSpanID(_ context.Context, _ trace.TraceID) trace.SpanID { + spanID, _ := trace.SpanIDFromHex("a028bd951229a46f") + return spanID +} + +func TestOtelEnvInject(t *testing.T) { + t.Parallel() + testTraceProvider := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithIDGenerator(testIDGenerator{}), + ) + + tracer := testTraceProvider.Tracer("example") + ctx, span := tracer.Start(context.Background(), "testing") + defer span.End() + + input := []string{"PATH=/usr/bin:/bin"} + + otel.SetTextMapPropagator(propagation.TraceContext{}) + got := otelEnvInject(ctx, input) + require.Equal(t, []string{ + "PATH=/usr/bin:/bin", + "TRACEPARENT=00-60d19e9e9abf2197c1d6d8f93e28ee2a-a028bd951229a46f-01", + }, got) + + // verify we update rather than append + input = []string{ + "PATH=/usr/bin:/bin", + "TRACEPARENT=origTraceParent", + "TERM=xterm", + } + + otel.SetTextMapPropagator(propagation.TraceContext{}) + got = otelEnvInject(ctx, input) + require.Equal(t, []string{ + "PATH=/usr/bin:/bin", + "TRACEPARENT=00-60d19e9e9abf2197c1d6d8f93e28ee2a-a028bd951229a46f-01", + "TERM=xterm", + }, got) +} + +func TestEnvCarrierSet(t *testing.T) { + t.Parallel() + c := &envCarrier{ + Env: []string{"PATH=/usr/bin:/bin", "TERM=xterm"}, + } + c.Set("PATH", "/usr/local/bin") + c.Set("NEWVAR", "newval") + require.Equal(t, []string{ + "PATH=/usr/local/bin", + "TERM=xterm", + "NEWVAR=newval", + }, c.Env) +} + +func TestEnvCarrierKeys(t *testing.T) { + t.Parallel() + c := &envCarrier{ + Env: []string{"PATH=/usr/bin:/bin", "TERM=xterm"}, + } + require.Equal(t, []string{"PATH", "TERM"}, c.Keys()) +} diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 78068fc43c819..171deb35c4bbc 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -156,6 +156,7 @@ func (s *server) Plan( if err != nil { return provisionersdk.PlanErrorf("setup env: %s", err) } + env = otelEnvInject(ctx, env) vars, err := planVars(request) if err != nil { @@ -208,6 +209,7 @@ func (s *server) Apply( if err != nil { return provisionersdk.ApplyErrorf("provision env: %s", err) } + env = otelEnvInject(ctx, env) resp, err := e.apply( ctx, killCtx, env, sess, ) From 0e878a8e9884945e91d52cdec822bc79b23aab01 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Tue, 8 Apr 2025 04:00:23 -0400 Subject: [PATCH 023/384] docs: rename codervpn to coder connect (#16833) part of: https://github.com/coder/internal/issues/459 - [x] Text - [x] Screenshots - [x] MacOS - [x] Windows [preview](https://coder.com/docs/@459-coder-connect/user-guides/desktop) --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Co-authored-by: M Atif Ali --- .../desktop/coder-desktop-mac-pre-sign-in.png | Bin 0 -> 109243 bytes .../desktop/coder-desktop-pre-sign-in.png | Bin 73367 -> 0 bytes ...coder-desktop-win-enable-coder-connect.png | Bin 0 -> 237890 bytes .../desktop/coder-desktop-win-pre-sign-in.png | Bin 0 -> 75566 bytes .../desktop/coder-desktop-workspaces.png | Bin 99036 -> 100150 bytes docs/user-guides/desktop/index.md | 30 ++++++++++++------ 6 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 docs/images/user-guides/desktop/coder-desktop-mac-pre-sign-in.png delete mode 100644 docs/images/user-guides/desktop/coder-desktop-pre-sign-in.png create mode 100644 docs/images/user-guides/desktop/coder-desktop-win-enable-coder-connect.png create mode 100644 docs/images/user-guides/desktop/coder-desktop-win-pre-sign-in.png diff --git a/docs/images/user-guides/desktop/coder-desktop-mac-pre-sign-in.png b/docs/images/user-guides/desktop/coder-desktop-mac-pre-sign-in.png new file mode 100644 index 0000000000000000000000000000000000000000..6edafe5bdbd9893f35d19ce4b1dade2553af7760 GIT binary patch literal 109243 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Ν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

!6gbbEjZV*jj} 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

P(0I*h!_uP*&4XFEHQ$~Cp;*)}Xg zyNFTP!|>#T!CwRn8Rm~TaI+LC!eUmJI!=G;FuNEyobkj^N~%(N&o;Td-LcN_U5KvDMD z*P$2Y5iI_)f>u}wSg@6Pd?X_#Ya@4D?~65zG2YoiUZz~=AufA-_lsc_Yo4QOlY%B0 z4AgZVrO~lABWsmdU`AJA?sK+%n}`{m*6sL~8uH{Q=lKO+hH7Alg8{>rVHGG)T(`R|bwqip&?of+OQ{$owMJQt-wvQSnf zDORZ!q}u_-YVKnLvB8L!PtE{Pw>IycK1In^A?s@hBE;G0h4M^S-Nug{ZuMf)}oj-`Fv`hh=mZ(`rml-M|m&z%f53l-Z-S z%`tg9vOVZjr_s4*If&#jU~gCzY1yBU25^WUA6QOIa7?y&`D=Zev&wqHWm#$4hr|K)HW=GAlq4uf$ zYE&YV1huH~hd|@cP$SB>7tq`Bqro4%v%CEJ{)kc2=&dXy>qNLi^gqc7#e#GbSiX9~ z9A{1w{%5ay{j0rOxt_WFpCdACW&&ScvgUO61AS=CRL_ojc7dVr>7AuWx_VULb6DQ( zDR*IN)_>=}DW9shCLEbjv|~V~rJts{TYCT5rh0>B0hbzpCrxcHsULJbYjQw1f3e(P zG~3-C?4rn>JEZ&T*$+A*oc{1iWieHtddc2)JLDpG%07@a?|lhVmS>8tIF?W~w9b=! zg?Nzi=CjH{HaQtaNpG4|6HiG^2Q`;vPHR0wIX@oAI3ehZ54fGsZ}MO~xCy&#jt3mQ zjRjl%C`BiX501B8)%(4DGbq>2K%8j~h=XU87;ZfuJl-~A#L;%nAE*-Nff6(rN)#Az z9%%FMHefkW!GJq(yH3|k08E2&sVmlBxWpdki@bMlYk5o>JscA^R&B)r#WNQY0>lwg zX6hgxrUY@udPqC~cNg0yyp#$14W`-lSdNL*ry-2mH(ta2eztIRl9pC%lRB-w+a-wg@k%m=4_xR z2Nz^g$5Zd^1$ig`5o<){N@IVj@bQhg@k>8p9N-elhNNDx`xS_*p$4cPd zOVoT|Lj{2QuLZnaNVGL0`;#@xeXyriklvIJ@WsqqXiYX}IjStAT(E7?Q0gwJTTguk9NEfWj5tX>y2w1JKfCJa z(_t>7ssneb9^*6Q*jLe_hb%h%VFW%wRJ252O}F&s43f6T3>Ugy=m^0V)-#D5wKRHT za;OhUx8%c>&ni6SG6bsiE8GupxmYzZ6ZJok1g&Y)F#AA(h}2n7J9i&!JMqMyoN^FJ zv=HrO8xe{h@wvFZGR>vL%@Xklk7)Gl_d6uov<;I^)T=EjAzhY)U(~z_Fq%2tIl9(t zn`C=;as08}^si&d))0s-2WR`BPtL*e$7tV+@a=({jtd+SPx#W@Ar;bM)&=h^hSOlX z*}G$rfGF}WX|>5zWs@#c6z_Hdgy_8KO&_hArK^lp{8LkXxI{+n#*T7(-kj^HnNDIj zBwiwy@XHxOf}ct!GQ2SfBPHPNC=qvnbegGox(1698|$rEJ4NrnL7$IKAEW+<%)|Pk%cB$G7a5+ zWPw-H82~(&bQIsr$T%cXnN%;);Z*LuErn!bGfK~SLi^s!mh#R8K6|j^boAMER&`g_ zK8YmdeXP51~1%J zTJ2Z3$wVA?HLu29##MK$%5S;K)r9#KDm4*jZw~5aIuVOS;ACw_^DTuFm*Qqu{}63O zxRN$3Un=+rf#D#pwq?3B3B7jXQ(oCI>2JF(Bc9FI*2)l*iOK|732ny6NN~Q3t2w4) zCT7*~_G#yWTxhhMEGluGhOxD+8RMS8ZA zys7nm$-9A2tL>TO!jz+*dU2EUOj)v{3J`hDK~1vK#)`?GEmi!Aui4Xzw0eFzr4*;b zE7PfgmjdyCz;?c-T4C!ehn?OvSufN167PBJVenljtKFytKK5Ff6#Gty_<;P(w3LB^H$=~XpdI0ZtB{di?4u0v7`6w}#;Lb{dHLb3q2P+N3=3F%d5kp+foP=^AUuyVbU zfz|A|=b+xwGKTg(W%?~r_Vi4*zHHrFu{P&D|7@xE_D%vvHTVvwma~5$J(hy#t82q!)(J`0=1-n3MF zmS?JbmOTN=ZDV3UUqaviH3ay>uuOl|m#X|yOX7n#X&s-OHZ&*R-{}fcZ7S8XTE4l8 z$j}D2>N4N$i(=OTmQ*g(rhNYjh$z4Axn!6m$8!z=RhZu%g(vul1)T&@eR{Cty|wde z8?c+JdocNCQ+!**U^du1jplf7_?q_Pj#|%L7##T6uDi0YPr)Qe*>^|= za-#F!HkkWNi^@HqNpv79A6$O(FQlyfW{>(8wac~&{Pe`1yWVKq&}va%Uh~TcAY=&f zxK)Q!)SaGi>qLk#XIq-iw7-3-{)sIF(&DbR+`z{CN|WrcCK`koQ`d>BCw8>*0e3kz zl~Xb#^j>M+J!Wui4h`rlu?K zi1q%n;2Y~#`r0}4t4xvqiOBoLrLVoOKDkU2Fb0bdv^uSFm$;b<%;>?NqR;=6s*--v ziw*?XW9K1d<-ZzA$=?gmi^5zy-G~`|!Iems-NnMs)gH3~Y?W2_a89or@l}a#S!W~X za^|uiOK3@E28$80Z`keMtV)&j#ZqYTfeO*pw>bmdTzb~*Tv`l;4(Xo#)LvA?xev5q zB=A*Re7HQ~j^kJ;&p~T!JwfjUI2C6|+~f-`z$H$eG7-kXKQ$>bgCorZA>d}{O+NhL z-s(xfBU!oOcdC-j^}{beBfa2B+;w$zjvn$;Rd;nyDoz!(_*)*r->MA#9B0~v5lbuB zuCg@9u9IauPh0Ex!N!A3n;k3l{#4%PgHK`%htm}^AK#fMcF4NjHb*Fo8|NaQ4RrQ? zbCGoRyi){r=Ol;z(zU1W)>}2MF~CY7%+cNShp)V%#P^rSN;L`BF%^*wN1G))LF+(;+G(7I-1Kzg+kYt3&k$< zuj}(x5>iE4!XDoA?Ot@|!7KT-QGXtX%P6|fErXB+$D()`SUiN?i{a%IEclA~m<^S%xSK~|&7E8m~F_oqfTbxI+A*_BCx(v)+;NgVC{=RyFIP4qtw z$i!XByT?ozsk7;Q;PpW+GXN~@3SV(3?9*kENo#^4h52yTYrpz|B%j%TvtsiTH80-@ zXvn|1`u?ZL_EZR|SFNQ4K3upqN?OnQivBCO!o?n%#ne_}9CWiN%aw{?OC##+EQoa? zm!eTe`~jFa$<#Z8%U(Uax^DUGwJp>A$q;g-m>8xCYMOr)M%>a4G#r}T3H*=Pql!yB zlLKGFaYH9~8dRn`7k_^2g>>1)(8Bv=J*TxIUwAz{u-UbY&S@BJdl^SE4Nrf0+d@PW zcaSPkr+JCUy3Q8;R&mr~yNadAZt=V1%@!Hbz>}Y-DEYtLDJ z_#>&E?y-M9`TnltL?iU?N7$VkOJ)L77Kdu^3842uuxvKZrlBp(ZK}=UE%^SkGXFABl0ZDD@N>Yf^z$u8T@zp|RFX&h$ile!1lWG^md(_<`$hOW>v z0wj7R|E~dLZ?fE6bfUdM*nK%UCxdMdn;|Uz~|p&<)?i3-tW#bUG)a= zp_V%>9=nc9U1TZ}dfVS4BEfs4bYrXEBl}XMB%_z#Tr3!e6y=XY=-sMZ%y2Y|iiJC= z3mGaBD8PX*n)tZhx$7WZ-Ct()Xwtc6(L>;|OV{bpW?-1-qKowr!z%RG84Ze`XY@ws zO1ee6r`y4y+lX&nfSI^;!cpPF09}NOr?m0^E*SMH(VsYp(0#=}+tAJ8j@r^%3> zjBik31Urg}2a%)=Qg(D3s%Tx+Mb)d`rpNSM@1)BE6*$tA`w$PYhNFh*p|d}RFZF3I z1iTFoRTP?w6>nVv8&^gikR1q9-t|;O$tFOj;VtkHhI28ytq%ZOZZjPCu9&L!M+FeG z)17do{?E-16lP>dfhesm6XsZx0_4(yQo?XRIrxKx?yWdHH(hn+tun(~8^Jd|erx;N zx!6MHC`2_~VkntI|Ce%Bh;rmn=g+Yn(8HDmZdul!?-03!I+pjb=T?DW&mfVvwZ1?w z0sfKyFf57eE&B|OIfZ0KvqVMdO;ZfsR`o|ReON2aC%wiz0^uUD&RLm&4n0yMN5q9h zEmv$eoCeZ;=~QsIlDZV8EbRANDgwGuT-vj>)d{#^6d851`XDWvRqhEoWjuQ4tGKezwv3C*pbo=_gF5b*l5}-vl$h z^U;pC)Jrh6${CfCq-x?JTX78+sWPENj_AQ~Tkc`Ek0BZ&jwCOo}p#31W%bRcwoJHWL#T$nv*Ts;-1N{oBmMC@K|U$ucJ(wOLwP#5Q6K3 z5RynG0Gybn#3zxLz|jbkU}3R;qKcCbZI>@pl6~tg1vfM634?l5$^Gf6R~n;;72`sW z<3X{I-{G_4EXSUmoV@aU2cxnBmM-M9Q72d8O*W>=5OS*Y>peHt=!*<0a6MR^`)fgq zFzkT;j?}xPj!oKrA5Sswt|jxc{ruUAsigG?#&#U3r{vc4bDGVM^>wm+p`G;lIu&k+&)pq%RetLBoBIWVcttqBVZ-$&C5kUgnN#!FQ!rwapV3OUFKEPqv6mOorIwhPvlxL=8kr)79x zGfp>uwHk^lAy_fet64jQSX_*3JMsNhl0Qlm$1lnhdQUdf&>+_sd@WC7QEM9#`zS==4{L7yzj+Q&HIQfe`y4~ z5#|THt-PcC8ke3rC*MjR^+#)IV1REI=0ZeAkZsXI(m|Dw?g_G{2ewknpeo#D$`zNw z1SPu3ob~Z)m|gns@3G@xv(O^8)K4KS>y#rlxu7!YOTbo+Rc?nXDQ_Z z7Oe^d_6cZFucKw}y8%4APTwVv+Nw6<1}gw}qGoVM(D_ypG?jHysJgc#s2RHv;p5%! zM9?+lnhl571ft8KO4dQbGLMJ1qL9$i;cr4*?QmSF^#i2a+GuE zP~zw%d3Nnd?XWC8#YTP@cCl2mVv|8oT)}2iDh7ETu1Ge3f$6; zKooOID}LfQHEZO)P=inErVryns%nFeN0}0rX6@2;=)=a_LYNJoFWZ~d;?K7nMWu2J ze$Q~n{moQ38B7bl*l#C(W$C)@vGj=^p*`fyTi!crw!GB}i1VSE14v?W^|Mb7z5Q^> zI2{AGjuL?y7B`}as?nW`Jo}kS=t)rry3t$Ty-zD#R>Wd#Qlnp)3p93V|z6k)^*m6!GCFpAiM}c)3oi>Bvre22;FDLVOLlS4rhV(&2AH>06o(n`XI- ze=lq9=*+apr;t7^gqBn<(FK8^*HQ>%e9c}f0Oe^AUTlonL`l-t$tM#)7+zB*91nq& zSkzS3iH*bfNYH;tYDq?!6F-MZmCQcMr{;LybTXp3b{X8sm^_>rOW02%L5s~z43BRp zT-=hNO$EQNSeo~O2haDe{G2!Ox0B&FcwDKIp|Do7%EC|0f2aMeNXEsUW_rpyIBC>h z2pviQs0t8NgHS$7PLFCM#p$s<#S?1G7AC6OIff#6#QU$!?EFF4fo69!(&1({aGPdK zd%Cl(#9HpN_tvLzatYJa;iTtQJ<`^FVFZ<|BLw^~rOatPeud&>g(q324!kp5;e8ML zxmo#r=E!-%><->)Z}^eb$p!j3z1Li>tn^^|9n;9KLYG3ihgWsVEE$8nfS^BYm%c0;98`WFaO|gme+gKXxx#e69y28 z3V7__)L&8DIDG?+_i*TL8!ai|$n@R#a`N7i`b3hG$17d<+8-V`?Gdouoj7O<(U~3| zWczP3H_8m8_)d+f8LiGfSyOOq{zQwxi=t|IsQq-skr?FVRgKS}f**fEGB4Le66Tf_ zAQCq9Sq#CwHM4_WZUQI)+~%rttMM2S2|RYQNbob@u`4Xw8J3GiV$aS9C&lj?D)x*( zI%c^Xb0J(KayC}v`!jDA0@kUKjIg*B7NatmfF9YcpB)-n#Z-fkanbqidDqz*V$**1 zb?E3DBf88blAH9|V(pq2Bu)z-mTtpeBBwXtD_yC=mk7(PgMag}PySVL9ZazjSPK!0 zM?Pk3+-2FM4yW~Q?y(djhtCBp=73_Bo6?$0(tEWUHmZ10#_mkZaX-PzKcg?B0sH@^ zIIUw|CuoT@SosdyBF41}UHYvPo~?P+W6 z)8gcecM+t8y*{15+OUJ{2mM3u8e9$YVgW;5K+}2R*OgI{)$tElpsv)eWuQEXD@i=BsAJXU4UajgN18G$v|AJMJ#`-)^%BFB5a2yyph5rOfx zDE*H_#bmgb0jtN;X@slX8@UH&U5UMg1g)PLrv47%p0*VJ&V8kdQBWOg*Y)d6CyDz&w)>TdlZlay>%y?p7flCKPza`fd?zRpPuiFY2LR-20 zWtiblQ3wtAL8zH8L~;t`MM`5=74l_EuOr68TX^Dp z6ifQ8?uDrzYNW40S!S_>J#jky-DJ}$Q$twUS{giQ8>-oB*5{3Z3?$-7fp@RiF!=XR z4bu?vj-cm%5R`a^kiIZ&8Ik(}q6y8R_iVQ8elh)(@xXoaBsq zMEvz{4S=lkI|zYeWU`fb2C5Kom+`ozU4ru4 zu6P|j9`~eDgcFQ9!Yj#v42)ankbe@mfetxX_mc)(Sp5M&KibfkJT-1UVz_9^@zS*g zxu*HXk05=Tys&bm6UXT|YcS~%7o#k%lG;!9?@+Ix^}cGIGW~ib8X1n}KWN%}AX@@M ziR5!z5Fiww`Qih#x<@LZgB$SV9hMLudyq zdan1iF}K;H*;M0{;RJ}&b`Rt_ov%w(;$NwoE-`v$!v9?!l2X1VI%k`96y=>)wKu7~?3CAf6mppyY;twd z#{jCm=8y~t(9YdtArbKD{`l26|I>wG|08=(D9kKE(EQu_))z%T*T5zo5=J`%?`4Q~ zV0PUgK4|GbG%XVK=>oi-gPlIi+PiO)p1c=oA5io+Pl{P_eb|w|1?L3a`M#mU7u)--Jj{hceef}ux~6F}MHKRy zM!wFhTzz*cC5j2Tdh8&x?VBg#U(|`yu~we2m`r3b{07H<(L9HDJi*ST-GNMVfC@yXHMReo|`sE=Ife8UU_@ z=hV_Ii|kdcrZxX*Pn;c>3{I~L6?6heY2~W~Ze+g&*b)H*vv?rCWofW zPEL^3eWQMCdFNqJ4ak`@&C(OZDLra)q;|koxuFa^5*`wf5Lfsf{Mh$3Br4;}{=tkh zU5m60Z-TC9_pEKk*z+FxyPf{DC!aoxr(VbETHT@I7b%kXRdVq?V?_3oh57Q;Tnt8# zAPKCmKa`}qCU*%_hav?myw(a~vpTuYuS&;_^bEPkWtnj$@6H<4@XXf7ZM1!Q+g(lN zeU|o{V0J)VVJkG2i4H!iKCmWAhxH3?&e<}Xhk!!HTf4LXEBA;FKPELcg7Q_%sgS<5 zqC(0{WgNFR_($1wt7A{m-`iac=H=D2dshCGSvIDFh78sDYc_ZOw_p!&lNHCpr4SM3 zjl%dO1&UgTiMWbSL8(mTuiC@e|c&EYdj@}lU{!ZGAlCC z7`m1#MFTp>_i<38?jh7{xZ*6=s3V?{gnAa@IpH;L*l@Kaqq|X`yEM4u@{gsn22It+ z+rA+^-hEmKxb_!T|3)WTRMnyaw8NlR2F)2OOx%gI0!^mBZ4VG=@97LgU-WC{zKlEngs zYxb1Hu5)Cj6wfhSkO|w$dbk5`6v`M1BM4!mx@aI(1CsMvIkp`@QyV(MP+;#boOCyr#N1TMyTAU z6uEWE80N$$0PO1u7?(-#hY%|e9es?{| zxYbi*V`kxhi|c^!q4SS zr&kCkrzm@127jJM+vvbdcAyJQA)L&j&IL2h=jBc{{K|E#&SrehzwhasVBo03Df>dc z&6&rcJ>#hbHE}Bn$WD-Yg@YY?&qTUnJQFJNXmyQ`B(lSx8m+iF2jN0mY0uZ}bpv}L z&J^Vq!BXpE#UqbE?ahbg0*D+NQk91aGFqD)fSX{1O9y+yDj~CGpfJLlfS_cFAoiTi z!lG-*JjlkXv2vOn2CJy=TVcL+H21EFl!l^do}ed2qa`k>&}-m5{bsNJBJzVgp74&~ zR4J92*z=^$m4yny{$-VM?MXHVH-=#hkJ@Xh8dXv(rN7m?Zxp2K!wmB}uUdc_;JyP? z%EWhOrs5(rYjs}oh;MBjm&XU3e3QyEc3w1UE1_1xj-hwHUyy3E&khA^?>m!oaP6fb ztqdHrn;@)mOHe-*lS9W1Cc)Ptzav|tR7%4QxI)K!^Xh*mWs|P_>|VA$MH>%(rYCM} zsEoH|UFmE_@g_V(YcuV|D1wO}513n)>QOF1s77MV#DwrpU$@3+Pvu4v(MM}4#XBPX z{Hdnp9O7NXsnyXkH1OLOVDBrm^RVljS)YhG;>IF4i5xE`*KIk^emmydgpkCR1n)pqOOKcv0FiPmD#wqILz( z*->d~{3rtH+&TT^*s@>e)Zi{_FX}+-%r|$5@JKdkKZ1K~R3uLDl#<0#Ncz1oznicI z%XZpn-3;j3&N{7LWhrx6nNTuH6Dmtx^Z!L9L8l`cbFs^FZ2o zDKxK}(#>sk4nPe$7Uqt2H5$$m{2P59HxooQFTEE;wF+gZ`59qP_@?tjeC5iG8niR@ z`KYxQTeiZL4{CVc25+X-S?4*(ysFu`(qH6~vF)vR^%v$Uw)|@D4V9iD-hqhS(N#V{ zIDwm9asRY~VxuTA{9b+9s(*_ZviuCapl=mJ8i!aQ{r00XGB`wh%obkX!qI4riuNbm zdSb;P@FeEpAKiN~R_9a9^V0CS3l?qaBo2=BMpKXnl2vH%yF2ym@|+b1E$VA@2gD4) zYo4Z?s}p^%Ud|!pZ<`4OTX*C-QZ!@E_s!^-?QTXO z^^*>5jOS)PI)Q6&eI2Bn2M99rSrM?&j>kv1@#IQ}34yPEKtqD(>ezx@$8v*STFO*V z)>M+yNN_*lc5esHqrn~=Pq~=bvleCE%l~e2vZHakxsd2$>CzH}Up5HH*;s#&V!k=#>YQjEE*prs*K}X(&)ONX}!q zqi40ihr3f#r0`Vs*%p1$e+ne0u=r7CGPM)pt-a%kr(A#)0fFft{*~9bv(<&?g=*v{ zPWudEJ|2KjR7ivv12Begy7qD`23d~x70U6 zAJMscU)EEmjorDsgq^H-#JT?g%=!t9c~YKFqs_UG;IK*?uN$H--l5IjwfmdnJgphE%Gi04QJVgX+7%26) z!tbaznkB2*B^e>pWYW9VIwMH_W#H@N?mb`tIxs4aQr{VBC0i?LClmGu_nJRmmJuOF zimZL=Hona78zm+6nUl5B82hZ4b7}zO_=@6Rfu<4*slb`QA z5x-kd3qmT8?i2+3EtO>Fb?z+Mv(ax+I;t}Yu*#PRN3^o1bt@xp2xiRZ1K(JP-@K;( zcht4*)qpw93#btehEvxZfG$C7Y`mOM0wnZEgcnEgvpboZFci>3Ok&$P@2|c%FbS|a zkepK>iQS>jY_1d1iPp)`a7>(aXokvjM7I6fJ!WAzewL6?=pYkOsjdA&8ugdRg}Zed zl%#m=gX2BfH{Wy?d5TtMBi4FX$mqS&6KFonX`RRIZ-vBz;|#hJ*Psg43pA5=)=lj* zN*s*ByX~HDP;0!tJR&@Uvo5!sQB_|zQ&CAvHe(v=$ZHm?423_S5`1q>0u5J-nXz?@ zG0pR0hC9T@EiF{VvW&NBOs5PVr>l9BAZV6;>R4dLA_cETnR>2XK0gVE_y>V zy_w3$?YyYe{93{2TBO_U&w(qeVz6I@nC6T!IXKwVdW0reWH(aG0BdzLYT#$m;q0cx z-mh2pUwD!4^V`?&Ap!;T5tWQ0L#_0%meuDEqi%2u_OG`x%bC zk}&mn<+F?X9HmdA`WTu9Y=Y`ze}5W-dwba~HCOIfrbYLvG@MDG&c}*C@9VvQ-oX7E zOS*%o7X_YGyM6AwSHc1t`_s_CT-e(tQLFTVV_L$RqS{(J_(6ni2j@+Yo|x0gywNBZ zB!_1u>caOQKWGX&H>J9I00!dDcn7VI=$lv5!o{#dhu7x#(;K})Xr-Grw@uXYnN{gx z5ql%h&^th4u{2~$$Pvr`iSZ{XFR9+pOX30jmzpG(|BA7~hh@pb)3%2Nqg}C82~D*! z43*>Rr#&&iTA^xNCEwLh4Nz;jZA5LcxDivAZurXAO4Fe$f*s*L%q?#`5%xx*7N?U* zmH2=8WxU09n~S_Xv^yPC0A49^?2Af4l74V%!kcM7ey0!>>jMv}{(&(TOm!>q1^hOB zZ8yH%K1|?k(~RDyd&9G`^~22=!_zmh42JK2TxaG&{yl$vk$&gj7B21vnz1?X8)BcQ z&h!CwWl+4VY)dqRB2{VinYoBbD zy6^m4TpQ8&#rQ}G6DK`%@!JM|bdp&t?85nxV|_C4#x$&CcA5GzJdC zdS5et??eTdrM|ADpM(i;d^P;mTBnxlG()mtBSsgP2KEwqsfY+HR5sTEbT(L)<1`wu zBF8}MX*bi5RB%)Jorkq8Yu@D4adFt`G7A~|Gh@K>VFbUs#;{yUy-RE6$wo)+=dD@( z@$gE5AVESn>i#TD1FXDVqyuQ^@>v-_OQ%NvGZoIU!; z!J_vRZ)sC<$VzU5a0x%`9;m}WEN-$P>RagM&HuabNNehYQ~;<*DZZ zwFu(3E?{Hxbf*Rx@H{YT?#a;E4<1nKA(G9=TLRF&Q~BfJ9~nVw0ZRnJ{|%rGXgSk$g(7yyph0HE+T94*c4-bOJ$M*WS`}9_3XCiis@Y$|J6X1@2CBq z8^Z-%4WfIvsPv-RFmepa0Fi#mj?x?!CTTg+IhOiyNsfS@#|ED=yIt#Xt~2g%q|FJG zK6$ksf4M!0FnM`jnD%OU@tJP&-3yb{sN0%T+9dZlsl&d--0~dx3`c6?wi8)hEoU&prB9On<~e->QAeo;A2F5 ztD(CFj`wJliX834%wDO(DXjyQdfQBK%CvuGv&uuS^-8w%>p%ggt?7cNK{L7Oz`Sp% z-{;5H_Y9OyS;#d)-ehLF>Ok8w0%BO1Z4rU_MBrgg^m<`_Qh!SqDV`y^rKWumos9Jy z;MILR33axYZAabCMf`iSOPrlh+Z{5H1D6lKAq3EBtG=EU^-wUd7$=W}Nl@#wdjOjBX$-uo>5M-hGyRW#cLVL8xv5=lD+7Y()yp%XoE` zyrs?qjWsfBmAu1lZ)QM7$o?L-W%Fv07Q zWg9^{SPfLapZ)D-dKDo8D%}TTBs{zz%s6r}PXZKZtG(GKDW*$M`tM)x+VPKzz9cD+ z&1WF^iym==Ix6y3w6dR!KljV(FC*0QHOsNWr6v7LyVev5#w*8}tY!cte`#G!Uhnd~ zj=HIgFD|L~eOl?oOyXCJKU}C}vr8u3Dm`j9DA}i=yMryA`wH2}{KZ_NTd=SEvyAtQ zV*RgO61@f=Yh~Mb^m)N(56N(k=XP^7pSF{gy-o4?-6{1>ADgME!&1%ZCLkVRyoaEl z+n`lbu|g5?BK#akuir<0@R1dR1*KQ*wm& zhsFk(1vWC{;(uv!HMvai#f|dXwaD4a%R_~z{hu}rFB1@lr6KdABU93%f2of2*M&de zy;z2j5kb`&wFxsWan@vIv&f5`Vzx&>teK_ngQN(v3jZbEIkzWV3?Yh#u1K8bvC>Bp z!oEmb>Vrlmj6dHR+wi;lJgeXpr6#DEMzeGMWsi1#0A`w=`IE5hcvt`*C}SH7nr+^>XQ5ZLjjG z5DTr!35HluZVBoPuR$Ak`)hLBrpv69jja^Z$4{!bfWRtCxh}(s*p~^ZVAS+5p?q4o z*-f8i$3Xj)=EMsdO66P|cQp4z`tX~=cady(A(u!mgMHL;_>zIerJ>*#E(M&o74Cf}w=Jp$@$WfxA3l3enuGmGR;rx zL-SV~-SuxfamyD{H0MSC8ZKt40zr%**SfbQg|-U}sf*Y@g>2r+EjfuN3MWe{OWQ^V zE?OlyCAk!ABjs66MROCSh^t4D;~if_gM`bWkza1`qp{eEV@H=Gvzt~p$xV0Ers)HR z*NZ?j7Pb_50aTNQsHwsfU_k22IPQlWIEmz?zuJlboF?J z{sQ-1Se#tNzyh?h;5jk=qeKMTNj%+Z8vD`!HJJlZxkb;FL9N9!5b08VFs)80E-brq zB3oFNgz2xz8G=fAd4-w(Klp|xfV*u;sfR9@Kbk)JcdT>mZ7VZ}+u)=5sE@dIG1@-k zbx3DpwwaT|K*5j5T<^2*2-qv18K!44TsmQ`! zrTyBsTJCZhIa`&}dsIpAmwklnlVw!Ojl^_NgtkieQv^SzD^Ta9oDSG6R}DH;s-MV1Uc&zVc4R{%mqB%75|q$J${wwxMv%IHc5E z)|+XYPs^<}20)Ok8P&`GiUMw=et9u{SKPy8G}l;}W4Mc0rKzc^;|eFVI4$&^i=HVx zh)+*F)v7}3q(4yU&Z{i@3b&Y2?h=F!)fp0AxDx&MhUp zyvPv)Btll*BkTy$pQvAn*O!;F3RFLqFYGlJF?S1Nos{za@{<7oM+EGagkXSbA-VEB z2fjLmS4HDNq%j(8ZcPJJU$ujEoM=N4Ld`&6p2U|YXmQqBIm5kD9q`Sz0Gs-YdBB2j?BuC|7eRL4|(Sts0PZYCR(wL4UVDdUM3$Ua-{xV(^*R zH5niS48vNiF-Ll4!M!YT3xw6x%H1y%xi@sKE{C|Qh59=e?k7Mp(0CahHt~46w^@Y? zX&fvxZXzye3*Z8g(XhHz-VB)dNt^|JP#)tizi!7h%B#IRIWAc$c0xn*akP&e1@4Q z6dg(w-`Q={&3Y4PS5V4#HPePjYBOU6AS;y6Io1&y6*=kCZubR20jpH5v%cLrSHeQl zuGr_<(FOyXVsiM0dZM-zCP*7wR$I55tv?-t)VCk^1NspylBJB-HaVi7v=2N07{`M{ z+}AwRjd$Lz4-Y6#uX5Ogw&sc{{d|D~g_{%2s|X*@h#Vcu?=#ePgjK5$mo?WVTxApR zW9PefWUTkD$tn13C#xNulF>h`u3Dn90*JYtKzMTPpem=1X!g zIXbGGT-ZO`E6Y}|O4(-?jI5u%2}Q`|0zT)p=I0^P^^ui*fx>thw#qF71GTb2gMnX{ z-Dm@>ZF)s8c5KUzSm6t|?nPKpQW*tk-;@3x@#hT=e#~Hr08BBXafrw0j;cue?^gLZ zJ3oSi-NulV6ilL&`0{9?YE|0(Vjaz0(DO^w`9w>fSdSM8dtGM}Nz=tN|AEA;z0IQ> zG;=M#UKtSN@&?VS!nm{)Mp-Sj=QWbOBv{qduayR+hgr0?G>Oa}P2FSIv9<}X$Yn^6 zkZBa^$qV=cuhm)5QgVNP`!2(O7HyR0IoOyxGdkE)+125_dK#jTkPrQ9ZcXE^+T=iZ z2l+Jkes3>VS~N8womIp~Y*DKbDWcsA<9v_Wkw4>`(ADDTy{I2kiFj_@#FLt+AVA0X zKwMGGSnjn;{GR{yso<=M=k7C|rU&I-iGdxeQkTi}6t1{JRO{AcgX~#MjRr+7quzMe z(r!w<%u;Ndbh`OydKX#VTDsJF)g2~_O|PxPQADs92+`I53&iHi>j=CpPfBrzBR}N% zeFrGMr)O`zrqh*GinFn+r`@Q&rE0s>8ZkX_XhG$3FqZzRLMrC#1!F>2T=gbdV`Xwa z0kIcXefrbpA6B_j-yze#*5{s@LSy-sDo46$@1kO`o2A-}m$>~B9yi=jDjSh?^brh* z1LuoxCjV326vQ!(+|NPv_^|qtW6xo=5}IPLRjKBb6H){zIU1~ zsCCJgYW~{&^f@*#LRK2ZE6C4^@HFT~YwcwDPUp2<;hN)=b;nI*;^cL7Cif%6&pJBN z&Ozf{>H?Q_>AlA*)}1xta6MX~VjMs2xp%Ahd1oCNNdw~xKP)(h3Ut<`AVJ$>>UMer zy51-$+d1Umat1K?h$##XDd*l{wKX$muS`I%?%8GtH;{*dk4+hc&;N=BP+M(-1Ipok zp>tG>U3F~GV$vNvVp*@(+|^_jWYOjUxDlNX=7(J|v6vsE?ET-8(4mJhL*IEC9m;0| zTik%iGjhJMILOgN@2l#ikW1ztYL+8E80hJlHomU@vur{0wwJ3&Ogt|OsuyJ^o>&^z z51MG(CElhfg(3u>`jd}8Az)_ye@_@Y{j@|e>xuOThOuMUByMsr-~?Mdh3{>_V3tU+ zHn%8EenJ3TyreC$f9?+NqKRrv&3Wk9xWbv~w=#BE;8?_YURr&4p5?zQBDZ$-oN#+; zig@~-fK{SMIxXZSN4VcOZtHXGdwTE&7j1^~@scnDpQ@QPK<__j6LG3=&Z*i472iLt zDyW>%SOs*&3A9uso;;E;_=*0cbA|#AN9#C%HJZ*50;`op)q@g2G${~>%7Pc830V14aRSHD8)Jl!8F^nrE1 zLF8!Uf;1ln{3)SkBZBN^52T$8Kn=@8HxZkJ*~G}21S7DAJMV2Lp3e#OG&?!H5MR%t z1h2TBOZdH_=Fb%B+RY`B%*vSw8^?YtwHDd3>0es7Oj^`>40=-AU_Gv*JwR8nMTdiW zGux{5($x}wF7;e`bcIL|SYXY1Z{DGI$s_(0F$cM`&wS?4OOdKl=)A>oE0Zj>z(^TgSpZ`N~8l+9>`GUET{;{EYe#D}QM~v;AwiL*l9D zM){pHF_^v@j=8M64i3wu+JXF~7^&HFed*99QnT4dZA8(#u(y+$x2~}aBL(?`HQ<&z z2+G+Mg`!9t(uWKgjBWDeWf+XOtW)QC1+!l0j#``#L&#ZpGd^>qc?c?7I474-~WbdIdIN!EYkEisy2-@YLtShTFF z$z@+blfx0(mL@bJh`npiou=vbQllE*Rpk*U?Pu&2Z%iP0bY5rszUD4U?pWh7PxCR7 z1b;mG@#Gq_P}r|jcKgifo#R(Am)H2!ev^#VLPFhHd(EWI#{sJ2vD3ugdrs|g672YU zx^tIzvX9sROPNE_Sr*VZYGU_EL9QCdVj^bVi8#H-9DCL&2`{l(qRfHIU8JwTm`)$o zdste#O|g4qL&-JC#jB#qHD}|vGqQR^gfMbmq=P^D(;7F;ridYjHw@07@^p8Osw64C z{ZgS3Ysvi;g+dk#I?4EjZ}8|PABKemkGiZ;mp!Ss-KbuEAm=@>6%x2j`ka{!Ozv`e z=VYg=pUg#ZjJK$2&nM0rOihQ*i@D-HG?-nNH+(1M@I_!Gah=V?%V;y#09zWIRiJZa z!1Sj2{q&--f;HB{m_I{5_V~8|3Ig@4js$-}tN0b>J+aOoF57JMgYQ^|2R4OE13@OQ z=}*9dTT;tjE?VRo9_w@I`v^#6bW`E;2jg10)LMRy7hce_ z-s3dOg<#v`92cfNO$qMoOeBpeXr?){PV!o4>jclni$#sp5NdwJBj_~N| zj{hTY?UtqqF=8ka(;7!+KDpcSay6W^9#ZuUbhA8Uo7>q}%iQl$3)GWpez+j1OTp8! zBASfPog7zI6IIVkSa+G8#F<62g^qdBChzy+pAZ z@47w!Rz1&`c>kN@!IS4=shy32(&4{ERg`%aXW!g9>^UUnSx&}_buq{Ky_4vC!n|s5qWCYXzi~j!vnazAtPG2zxd~gk5siAj)MFYhWrq5(BhRwfwlGuyFU5 z;tGqYxty%AdQ5|mUAP82*`;G1OO2!P{Fl;MF&G8QL9DpY4Y)sKmLO zC0>}kl{J{u&vo&mMgdW^(tf`t@mfdTjI`)Vb*kmKDT(^dPq+>hQ7n`Z%pFlyy=~3w?Rhq+D=Q=DD`e|aM-$H_($#oK zSz>(67W=1P0#v=_StrS6#VK{eNa2@J+wt9@i+XJY;;Mj1ibi_ZC83kq?mQwgMV>+h>-3faER zUSA~`8`;qAd$$UPGe;rz9+yEGQl0lx%QwbIb5zJ6_6c-1A>^+pL^yTu3Zvkf*&>~@ z;9tte!_*PwA_qTVgFZwYrRo?!>QRk*ZFjn)-dNu9n-8m*{LV2~V(p z&3DUX-$LqdZCU^Z^{FYnwo5r?HV=;e{e4@_2s%J{$Pvb*{1(HWuXI zkW<^HR0Bj>`8+Ttdde-~CNkd4PX!+Y&XA=~-big;WkTI%xnNzznG0$`X_S)YLL{!0 zj`7o?H4jR|Ojo0@fwm_Rdd&(NRAW6`y!DX#ns@NJcDDr#?1NAulZk&RgE@IF1Q)^H zK)znOP)3A^frGSzJ$P;Z?v5^bIMij^qvZoF0#nAe6JwBv&?dDOgaZ>7vJs?gOBzZn zuS{Tj?kumUbaA>lG@vp2Hhj+os1)eGG7D| zk&cKaY#m8r_}~$@$I&{kYw#6Flu)G4Bk;v(`Vhs-7SiPD;Rw*c$JGwh*+Qh-@!v*! zbm_$Qam=N`hY!>W5Km)!ebPg8sD1|q=Wg!)tplr@AObm$l_BjA3Rrt38W$BPSL?g| zq5fvg>SJ9*?pSvc+bYs*_!AA1?xen?JltL?7euIoF!iVRU>!mLSUaZ7ArjM1y0Vr1 z7~}^BoFXSL7xFL(vALG5*?0ET)4Kz&rj0xe<@?MivZ1)*9YFOjV6=??H*d&|ZT{6? zfJ&11aJnil4LHMFz#kMC&iMQ*y8i>>|Es|y*MDEPA3@LYwL9HAoO5ctr#Z`7X+*1| zfJUvrTy*bcD%eiyb++TuEk-DpwiNF{w`2&|^Un23*7xsNW&5$rWt(bsKlm8$p;Mby z+bi}LDW!LACVAW8H$P{Ox9$?-=Ka<+#7sK+9;eiREZ7I2vfKH=eEc`vSykGkw+exR zuJJcwUZqXW5~+GN3hsqds;bf1?LREzbyAu|(?t!-TTwdKofo;#OOoY%p6(&W(flJG z_rr%KA$`%cjco-`?s zU4x|;o5(0%J75jMuq48qxCoC^P!nA@It^-W>5#Kk;J8u2jI#Ju;bplQtyDBrn-&U0 z>(t`Er!j=!e)xqi(7uT^3O^n4!ykUH!WJ^!v~}83uB+kil#{ z@lfMoHU5-!1F3=B{yb10A=JSzKPgP^t{Sd(%>HyTbBKVGYN(I0x;{UVk+sN+-g4S+)TMY@boEE@uM@`J__-uvSuitYVLvI*p_4V0Tc61xl5_`GBA2&?O`7gw@4Rmem znJ1P*>lr0DI9EQo;1!5%Bk&6!7HofAz%P6erZfz964xyl>GCt~p^OMlsB`H*#!0Br z?0POX1?G8+dVP_GCLj5jzF#`Od7052x0zK-vU>=_9sTn$-53knDqdMPepOVuFwQplPIi&kfYD+!Wk`ix5q!Dop6HCiIAAO z#DzQp88gcb-@ZLuk;6ePz`|b_Q^FWFo-grSI7uUD^no$$?>QhsfzT%G_l5wK}_@u?pOC8B0=AKkVbuCuXu+sixGNxKj1oDhs~`y_VquqPa)usO*4RXYS~Y1Kac>)uc(Cn?AwRE$|;X)J^7()I-f zXQvzXl6erAc&#NReB7IaxGf|+3ScPf@3mO{)4*3Q>gvDl&`g!?zd)eQSCMzfMF1y^ zeDW~H&T9Wp8X@EjMNlwAjA)mK`eTYXwJT-}lZF}S8|SDNYee8?D}`U;c`Mk6rD4uJ zt-F);aDVH$l+i}-^DAR9ofchz%|MdRu5v|J;W_TS;v8owecoe`ULR09;L}TV5COc5 zuWR`!?o~s-#)3Xqe9Q>^tg=^IJ3C=1=0)iJH*PN}WSPR=1#&=(<-8?3Q=Cbg{VQui za`EkIS9&(H)>;(Z-*u;2_kd9VFZ9dOqT4LcAxN_!x(W0cKT1UcYfDz5{mIF55Ego^ zjzH#=U8zKudk?G!>P>`EKH|DYT?Ydy5^Jb;Bg-2G@Jj*yYk5nLI7%5pTj_wuIlYx$ z7c(GlCQcMb&~ zePI^)19zOVpN&mM3?VPeXa7hHoSxCqW!sq{m%_@!u;AQ>8zoLG0&HjkQ|uS4nz*B1 zYn2srT`M-PHP4n};M_;~b|LLCA4l8J%39wk`a?Kc$u-`wR zKFk|J)!_4BYVOUy8yhN=)7H}eu+j&ydx>uQ`9ZcNb~+~(I#!5@D)IL68&+>+`Ds05 zySlji$IfR>@1;A7H%;l8a#7k=C?h(IXA-CB8!C9hO-*S`mIjUo$P@Xm{k+*X*Mf{dylG9V+Gn9rUPQ_G)b z*}cYxKmEojRT`YI_g3g922Q=`lPU;igGR*G19Nh-R)qc9cS-RnHslPs{=-%;wb03v zO@>LV($XHTsi5n~SfTO6*Cb(4ivz5$$KR82M|tVp$0nIw+qgybcq8R2Sl6?XTIlW> zp5|<{{YYZAz$*7O)NmoEU83LFuaFkhp;MlrWy7y{LOBubqDfGkdE(40fC9qAs=gRV&a9t4yW{ z%f}buVY$7Cx^kJRdj<1eFM5_ro;D~BOQd@>at~}i+7ckgvWj1Jbm(MIHgM?0f0BvJ zTMm(vn4KE@*}LzjAl=lH!0d6$@v$DOuP&hl!`Y9pv^$oDFNT&l&mRxjpl`eJ*L*D7 z&pI6JfHGHFewxs>J?CK2r$bPHwBh;0fy9M@v5XG{r2%9p_(!$h0=}H1pIHQ>Wa+v7 z+^PtKRcdN}mBS#izV*ZZl`zNm7fX({Z|H>yk3pka?`wMZpBgcmj+^b)dhG!D&OB<1pU;w zKe0}GWE~s_BpKeh>JupuI`p%bsq#8!JP=tpuzP8)aQ&_({=CsXU*&|A{Oi{xOla`4 z1)ew94ay_B;>G0%IzT4Is1^>tkSV0Rsk@NF(>? z@sbn>0h9P>x(X%eUk!Ocb$(Fjh)?obF+6aITh3{zk|;Y-QxCYPsVJ|azC84|Js@zv zd15z`Y8O_AY#MF)E3#d+CnEWnMjRv^l&(G^$orYXL$(yyV)DIL%$I{o7QEI~0%n!h zqtSeY&dTTHYS^b{*Ijx4!xRL{6Ja~JmbVaBxhE5HZP<4DiX%3&60*)-3E4HH;>$31 zD8MzATaMk3ix0Tm&t?fvpH0dxs-fp&2u2Sx(mUou+Mh+WV$pxqM{0(H$}jzl9MkeI zLokp7^%q|%tLV3XY1QG(O#1knPpE7yv5u?lhK2^!mtEiDltbZzM{iT+C#+0cYVo8mIvn}CQf|-RQaCVuFvTkg3snWQ zRb^CnYye)~J6v~Xm2zq=5DtYN^UwFtQ}cZl;2}mjeiXX{V0)4OETWk7|9i&U|IYZI zboC$5J!d?cJ~Ic<;o1**EA4Y?vDvtcdKWnSTMZOK1=lV_wL`movFGsN~BRNe#(=bbN0iv{=FzqMb?7u#TzXpTEgk-~Sxg7JEQt zWg|qOdU+~nAI_V$OVX29OU8pbKGH)gE4Z2t9kGpj*)dw2AgLYxL@B|YmL6`LR!O?f z`zh|H+nct-3_p^WVARIr`}KS_6=c>4)nF=#0?$j=xzsp0lmsrld8byZkSn&X+4h5v zS;X4`Gvlx7SVZIXZtpGF8WX)7qx_5UwTB22FyXZF2aaoExMvtDYu0=sYIX8L%El}o z@R>5l++bGnjAN+o$mT&l*ZeBdIH@Yznw)t;>ywSqKONTP}^bW(ek){0SE23lGxRP7O?1hH2_Bze={?|J@t z-*eu7b8>QW&UIhk?{$B!&j=FZ_YhpM;8W{L6E=>pr=p=VnLLvsA8XxvS;g~Yy#oC^ zcq#u5-ja8<+y5&1@56tv1Z!7~+FdT7WA#{26pePa<4%Ag=)nXVBQUVU*!s5{Cz-Lp z#o9)0hP92M02RKH;eqJP64@?0mEdH3{Ou1QC))E}$11Ug`Kw3&lAkoO@0@tof4us( zY??;uU)_Nk$H@`%JZrlwAR<|WU=*^^|BKB)3)=`7tWkS`Mi@ZdZAGz2g8% zQ5*JOz+}qL4!1PjJchn)m_M<>reM)@_L^9^snJ9A`;`rHeZnI02DYYC8uvKyu8NaB zbWp*r!8YQjYhX5?s2@v&Cj6FmESd6E_UjQ#vDX7=n{QDp7eLF?yjiS6-G0*ZU0Omo z(w_O%Db{F4LQXhXE$rs&`cqBc^y78c$f>UUtlS&po5>$B7-(0E<-5eh_UhNQub<_~ z6wKJ4ddZ3XaBP_5P}2s@kGvwn!M<`ULL@IrBLyEiO%cCwo&PCe>{kzZvY<6h*TpGo zw^vwA371%8ae;Vm9chYHVo}z?|5R!4Y_cD`H1alJdYjWOl%{*o4{`;IUY`YEoOhrUrX%4HH9ol2gX^95DIUGC{=lW|8=}`;U$1U*5Svs z?y(lkmH&*||CN-Kvd4e;m^JVIW0w5yhyR(F6#XkASi2?*){&h^r=P%k%_Ls{R14yD{A6$XF$3T{B8Z|)nCnq~r~J1(&^JQpXnRsiOn zb9SXZx8M)A0PW^SJ#!#wc9vgmc^Fi$*&$Kw!T!sIrXyo%nFrBXbtk@$;SY+3mKt9MzcDdOts-?^{bDLI+4#nK78}hU-bUS(=Y#&*bK9t3Cz2-nNHc4bXXCL0|}B7+NmK0gQr? ztJn15+2&PkRu4?FzUx5ZTv_h8md(Zv^(N1+`_vj~%y zPQvSLRhJ?IppjY<8bUvE2hm?maYlO{x5l;qw~<*@PyBDySv0Z#Pu1W3qmNNn(kfO` z3RqTJy@?XZTCk{VcKhjK)73uFPHZQBR=TK85wnv6YC`CmFnH+GH#gKCqebI!dMZ(D z?aC%W^me*xuc(dZlimq*RoM%E`?x1Pk;95gPx1v98a?hVcSmWymrg0^#cE5DVYam? zRWtB}byMp55iXwE8mYcjfgv5LkK#eM(T6X?=Xln7qnveTfPEn}UkjDJd(PWcgR-Zq zvKmOhV~2Jnvx|dy9=S34vGT?$*V8XK1wJtEJ!schZJgvCDqC&9hU?q?OoTq%nXG5Y zhvTUP-(t6XL?$8V?i^Xr`v5(iE6Vyp-8uluPi z(PN2TZa5GItjI;1x_zZ8Zj$%6VOWp5i1ub=nR+cYPWZ@A-*h(J!YK}z=&t8?--T+T zV>z;3?et{*g8MiCL%EiWarYt2kxr1ZUZ&V9{$0+hOQ1$Zp;yqDn)(m$HBp^J%CD%> zfyJR+m^e=s(L7rB%*BE21h_x#Y|QdicOxu*=!2^jNr@xl$8WbW zcFQs+DYoaGTx-8&`m*pxEtX~vZt!^h=f7RCYwWkoCKjdW-xY1kf}EQ^Xj)=)9^>R$ zd!r5pi;z<8?;*oCy=f}m^_d2LYSo`)%#6^c;>Gohxd_D^n`x0;%cQkaUVa&Mw>)ZP z-MH~KN+SjUwUB^Yc4H{QB~JTfLGNV~U`Ff@^O8p@zAjv4?grXe8Qt*A@hPUZ77o~bz#t6SKfPY#<~?`9zXT{NxH*+k8-$jSdiWDnppZ> z-~-7H#+W~Hb?}Z2tR}5QH|sP1vhrZNZ$DoWG{%3tep@S6R&6Jz;_mwr!{fp9v4O`A z)50#)Sn~U(T8g10E)PT*L`kpAA%F%VO{;(!hoN32Wqh%|=3#&l0Z~3seY`jjlkl)` z;(~9aSA_0_;LGQwus5OW6ULpKry3#3NdJC*62M6c(8oc~j{)EEh>eBVJMZ(XoPxTR zh`bzr#o`!?-w3pQ3GewxG)ov48a!=F1b-q-!0%=!z?U*=00JC<>6-C%?X5FepGk+` zw7f>$W!rZf!PXZy$}hjXs_;07_{&?O zcQ+U-UZ-?Ln|jb4Pvjr@M-a6RZ~`^t~~e-Q~Zdw9SVs z7;C+VrP#2Ks}O_7n9ijPpIbqLd9qxc`t7L7uvlM|*b&3bnl7=m%{$k%nb&~KWhy}cW{K4 zBOV-K`>pek2SxWaE(f9g1I_TkX5)VniMvgO5)P+Ui!S{A5<4ROsV9f)igNgLae%-5 z-t1o%!>`Zh;pn?R6I$G_&EIAY1S#ioRD>G%1I^zLS)v-{>od~1y%dfDxThzkl<5%$ ztLTaWMnvTJ;DdlMnQj2JeEa8gllF>ot~B8X^s{pKP$}Z}b23a#NoJL6L#R@&!QWml zzH4sWfse|2{On!Z{&g-P+iX}72rs=g?vtjr#k$<5ylw%ZpzOrBn^m${=@ zN@9CLNDj`!OqZ3d#fCm-yI;4>isGBwFPuF|3kTz~?sN3N3zO>Hh_H}a7pJ7?;=qh6 z^6d(jdEIrj0k7P}uiee>0Ul;;0wU`k=aN2x_h%TFNC+MdiSdmx6c25^5+-(3wpW`$^ zxV5tgbp?VE0NhNSt<;7?y$o!JmB|xPe+te1ymlW;oaa3-B-Rgz^N!Oulxs#q3EkD8 z2-rGsSlvcUr2bZ{4lwcsbf|Kx#WgPz<_5`(GE7&>Hd>?_@LK$de0P6&)}=9i>iBAI zhBp`0>bFxMDU#3FDy#J0A3YhvF(^ zRSxGso4phj#rVdP$$~L*2@Vc>k_cxMQ+NxgO6|I$y502%8j1pAkn_X(yR6zWmdz- zb(6!I_|pMkU}UDjvEwwms$Sshk>Ma;<27n+i70JsE#mh9e~>c@vZpr`1(m3zwZG(a z_yvbukz^fYra4x8TpPmpbZhcS0;6kK8 zy>DKsZ=j3+TeBj|q*dQMnIL|ir}TR>4V7odZif5DB?EImljYqkB#2R7lb~`E2lj&m z1^=qR8RdGKY~N^H9`7hsuXxezPT0jqeN97hPy~$4I5lJoEMnY@(dI~ zLBC7h?~yhSSqJ8@F#x%-GIl9E%D8=#5L+2R&X4Jb6ni$7cYoas87d=8U}hTZ&~eK4 zCHa4CL*Krsk3282scSNdJ}5l_sjVQ()FaKxu_G=6FaEEAmHR1ur_E} zY$OR)PZn;IPpE70evTTrZnr!@cJ>v%u;ZlXILvvVuHxD;zZdF9xp_}wq$;Cf#Tj=O zbb|M2*V(Mh-JU+}wkex-x=-An%31pz(c&Y0T~^am@-h~p6;mN=px9PSpxL^SzSigb z${n)z@om}5Q&OFF);W4b#nX2koQ!ulfrzM~sRu-rlDunvq{dbF7v^fSmzNn=Bt|s3 zqlVCG!SV3&SjwP83nHj}Lzi>z${hK22fmXP6`mKoF+$ekXNk=hU5&>QcmLncdhxHb zq|?5&EDqkHLBkNN^($G{>7dO@yz_Gtt@Fop{c+b}@Q8@hajcWW!t)olN*2a8SIf7n z|6H{BvMjSr0i3_G=p8M54$o^@Og*{*!metaqb`$8|%NTOg@q^f0F&DyJ~Pa0qV}=`zkX8&t|dvfi}IDF#|) z6n#}K@?X;e?($G?Ui6-&=kPJsUcv8We@cnEv&2u`tGVUK`X1smjjx0{$<$(Aq(N-wRww{kqVJ(+)6}bE4D#Z zNQEP`em81esPiSg@gegqZxFiO_T#n5?bf_(k>n@^wziVanTj>alWrtN#C;!uI=N`xOvzpCo<3G)2szVD&>EG^8nmTfJJL1%6>+gC|`qwaqgro$Jx*ckM zq!mW|GeoMb+tC+}vFaZs@;%qr`6Q`q_w-0k)ii8&Vo%32Fd%1wZ}j(D`3rNjj}%HF zDu$oQQqInA;2pU+P@yY4wT-1OQR~U27UY_*wb#U->=`a*$hbbpPbf7Wj}g1DvYPh( z0#Jkm^E(Z);?#}bMQ}-WaGu+QT|6**=RU*vA>o%6Ul}s^?o(`Pp^XH>xq;yw8o|Nx z+1p{Q78R~0$Tn*v@lxMS*foC-ivG4KQ>q*XIAAUF;>M0Fw=emEj+hQr-x&|%nh+Sa z{&?Z#(H{w@g_fzhZLN-Kc2`Pwbr7yRE<~Z_okqD^g8OhIXqwRV_CaDj=UUjhA%;}` z_5@(1RTBa2nc~2W7GkB75!L=22Dd)TC2uJl5%}<^Ey@{;D19BgX)4El=#vW1E>R-V zj_w&*H(O5=@@3^YHvEkhH_FFr&tG3U%6;SL#*g)@t}sl?NRlaz>E}dBh1ol^Jky^7 zkcY-MZBni|vuN(Lq~^lUuyFw6-zwDWd&s|K*}rM?e~_VyFxIR>%QX>y{twDy{)6&x zh~TPFTW#25-=S>LjMofxzUF>j->$G7`oWSmZ0qF7f-BX3s|3&Tp14$M+O-`TvY>O) z6J&bJ3|gJfS^8>yRUs4%J}C*E%NyFaEpd<+IKy)Nu-|K+CQn4SXvRS$mzlTDvnL9y zDfha!J<#68XdK;;s})!=^g67OwQYjo+0N}cjV(|O@0FP+Uq;2~AJLrl(*qV%8uk1_ zTo@;5jo<$)0~8)spt+nDJ&Cc?ykG_RzTTnwY7(lU^s?>Dq}5tvghJVda-QID=jSpP zJ=?l4O?;OfSETC36L`Gv(L$II%IgE++}e%&*Oxc_gJ^A0;9F>r$MyP|gM7?uX|Zwx)+Y#r33(i^`BYy-KPB?$MF5UqBrXIX|DH`ExwNc>WkG6p z@A)Ci!bQT4`eb0wb6WR9nKdt}yt$`#>+WOOh78N>KFnSzkDc$q5bmM#`7g~}y3TkZ z@ll88+QY8}H##op{Ltz6mNmGpK98@Is7@wmoOQv0`%(vk?z~7(?<}O}UPvrAnRb+t zw!4?OtELD~!a0UWXVW5Bu@cd=bdz!ItmETRAOHKN_|M%%Kl>YK?jIBsf5uPRG`Hr!BiY2aH!X zwj{9vc(nQ!h9Js|JSSI=+W<7jV~(7%`GIP6184g-iBW&`LbiaVzEz9YxQ|dX`Cg^S zs=i2)RYM?`&zlCDD;Ux`J8-n?<+#0;@+Xx=G~FE4yGO5gpfzs!wih;~Jo_wAu3gJm z=IiT#vbH1o9|ZkB+f`PDi|}!m!pKoJ$s*6lHK*urP}#NK(I0=GGmbWHiz>cIZEtnS zA{?j2im}#F(OHFdf}on?8@^R;Q~67KKp_+Z=-1AVK1^? zSIX<%)-uqM7(UL^L49}T*Aw!Lw#WEeZSIxjM6cW?P1Etg-N9Iam94vx$*&!TK^TI$ zd3?_j@B*K;J{Qk5q9{+>M8)C99cM?KICBAi_Rg{3CvFcKSsr@ucH5M-%0UmpW?dIc zrS*}Oil_qTZ}yB09YfJC6|6cDp08qWv)pa0u{)L>-)E9 zp}#U6-*g2pEsh>ljc3kl+Hc1Kc-ZzpkMQ-P>n+zoXR_|A`&_-&PU_WB zT%yAL0r7(o^D&w8y(F%#{R;1vE(-6ue-EMbTC!5$p919zGC6#OI`q<$BBn%HZ;uYC z94>jAE1RiDMD8p2a(&wG{pjS5n-0Ki-{u3|{}pK98*0tAa(0w&r-raooa6cYlU>cs zLm|530*+a!}`xUO0zMAt_k*0eZ+fjgjxW=~` z9vS*;uWm!nrd-RdZ`OO&_uBOpEy6cyO1G0;;OT3m(J!A9R54%a>oxK9<=3L1_cpzG z_7upzpB_%+*Sow#krz)Lr9SEX1y_>*l?uh*6a&zDE?%9|FL@cickj+O^QQ1Cy_5Iv zD|~3QBl6wwGvY3P``b`Lq?@MpjO}EW=J=xdR0()6tJ1evL zL3C80!uG9aG9j>TMcpK(g7el*aT7U@w_e_7P-^D}&n>7}0H4@kJD)CJ|MM$3(pI;% z%0`kM-Uo@xz9(f6+syqfWU7vEwmy5D4c4=fZOEd8(eZ;RGxS}C>AW<+o))L2wcJZ`pOy64fU zfCt^Ljd2<$Ru*3%VEm@{iZaW`?5h6QRZjeRNYI`z4T0Icn)m?-oSS2LdRhp`0}L6< z`OY3Yot?pJQw!{Er}|7r+4U5c;66frCk6BOMD-ysmb%}^PtD_O{=pg%iC3a8<#gqj5zl` zJfDJgb@k0t5wbF3+2MXZVVo2Cw)>~B@LoM_4Nie+L*x=BiaR*`9GTL~y~w=sfUhrE zITY_TI$m_k?DEQm51Gd=M#{!03OEj2(i+IqK4n(zcT1wJ5|34t%pYJk14fn8D@`{O zQ)T48KhqYb8q-d4u3T$4d`#3|>rwERWumZOY=N|zXX zcxjk=$@JKUJu+DtzbseF9x0fAnfB6HJegD{$8h?{Pb5e17{aVwBkJ|%4~*f#+~1S+ zgvRa!RhDwYdi(jB++l0C>l@Wj>n^F4y_aiHYH>*Nd&@JkO*9gN?Q99lx}<*J)}B&g z^lRi(@C?QrczCoy|3^!>UQC)$k?N(zrjT60VR@nHR90^;c~fF78`IpM{T3vLozQz# zBE9nKkegEB@W=d1!HZ3nO$wSPlA>bF|DD0go?B>8xbD^FAkAA6HQhaXNzbD-%W`Wr zb4K{L&6IdYuGzYKn2CGfn|2QNqRA#s^Dc(a7r(%dQ0~1ZMn|?6FkV* z>NF_qPP~NL<%$^fSJ>p|$-ma?3jb8Ud>S|ZLxXdJ5`xXNiOP)Rrba=gi60UcX64(* zUX8~v@P8VEWlHzgsOqr9R1L_j_O*s>_(P-(qVe~g^shnw>W(hrRBY~HGm6>36-7v{C7ey9btP_-5HyRA;7v6B?GO=L-W|~7&I;an;qUC>L$Sx> zYvb=yLJ|0NQAevge6cf^vBf*tFAP@yR9ys_B^Y zTY|$tuRhzd!s;%LfcVHwjkzU*Yq}y)EWci&O1zqinihCeYqjjkeN}_$s>G=B%jF?B zLL8RhCQ!U)1tr#cJj|jggb*^3+jGuKzj1pCAD)k#+_0xCaazQBhb!C01z&AB@_fog zoW%czYeO>oO4dg7#rY;3Rlgwb7DK;m3L5H}#(U2P1OZN5o39xAG>Nh0U{m z?7g0Zl0gQae{FlO(O*u)akZ#JrPKS`Uh45HF)#b-8wwm4G|9Q};|c88opQ@su^($I zlm1IX;L~R6na{Ykgmh;c?rq?mCMK(fV1V$TC;2z*R^RQMZ#Ex#9W8y{>J*urQJzkS0{R4%If&fWas)Czcf9H? zk?ghj3!zWq4m3~}V-%!An*TU}*@Xf_>7j~abXR&EvSe3l%a(BfNda#-;)-_bMt^g@ zbCnF=TFFYT^~W6zZ}7ho5)5CG6u2gTP=K|1P;gyZGth?3b;tgJNe}Q`TZdijQSV3T z=wD{gwWSi^M+>AyXk%gY{7@0CdM>-U`m~#{WCY%4*7He4N^-%+2yH_yr&8uK1R(lV z#^8}g3#ShAJjp&gTk!CwaytWGp_PW{kNO-6mTYuZipFWTNsy4~R0E*=>8sx@N%19^7o3B677@o09F zQuSoyhND&LyBiQ`x9oc1F1T;kpun|X&*#62cXc(;$ep9Q{n(1Vqh!HL-a=?OZh{@7 z8+1iFIS^piq+pky`-v}}L$!N4Qe;jvh z->^2#;=c`3Q@>&#Pr?&-4+u_2Pi6DoAnhdwZzjG7NpoN(-2453J6ZZ-i2euEn_o@3 zU6y(SiJLdLBQ@w(vshFV3*eW0F1}ap{m?lmNwAM=PbGfgnKeeCbY-7A63@BM6Zyt- zu)4c}mR<_~6-jPA<7%CFbW38S!}9*M?bh>2L=}6cft8%!*iQ_E2&)g87RBf&yQ|Qup&o6r<~0L;R$hV$mMBD+B67B zP*ub9;}TrQPDPE*UgBE3s_0Gta#VP!93a-WW9IJw7$J786Wv3g!V&%F70(GoVusE6 zu8+P4?H%xYqHg=cf4@yF<$5~Na@g>ALs2)5d+a`!GS>%Jm3zB?djT~o&^cl6W{$eC zSJw`P7ryT%g`|``NJw(Aw~13#P2BCzb#+R^S@Af5lu|pM z?t;S}+-jVVBt$iCwIptBkc_+Xi7-9ET|vk%h^!9Ac8)x zJ6ZAChtlFV4K{?hOEV5latoh}+9JZmK8U$w&3u*hxa5Y|!}Vlaz+;c4Ar3UHnutYd&>&Jvy|j5je3@c(MN%;ce}6A=uNmyU3X+aB;GUiRg-Or z)I`J153=k85WCJ=DtF8YRFJ;we!XBaraOVIPdtWy=VUvxUSAqDgt-goh(GyRiRrdJ zQH3V2R^faDMq!8YLHz2n`f$C;NN_YyPQYvWsVa^ntk!?TE2;R_H$ z{q4}YAG|KddN z8gs|6HU30jedVy1gliG;QGr&u7aqtx4dT4rvNTdlnV7wFY+97h`-ugo$-2Kvo2^Ys zwNRWm*_601n}oBNOma6kGTmIKA0HU~gPL%KQRe?y%@DbK({6zhnlhkOSNJ3Bk`OBY z!LIF>k>O2?SlQ)?_x&ziGpMQWnShmi$@x~=c;~JKCD}K7<-FCpzpA^I(eZl8WHQ}+ zeg#wEvL2w2KWsZ6a&|b4Ub;ZOA0z_(0z#)_jqn+obt3*Q|)M7mcta;7G8F@(2@<49az=hv{^dR zCvYb&M7ObO<;qrmFe~AXO4Hy{S$4KE?aSm&>=?&=BWr%v6ui$uZ z?S`bU++j3Kv%Ul{%lh84L%-?qJC5W~+$hm&we&*y8<-1mTqyp$I_2!q@9FNn2Cn*R z@BM-F6cBP@5AEU!!v0IX_R)@v^j#8riG}<4E{XHzD=8mFUvO*B%UkW~wwxHH$V^P4l)|HDG`=%BkvTY8tX-I4kXWNTux|6pnx$d> z+@EH<^3L_$y0?IMf)%jlyoeO10(c~omD5J#!JAz}97fn3c-gX_(o6>4a(N(nkKK=p z>a`wcErG|KJb5_p{mNY%L5%&uz1JnZrd)@;a%`HI96Up}JR!yT2(-{<+s4yef378R z*~V@Z7+yh1UpGi1@_mkPJ!Z*{4Bnl~obT)iY6j2-E!YmS<6E~btPL8cvqJB*+BJd1 z$Fi-kdwyZ3?M6$d#X>B*@GHSiOU;79@3B)

  • v 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

    *SRj#Q8Y#FSJXZKH2lq&)PLdq<9x-d?WN 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�y`?)U0Jokdg;(O3~VlH>Z-a8&F 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�v_;ZED4W3=qo>=DL^`R+zS&{TG>AC;_U5h3=UCpQrzLO3pY{S*K?+#YsuRW z%a1cLDmlOd&?XnO8nty@V`2dAg?%CjYvy=NQ+| zz&%mAi-W2a&u@ho)y}hGNbNk37k^pUdEY507Yk4ALN|?#~g9KMz60Tw5ky zUa^7t6f}Kq@<%a#!ApGNye5|}w2l#hyc)Sd(|FxZM;|{Qy~d7B&(jdmWqdH_v@d(( zKjYT<)&9xS zkX|z9sukv@;qPYIgdZw=mSLSXpI9SMUo2KdIzm`JX2nBA57byhJy4bq>SRQ%~&Y$%bv=$`{ z%JIbbMiCTyOy-kyspo9&Q2Et4lFBO|(J99~SXDYKe!!mdb&Mydq{J{!I;Whz+{e0g zKx}Y0b^LLQ13%`HokoY74;j6hFrK0Tb}-J64v!TKl_$aEeEwC^jFPgK&2~^{O!IFT zB^?XS(H`~KjR31gy5*8*xZEGx+!$%=6D?BT>mx%OW4%=?S{8M54`_I+eXye^5fA1+ z3hVVvwoC;Z@5>eeW4!Wp4Vw82rJISzh+Yg8hHso$H3vJ_8{Lu0;jc!|c*0liXE{i7 z-GK;hZwV(&7Q|fT#S^KslM09mR03{Sj=JT?(O{i0$(+fca*e+tnJIsyIYRr;`VOnc zgm7DI+&?iP8ye&E(M`*(j&~-pFwPgor>rT;U&c?<cH&=S_KRZnICJGI$=O&o^Mzs|N;a{x{ohV9ME`<*5>El-ATluvDhc1c0| zq#i%)9NhiK)`DA5IE57@+$%7=3#l6Y%iiCS2qCA5*E2-hSCa60Yo8XG&Yg#QF<6*GKHKfQ*AO4p$=TV7)(R=Kb>xJN#z8#d<_QSfx_Zo+pnU3GSaI!3Y#-R-_n+ z+e-U}k!S?{zW(0+x>sMao~{0rVP3yz@$$w3r}UnTK*kq5O9->+K^$^?ep=FP+f>d#u18 zQeKaxhz4VX*!Wpa_rvgUWIE6acPYiSU4r1GQx+@vzJ@9Ic~>8y-uN<1b7u0m{q2Is z3c+Ue)L6O*F|q7#hv53X=Ep_J&>tGW>`*E?M5v>MTY9&DefED9IkXl&yrDi0}EXicqINbB0^oTpE!n|8lJ;Ux18omw$igNJ^CX6|DXJG{T57 zVP_|CM&K(tNcY*Xmn#eXKKqEVzg7lW+8ZM8AGWL?e%Hs!4b!9zY-czz_K0N5EW?tr z{D|#UFx(`lj~LLiDMKuzVvTiR`8Z1=3tc5b2<>D1{qVhpSf_Z(kXM(qBCEj z$~4_TaC$IW2;Bb=_*i337+&WWqTuLzdB;zTTlc?NxS8}N@aYN-x=s8xGa21~L6XQ^ z@$rVQ`|_WO955S+mkj?qTZsSjGI9P{e;{1`*Z4IX)w;9axOSwu3^KE=2&g>Y_>O|4 z3#EQ~4Sho-d8Zy+q8&FNsDBrQuM>cOmII!AvIwA)4PO}-rF#ND9CCQzOk{#P<7f=v zRwQfCHYm`3*ywIX14%WUhhI1ZGy+g#VeHM?ZHP#woqr{(z%NnW(#cUZ0t{Ll`xCV~ zi>hXkocg`BLMX*Su-_dYA30mG5#!qRIdQx6!4K>WXgUXrZUOOJ> zP%x=87MK^*gDjHgkcWlvdz=zrjdp5Kj!I3|0MV;^_HXPjs32cBTIlbf8foX(E8)C0 ztO>Bb%_b?Oclu-5YSR<%VR?4)JSFkGH23*v1fIsFPmT1Higb!_jXfb3t{S2;`4~s0 z9xPp@92VY#34a1(J}YQ3(20c$Q$bLmhMne&Lo4bXG0*)mIZdoa-!Jy6fai73xx~T` z%`8ei#=X_XFy0iK2IHzlI_)|Y$xe;K_TGXvt+2=x+&MZSOUvS+m_Z=`HBc_^_1lRv zlU{a3iVgukiW{nHn%d?@52?R)YkKzWS5X%3&#DnbhuC^Na7Km|xg9s4Qw+!IIp?nH zG#yTrh>qP89tn%)bJN`qM&uFY{6zUS?~;%6^g++Q20u2l{}{L=>G3U32IJo6zuc0z zE3qOnAinszNmzY}>aJGq_BXTL4$6;hLjU|(E@G}rip2PM4bs6O&?Mh*xBNl4uPN$` zk@`ToQf@DrPg|!zLQ}79J&y-BCc8M~;Q1HiKsQzXt^BFc+^2!b^vCEYRd!ZRpO2m# z=Sa;e?iv!L)3f=&`~ujb!Hw)m{aiqXORradV?HQGHqiJLG3GR+IC)#&4VArSPMzA5 zGJy7`>E+~$1Y!F+d-caD^I0;8e8}58Sen6WL>q3pRl3qgCUG?ZmM@q~`h#cSN8)Nf z7=1a~f(x^WW(k5w8d1vK3W%Q)HoQNOg2JIkLNxeAHg#ME5~|;JqEjVv^K~$l?bc^P z?rbu>^>m&?$$J39V|~){t(XOWwIh#}WtvWGatO%=tNRE1!t0{NMmw3RgV=7JECjEs zUF{C2Q{6-4HXGde_1cx1uaE$l|B7QSwO9S9x?-6q1&Bm#j%1Ogpe3< z9K7w?!_imc4{k5zU7qQ+TFq^_eF3$Py>0blo9K1X*f0LJrkjagkCyWL6^NrEngw;c z5_ss~rFLdOu5Z*L=_iMB4vUcZv1ak@2yVe^B<^|WZ%)ZtbFj0RRhiGg z?pG>EQ5a*_yKi&k6K#>1=8E8FqVtw_|7w!T>8+eSEH2fGybU-sA&9PK_97+Xggoh2 z2DHS3K=w(0dkj)BEpHfuVYGibBLs^?*K1cjA%@tWs|uAvlL^?|o9aZ1l`(&$=%5L-OOmi7j7NGV1W{oIF2% zx_8`3`Rb>+KgCm7f_@8te*ZVpvDKKP6$RWmcSpl+Lt6#^IA2FB#b?P~U4IBXj=J0I zq|xrRgY`eM1Va3XO_91`)}iIvw0T>WUs zZcO&+icM=P?bZ|vurTTV`b1ne&AE+iIqItV?MZr$mS@%|k<|?mU(@>L8GBKS;N1}l z)awr`3KAM@D~p%Ob~B&-<^LoySWln+nF)~&xRAfScQrvs^y>|24ARSyP01r8hF?IG zf_+j(T!Ny+v@=le6w>sy?Y%1VN&=uRy!qtg1XBJ|i#()>dtVy>DouAcA%7t`viYG` z0}=i-|B=qD`-#TO(`FGS%;k7=`l&(y6HPUPz@1Pg-hSztGSs$Y(q@&lc`%$m9_v(~ z3M__4uj|T*=4LYMS$LZ<`m1NSi%gf;V|DFkNbOghh>25Oq@JivD|o3Lz`0RO*-3#4 z*cEP+Q$Nady-2K}SAsy++4K~(v_o&hD0be`rCIoXRZ4s#l%<52-sqz;L>N@SKsihr zvl}}OSh}6az~}6$Ojg(Z(c}zjiA{D4g(%0 zP*kT#$5*BcerBUxEjlg5&ek&(cuGSq`k2cqWi7$WxW(?cKaw0j1YVh0@K3+8i?Fo= zE~p|Zoxc8#A6h`R-b8~rh;7*)aZI)rMNzlMa)YL%+Wk@)Z9Yg8@EU&n$()gTF)8i5 zRQ#J07UiI!UfXrlk=;%axYCq*%m(|=ggn26;WKCW! zw;x$WgvHJiu{1X|>3j#q!GfJTGHemQs^to=lLwGBMs$nNAKXN*6l2Cu(jj(|sv1$e z|FyFgWe506n^?phLw@Nxgi|duR&nU3!Re$W4_4mud*KZhc=A|Ll3l@tu_pF$gdUba zMbIX9b=ouie&9`4&UpH>hl<&lWuuKDHs5AioeE^LvF_T%UOp^q0UtpS0f3&UR_<)O zHn*Lwp?*vk&-hzHeNH_RalW+JsX0-CJu;=ko`lYx`r>H4s`()FkkBUUy|?^qJL!hq zpSL=?egx$C9iz>mo*K?6Ar+d~V9oha!R9JgPuKZ{nFacJ--D9LnVPul;6;Z^*G0Fl zUT+o*x`n3OZYjrjD+YWpCKKxdhp}x`H_xl~KX7Oad8G`t`YhU&dLN}$a)`&Qp|#Wg z^<@ODNfDq}HA2Z7nN(0BIRFI)ixlS0ml54R$Z%L{#hnw}fF%56usC3Uz6Ebp(Wioq zbcoqWFi!-o;l%nWrg$rWnXjbXYGFHDy5)i6A^-=O(+J^$7gk`=q$A?Kh(MLo9^#w(SagrR+Re9M&>{`Z#}!OU7o*oq9=aPVR3sEeh8t6|8a? z4S-6r7-h9!<6?kCWe?j?M2rw+X!57%{(5o-QP0|R?h_B}TXvOEaFX^0TJ`Nd!4o&R z%eB)YZYezQBmF!L`~9|X=)n`+7=vi#QJyJBt5TX1u!gJYcl?(JKji3v~Ki+f7`s@pc(jiWHgVnMz&CP+#$Cz8y*IP#2`;~1Z4Md zfrKRuUo3*0+pv3>!;k&@VxnJ{-rR$c?Lf!S4?MnYfc`UO(UN@G@1}^FI1krlDABT! zD)u8&G-WfIL-*%iKVwyyRzG+~r1nQDr|tD=cS}w0bgT>UAVd9;w!sh}*g%lwl!@+{ z=YYleH(%yYj3?ypnrG?M36ou9QiL9A$edVQd>p9mg_(emaq(wU+zbzzT${_zX`KJg zFRWib*g<{sUh^%?gRNWxN|{eVO(d@Hg6nWJ&eXN@I*ZB=$Z#h%X8H85RgS>pdFZQ! z8^Y{;t1=mXb;8pCr5FVO*;HaHNVnU#-&Ql*p2P*jN8pL1bMcZ936}QTs#1egr_*Bi1 z{`(%+$iL`#E202QCV>FBSO-Mr!#yq#U@eaeNIym1m8`JHg(`{oDa-l<&st0&i~u5+ zR`Q4;%{+!|N!;HE-<}6Kr49WzjH~mfbsFt;n7J4D#Q3H26qBV^b`j{x!$KNDZJXx} zDl!gMI{iZPDS2wZeAloN-KTCe;3%C|w53zguu@u>6rRqzQ)ou<;&&?X`?!~-I|oCC zq|mGqb}Dk@ORQ=>w{%g^5peawBXw$RgOeJ+mnu6k1OeqE1xb3?T_9z-A6H& zDD81RGq{tzik<1ybY$fb%kr#B{YiL!rVD&h^8>jkY}mkq@|c8QykRb`Hhc*dbNKp! z(%F?oQ3B+w5IpIRf;0(&Q|xVX>v$GAWYA)K26?Vx4l(ZA7I!m?i4~KK>DI-e`i%45yM*|Ak3*5&-kY8+zGdbE=FU0|?;q!TqQirVjoq*!4HlEM*C%UV zE5{ZO+l&k$#pdL1C-4u5x(kJf=n1I%23jn;U9A;mFJJer&!Qb`bg*V02W8%UaM1;k zRr1Hsq%|tNRgPR9AZybDS~8*0b?qAwa3`_OY?+>^QFpRBe^!1I@<5Z= z)RvN_-(<`{YE%9e!HTKHhOteX^~&ZItPz`U%(|B4k#}D7rUQR$ z^YltEU_RTy^hM=Z?SaV%-p%A_c-*~!5|w^J7MdR|Maog!gY_Y`fL{f6 zSws_H$Ouz69KUaYVmAIyjJ@fB6Iijv39FSF<*@e8Sw7h@_3O0E-O$np;3D(!+-Od; zVFs`;2tBlL|54ejYuOl7J!BIPEviw7$R8@{$|~%)a0)wZQjRR@h^_xN9awk((`{MDCJzAIPo#TK`NGs=6DMR!y)xSvn!^KSpzj+C{6}Wz9KY!T$@X~`B8PJ3l>2L zUY&fQ%BjnZJKwuJ{_{D8`jI4lV2^(Fg~Lk#q@I%?F+^Q}l+^`DVu-VHE_0G;hMc`2 zlQ#DZ%dn;aWK@bW+za}RJtzQ$CD`RCRW=q~{kq$;>4@Yi-=xQ;Y9^D^!zviLG0hqB zM5Fv$B-AkX6Ihkl-$tolLlfX@8u=4yM=biO9*iC~Y+eu8-+_J#%~H})9NZ3?NSH)$ z7W!aAm4XVSHzWsGI9>lfaIAc#lZrO;x*SM@7vR|BG93GRuLa!@zof43`NSD9%}Sc% zk|ZAw{xx^kF`OUh48Pk|VunSOtJMAZLd?m>!e##qg5}?75=ZrBiLm?ZnMeUP(!-5w zjBa#x=HosFU$z$js;(|+z`(Q5O$Rqo&Y?(xh%G*yeuB!~=Y{W=(RUcKr;bYZ*tEjBtFexUtI;UmA5NHD7khawZB0M5lN%lMww@YCb7EYC44d^4#&*+d5JHs2V9) z`h=*Y1g1=()W^`d-{h1bsb=T@JKW~`wa2YIxl)#XZ0&ZAurguE4k|)E4fSX8AH!>nttH=We@d2$|$_i+fhlXH~RY%ZtE%By5@5l5-;qfIiU>GQ8lXHKujE zgH&H4os0#8RTYcW-)|*x!{Wm+6Ud3hac4P7 zF8pgrte7z0soYm6g(?)7E_m<5U5Yv=#HZqp+p35B)omE&^E&^$IVsIu7I)ubmO%o} zn7anrqcOz1V!A^v05nl4t;OW2l;wsp6}}4cBe316@9%U#E^GDh;OEP)eT*uy(-AACPEsf&7^yx{POkn=wpa`sxK`4ec^(|4F5 zK?S-u5=AW`oqCnB>|B+MifNY{{^<_ZIWH}je0IJBU&T=X0bv}49Y&dlXf=u?^rf$I zC>n2fo;Qd=#8AgPUL0h9R$iz^ff{~i=u{-s<6o!vlluFtKQ;4xc={3Mc`;&Y-sDX7mV&8=vK zkWfpLLB1ra_+nvFQK)c|M7!l(Iub9SVQT8di=u3KJ{MW}2zIkS)~nu?eZATv?(j(* z8oueKfm6>b>IvHsd9qYM?UR79c=g5x`p&`m-)6ld1{P!c2)^+&W-E8E#053kjww$30`Qbb(6{_6s z^!5?`Q<7r+B82|*y!VR%PlVt_(w}D{=B|T#golsRiLArCobwlLWC~WEZTzVFQ{g`) z=v)`L83VB)c6Qt1o9uvpLr_oTAo}y347OeeLTD!v^ zqXTm$!T>TE$pytM)g4REI75GOpCn*|a{K1T8yK^BI8LIsE%ftahA9gSxNYPswg1e2 znt01Pn`IqHxnltY#({krtq+o+2<{P5M=IMO@*3htVWCrh*IoUkrHQ!)4(lv@%V<-= z*Op+haMAOK2d$@0P+rhme6BvBhk4oNL{L8F`zn`kJm8GZwhEDc?+MACZw|;ZOP`Q^ zgGmZE#!0v78J$rOXHXAu{y7?;!$?wU;-s-LuxaYqA{2_6KKgHB#J*}rZk!liYPwBP zP05L7q&Qe;3UT z=w4Y32l&)QgNxPS`!ee;ZpDQi<3#m0))oQI8(XJ((Nt5!h7$uU(<+hOg|CG8AH8$*7QmNTXVY!m2ZWa9kz^PA0f20jG=4G}yw z0v4}Qh1bI%h5)c^g!L6TbN#~0qFul)j;S7s-3>VvZg{H#KY?R-K8)jLY8l41XVZyG5`%_gT@`r8jSDC9aF+H0Q4V zShzwqyX%MPMK}%ZhYghIlh2R-;l2`b3Omv$(k*U!7lAb-H-eeyv%>!{%AVyp*v8u&^?BdQ}~kJ4lJ7piaR)r-|TxRS@AD3r{4ZGfFYz;=h0J ztZZTv+u~y@n5!>Y>K?rMxUo3=(~_=!>ujNyglQ3+5`d>3ieMGvn!ULc_m!Qm3u{Ix;&~tGy>bb__cDtae(%F2f;AEcs8`y-=lQg)0042TWz=Wf4^Lq$L+Q+>tzpjB!H~41^lF4b?>uGkvrcwuqJ+BSh^=vDbAALDJ{Sf|3)6Xf zO2Ak7-m_ihi2T6<|BC`EcWHI$Mx_kVqA*+n z1w+URx6sgOO+J=WsH(M=B|~=uIB{>wXe()-y594t;z|ncjQqCi%Ku39zsumX$n2GC z(-*HHzwON!*q3^HMo#eYV7H_6;cvuw@zfuEW=87B^9Ur_zfk&+ofl7473Lx>rQw$Qt6bwGPtR?YxS#bx+I01hAEZt?bF8) z^ZO(bDeI4_nSBrgj4P&ezA7^u@=&0$k~*)5863%wMnJML4PJUw>Jj z-LiitmAZ!Mr%1xi;&j8+6~4(d}OHHAvBg&r%E7*Rma{HDidnU7L*gK?ziaf$G&w( zo_jC5pQ~m5X!yb3xZJn$kW0pLwPO36f%BGY{a$_rQdKN6BVwnAdn^TDrbL@mbxoOS(x?}2M(;vR!* z<^wJ_rAldpa&~qWg5rO*R}98K4Ebm3MnmQfeM(m|ZzelC^;NAObn%y)a2FtoZkHa0 z;f*0Lj5=t(oK6I~2(W!2Onn!tT`E3SqG>&;6VA|b(nv=?b~u%gd*2A|7DmI!o1Kq1H?LII<#Z%nwHWwU9;DZFYrpSHgXtw=DSoTB?gJi4a5UGr^f=v6KSRx zWIbT)8f3(9OkK^Wc3o2Ec+ry(RC%t_Coxn2JmX{jM+b)EH>uOaGR-659jiN=_*&c< z9=o21Qi*)OHOhCQ+3g#bT(j(3w?kB7GzNlYB09RI$c?_sq6S&Q@8@&H`B6;(Hy95w zmhPz;e48?-5#B6cMh!<8w{kQ6jSd^#eG~er*+i4;-iTOhRHiol6td1Eq0{~Um1v-- zeMRYaQFf!n)ohH@+-Vh$8>hPW;gt&qD-^+AC8hW^C2V19Tqp7O+bE(8ryGa*5u|4O zDz}Xob7T8&{mFBZbbw%lTOpB(^haf}N@;ataSzue*gbvLK6#$HBUMFUTXF*O@M`WO zPPB>pt)fMlc*3bl%!y$uaBjwDE3iZ`JcXCs(uzp6XWpXd|8G*hZBvfN-tTVamt{lx zW6wT+(G^Wq6F*oOq>gD=F2vXsTwRCz_-vG!Vwi}O-*fXn@hu%s zC9QN9PG?!jmEh_{W8fi-N5r?0*5o?Yx!p=S4aCs-++p@CQLPP;RL*AEG?Z$}($6!` z&BhPB1q&J+WrW(HJ^F}&b4{yzS5Om)AJG&EA1h(5|F0wB7gCdnZffRi`S03n?sR-) zRg)MY+O5iZ0==Tfk0#)5fII(YuwHjwhss58$j0M+|B(`wU)Y3rC_6uKAa zRiSg-7Gw$}75LGcg(zvqugLaq)7ME_xqR?tug}Ti#6H3tjqyWWE}q=N2l1KQ3ib9>j>YQn%tBJ^^s_TJ*$7AH_U>7G=k|e0$O+ksG5$WBD~y}`hUL+zb62b2BVI{2 zaUpe-i~iI{Zs6?E2WlSZbDq`LB(sV@t13^QL9I+ZywaPCPvy^f|&j(b2DA9Cc ztC!0REm1Bc1|0G;e%*Ym<6lhOomlNzdz}YKha>qYr8SEBpojV3D<9z$-Sddt83j!p(&g_c2W=F7$hzXj;2Cpa1IM=FG}HAsK>(F(rN5S2u^N}B*53`C$6)JN+x>%L=S9bc z6^aS{mXke*|5!Yjqh360#{hStTnKl)`Pabt+zB~8MDa@ z-xgy3X#5BeGXc@6acZ~uO{dWWOy65CpYEpxMThO5QA@QYZSF5HDmm@ZN`s`!Qc8)r?)8_{66g25-P?~^R*$7;g`VA>cFA?|+Xd$-Ey?6q z)+8zFK)p)?#p%j@q-`MB3R_fy0|4Hq$-|B8`faB-=>LxIktc~HvK zK3>8!*xc2m%EPYYXXSDT(QDS|o@YeSh%V2y{rL&w(I#Y5EB3SW(ZAzY=Y{JurPTrX z46~@5P@Mu@vHLuY%xXeRe9rHxT36oxUG$GnHwwc{EsDk@3enJ}s-RSc$uX>}qR| z5JI58XYFkU_z|Y-U{2%$3CfN-pV2N=fdc4SUs1bwRbRtzUDyiqSb5K zk^g$hW%UV=YQ<+6Q04(BA7%?-){eWYF1J373scr za92<4o2vG_rE(%Yc#_Z^lEb&H&l%S=?o`!le)5KX>t6iD6B+$@EbRDhOaE`};m} zxow#jy2QiA?aTE&U(x@_H|y_8*rJOcYP&?<2N?>PTPrZ-52<0zv?Xk0vDfJAZTwXoG#_jT7 zuSJ5g0~45~89bU{%;_$&uG}>S3jdz`q=%a znSnZmQ@oMVGrZAE<=%<5v8>$UER%H_`e`m6^*^1y29#P#wHt1+^WQK0L1g*K4O2>y zK$IzSy@k~s5Y;TOTIQcHA4>pi>!9NnwT{hZA<*XK#lQVuO{BY3hhoqUv+%cl&j_B- zZe5p0!QN}K==%M=3J3NmtXa*Z-4b4|Z{B6TU{vx)`e&+8Ds`&F<7&X;LF;>6C9e4q zUA9M56nicQFtofHkX*V{VmY`|E_V#St||NScTVlmrJE4Km4z)OCH?iv8nj3o*5o9g z28{Q)w>__fP=D1bA$f;5cW%%A*7;3dCE6w}-Ob&rvnKxU>#wgKt44K&1&P*o0Z)jf z6{f4E8F!Dewub!nHQzx8a=GW|EZx%m??K|_cO1rR{;2Mq8I#jtaVE281{=diSo;-# zt5j^^=9gCU%TP&6Ppy@p&7u~xiND9iGJ%nz2@9qz<9dyt+Mu+!l1lC>PZnTJ1y_w%qlg>76n#r5!MkeiMV<4jA9--jLV3 z!-+ZxKSlJ6_Le^YRSUDI@W}`uTFg({HbXKU%&bOJw+!F%iW#C`iQZ`3p54x*)!tsn z-g1~2Jos?Ddp6SDR?S~^0%>^oa70ozY$>{7vPWnI!FzB?In{H7-L1HYBscIE+ z>U?(|E55tTr#^fyU>1&HR+V>c7=P|OtSY1lP4*1rTCL$4(TSM@J?pv6a9# zrmk!WHvwerw~%D8bbWITw)W>{_=u|t_^7F!EkdMynK$7S0kt=13Z4Hw-o@yFp=>A5 z1$sb8{iTlhWIPSiAk*u^_8k3?D-=TWF?D^c*XZ5@|Ps+@JP2PSk7p;jU^jd>V`T zdmi9{(0*2mxS$u^cw?^;x{l!8q20c~J>!roQ!t8OJ{oM-_FtmWT(f)m!f>&k&ScT^ ztklXIQE5S!2L+HK7e%P9T*IUfk7>V;u84 z@^Lo#{KsS$b>!Qg%n^C?OTN0&Lb4?Nd7M*n_AZOM7jh=M@7>GFI<8t^w^8C*>e8Q2 z_SKo$vX2d$3lT$cRmCd;v{^w&!RufO58PKOX zoYO3%+9*twOHj~NhDXo>Yk!YWE?}>+iey1e@nb!?2w(kRSKHckxvV9+v%7GLLPW6{ zRhCsiHI3g@pkMxfkAr>=E!N6?CO5CnH-0$*#E3sXng{E{Nr{bY-K6pt{QZVJEmaUeFH@{YE(dwkTf2~G8BGOj-(^njN)emJ)H+1%F z9lPtoY)5ehRq7Rj$BR}E_uy!`{=ifE=*IK0$Dd0&j#4P3=GttRx53&lB>g@52Ez*O z#F-mGV1;S)jcc#5tRbRw`<0i*>$j=`&ImG|tmwnfVn1(7DQvaG3Ty_m1=B^L-?28eH*PV%-&nPNbn(u5 zR=t1+{|j)n^Y8a3B&>l8Ib6R+kLDjZW)8*@7viLc2!2PT^46?MB7Mq}+FP!c4)qcK zvRxj%itfgyPQKY31^Q$x0W#0;cO)V>`8$cZcDhLdmIO2_V(Ix|UqUJ_-agWgoG>g5 zZ}8uwcA<|JioKJSxL?y8cdG@IYtN;vMf`sX<$rFbyny0^gI-WC6l9p?OfBN zfyGRa{n=h{NMBOOB3-VM1yBlxmVk56HZ?U4t-NIIH$XD!O=rCLoTXZUaC(%kc8bm= z0T#GoN2^XW-G7m-ot}T7--tW&`W%&RGoi;h(sE2zz5p4NKO@(FV)8QYNtuEVQJuVS z$#+y&?`Gjur@r_@K4H6Ijf?4Jw|og{Jh+)x{nFTW*@|n^v|2IG%C&OjPC3*NVfu^( z$6sF)y6OljUrssb(T<6Ew!dn6;N^U%;+IL6EB8-SVB#^3%`;i_HcbPY-Ts@R^#h-H z&_S52b2I79T43kMQFH4-gem4wGJU0r(iC%WxAFW@5dcJ-d=rr`rj@~|KFeM^)u#yV zKAW%C^8f4e2II$Ih|A?U6+5Gjt9t}9#WZfI$0YMt&1GH5rko~AW1gFzW-;a5HnIlx zhBJ;Wkj~4QC9FEm)P-kO`kq}8xqMb8q;15^oXvr}ir!Mvqo$NmR>lFF!NAlH^PJ8v zmU6D==vIjDfcZb+CCOgiY}LNtn*OOB3qWl+Q>fHH;w|Uq$tIG{{&qmSx-i0J9sBTm zpKKHuPp_!ux}rA@VA*&Ji!oFR58!0Yv1kQI6J*`IOVwI0wF^gx-T1q>6*Xq zW9{@u61U~)Jd{$n9Lg!m9CD!u;?D3Hh#CWt_{~k#zo-mOtIr_V3F9 zR!GJ(N2GPLCDL}Bx#apuHiPslB6bHpn6^LK2~pK;+4!T!HLu*=*u4~wu`PavW?EZOO9K1p)2_|XF9H7e)GFr`rgv64Uu+QRBfF=uX08|=cI$IC0 zPS#yLG+c*j1@F53nhXHt<0+#y7R#ef^neDC5bpu}zl!Xi@@Yq`jGuM zABUI;!Gt-N7D@Y@ar&B@w;g#%b12pMoBa6TPz}&u#hg}513ue41G(a~CBhGR4&3V8 z&e~ZR%i7$;*95pYaU_~_`P-dfEZsQ}i&9@BOju}gshTO8#Ccn|Pr;-?BPYU#H&l(4 z&Ci%GgEXTwTRYzuoyxJ*jStT3o zd%G6SrM^BJMapfV)n6wXOZAn0!TBJ#K9fm1bHNv1ujrNM;cmJwHKG!AODz|z_H39> zOfs?M@uf1ZdlPBf7(BjKb=k~It!8YVxc5(GLwhTglB_kEM#(>eTtvGKl1`ifzWpEk zAP4;DR*cT?C@JKQnDDv-YtFX8s5On6h=ccOodJ`nk#oTiJYYICSvEYMH5mc0MeqHl zD0f?zK{nf~;`m}Zkoh5w$=$SIQd1J*(ZXuOSZt4SDwxy{M7b!Cb%uGgGmoCQhLeBc zKSYzRtx6Ut;#4+BI0eVTZzbvHx+1+{^R*!#&@Ic-EZ*-AU#6FN#*BQx>c?kCrI=#k zXL_jj{&_rQW6jyWf&mDirvF-N|0{5_Ag4gvuv(1K^Qi7ppHzU>R2w-AEby|((zEG3 zAGnt@avby9Ih(lQD_^woYWvIIcaJEu4_6;`IC8+P1ffMTty(rG`ictjdw3gBmwJ($ zwNu@#6Tiz07~arpWxSX&a~5Gwmv)qe7cmj^UBK50&+w0Nn?s+mvXqD@xvaZJVig*I zz%Q8E}dRlZ10d`&sT7 zNLsYYXE9zbYDbDTH8Z|X3<+(0C$8txh!P1=2HgufoV;o?dv%t1sLnL|f}hQq;*|c1 zuYT66v~KTd{c+z_9rzLYU&sX0`Kba$i2=}>0q_-ok+MXqDF*hHW`mw+ZQJ*ak$8H! z)~;9@57hX5{n-~ja?W@f$*c1kMDeTgHYmBG+YLfNR<#N`EP2cAqsg5jR>G$pXU5-2 z+h;_AKgpMp6^J7x7B&|N9`?&#<6H!krD zBHGHwK1g(CUE%|-5pB@E)Nd@V=X`UXXaqb>R#t=8hGgyPK$~9&5SA7&19uy%UH+xd z)|}IxC!WEz+V?7bk~bfW0{_2#RtbqCde*_Pb$b`rwjr4JDb7^ z(_52zG~Y1B_S>)ep%38zOa=5j!Mof<_GnIqd~n_MmjW1X(@6(12lBaTyU>4?C5NTLbL7;$7;jX+t7yh@VG2Y2pEvv+~XS(tK;QVh(t?1g=}^D%)q)8Ix$1EOab_Ue|ST8sAa4U6_6 zu%vkBqU9}Q@PR;z;&B9K-5N$=rOVD;pjmwFw`8KdpCtc{;XVY*{<}+9ml;!_>_DIg zFc4#-Q2C(=RBcgkCCw$q)^l?GR|wovn@5pP#$5g25=GZ<;&at98;_`zR8VR9883R8 z=uI|ms)1;_Ho%YQaJTMXxVguBAR_>z%(r#1V?M3-L*pAXH`tT=-Z{NCW)caD-ZnNx z_Bm8J97H1?!Hz!JbDN3G9PM}?R=ICxF<_3X*)~f{FYZP@X~gmnOM~9SmUSH`zm^SI z=W%M${Bbmv$KY;UnK0L_zOwv>fWMMb@M`r z@4*YB+dgp_Mp(S|P2z4v|_ukm8`+nU~qW5R)->oen2=F^PYc|(2KVtp& z)y5;QZiB#XK~}N{-!g93)_Ovj&Q|pU#{T79n7YF`Oh(j2`-WpH0@C>;BW99&G z`-1W6->L8O!lybp{YpSy`+$NgDkSwg`a>iuC8FDz;hO&l`V;y`)%nw>|LEoAFI_>0gX%y3TZ1Q8B^m?b( zRh8L4)MR#WRy}0}-I#s_{N|f7Fitp!a_W22uvd6LQbCZJq@NE&&%VaVCYSldlwUWs z4)Gx`+E)D1&$bfrrX3Zz{qo=d*x{qhL42cig7q_fh=dAjND!t*?r&hw3yP0+`_@4h z5BpI&tQ01DQ>l@!>6YMN*})MwW~+kJSQ$)t{z~ zDUOY-bAQ_2f?DRgG|bi|3iw5p2(_nXT#v9ouUGC%kj3N1_hvIpMDOIHNfl!A4NEH_ zrX5zP>fzMKRIj0C+&c4lA~qkMmCZ8l7CSds#Ii1zZ)xRI=y&)3yzKKTrgK?wJTGF( z;-Xg)*xmJ)r6~j-Sv-Gc7@F(`X83@(yjb)vB?FEYUY&imTkOFGh@mca?J^xWgX1=L z1sYhX31DFR{q=NNF-^~EW`h=ulTa}FbBKYwUPn$4XK-`>0@q5j2NscgGKZ#5E5+xL zvN%EalQsv@cX$MCcnc#JFz9_)RM%8 z$+Ir%H7AM=-ZTyjJ8b(@-QGpQrQ*gEKeu@MCmj_y>aiOYd)gcP{Y$onDb1IBWzRE? z&7>~v@|KGShni8Pb$4sVO*yqJNtGOuyb#+*9P@D3KOMDKr@L;ZxO;uU(C`VYZl~}< zZl7t!yASa!&)89b=v~bBp8e<{*9=(e!3x6G9IPQ3Ax}^kTE;;d`>svz%L~)u09cR9 zrCnI{O;$g{K=wp1K-0Iruo_ld+jSgb^{x8Y?9!X=NXM}sJ#cgLtsfcV7HWYyK}U5S z$-F2!4k1K27wufqm+jA?O@pPIXxB_mZbmU!>`Tl2W_sfj@z5OFb^J3ha(z8H>}~&Y z=519_sOJqmlE4r+eiLaE=~jVWmKa>dK6%*!1cj3&e(}tbw4ktFg>*(!+fs8-XWx_P z6hlY!AJIs5Gz}-9$Qtj6b0c zAmfAs*JFYb>L$jozHk}P@UCilu6rUXKhJ0vaA{+Jr8LP%Nwqm+6{Hl<@ooFj zhU`+vl`j~Lh~2t=(R&2-2&%2Ep7>>j)@p*`;M zR)K7DSHE82Mj|cKtL{=~ab`C^n?&}7c^1yuy?%Ca#!{~>NfKvj!TeVYe?IyZo(~Oz zCk}m#)Udw}iixyW!r&=&0)v-3T79aS#SSxXIyS+3L|P@BIw`=wNnNutSmy5DuTfF) zoP-gGliybQd3oYypPnn2A^b}441?F-b0Jee;~umM{4DA-vLLN}n>7XQQS#~$4{&Cb z1K%fqPa^JrZo8a8CzHH>(}p(4xZj)B=_yOJDB zVHG}Gv#1InZSsCN__;H^2C40O(`GeD&xE1x`fMGv`oeH`U1dXR2RQ;+kd0qITIV}^ zxQlF`ynbnS;|mJ%SZyZc0$OFAJiU!9(JM6(DjvwaCz8pPp46qJ24WIsd-l|aAloK& z<%VHbk23uc3ZnU&AyV<|LP$`3KW_wE<52?>#R^8h+_ncAZ@XW~K_dw5iE0Aid~(qn z(setcZ9T zdPWBuE;G9jSlZWRf;M!<8=}#%ShDr^8yFt3+izCrD>IbY!~`akTERHDv&SK1)&B&= zjvf)`xdDMuZRj4!+ryakdYHg%C>ap{ARCyYL@a4M1#f^ww{IA^Qf<#!7ZZM=NLW@< z^VcU(KCaa_gLH(`xwQz8%(4x@N_fazn(!o*tV_w8b7&!;V8o3q)MRxhA7A?uyjP8K zv%t(4)R=YAoyh}w7C6VaLCw*>I(YcDktp`U$sKO(sn1klM4Gau+=6UD$CUGd3~>&b z1%x@@61caO$7xdCDLjD!o{g-lE1e{zW+b`tjVhH8t~)^UNrhQ$o}b-ZuiJrlIf2dL zHTSJ7Fhkt?Cd`|gCRVIK7CJNPy*uyif~3QSr2Fkw{~2vn%pYUe)CE>(mv?3N_VO0C zNWXOr#&}B&NKNQwyeET1dUqXVnv0PZ{gR29i3rn>2|hJ@KD~+X6sR~G0bF#g^9s$_ zR5>YOaT59vu`YiiEI1KzX&1-WG1%kNCxoHv(zrF38qJ_2wmz02t?sYAF*R*I?pj3q zA~#o!PJ}z0XW_uvx@o0ci@}BVsAnOEK|jpTh~sQx_v5oRzfz5_(JSH z7!<2iG>yP3>h5q(9}F%0Fu`cK6|BqX%yO;1ir`t6@`&I(RMx6742%RpABh=~4zevs z=9bUgl=4)07mixz56$t5CzmwC0|PB6)Kv1uJFO}M{&a83y>%DV`%N~jq7~-vSgmoO*-Zv^P@k-j#BjN_Cf^B8M6Gr zCouZY0=|}e=<2Q4N$^@r8gVIcV{^~$DH1wK3fme{9C&}#X$401_0DGBHmya&U>5u< zpV6i*@v^Y6YA~kDxm)@#dav@*gbu``uJRBAxFMGk_C^2}`O6SF4$B&knV>$nWpRUT zVnSt6cv)78cGS|@pMcSwv_Y(F=J z7r8MW4TCnN0;y6|MBJMyvQSzgZ3Nb5W4~B@nW9kMCIr`OBWv#WOfWZW*^N~JJWY{0 zT#`jj)3QTZi@q=qB|SsmlwD0hN?aM^%)}GocvF@W0`1Pr zA!P1|T5i$i#XPyox1etw z0E055J?~YQ#bJlD)sHq_vd9u(gw@M?z!2J!7|}CN56?b0Q)!I9SnUXR$Wv1YTJ>og zHx2sLS_5}YRlA0jx~fm+CkFL#1Cy#BNqL#12r*^!)Tq7tB*8oGq$1Vpw2xD{8QSj& z9x$ldO5)ygam!`&x6TbuF;@;3{sUnVSw!P23Gh=J%%Pxk-bgLT*%e(3cyk&Z7iQ0T z_y`6J`VZL&rV^Kw#Ra*3#F?p26hX3S+e2A|-*o<0qEBc67}VBI(&-_k>bXS#Y`m%z zT&bX$O3cTfb{}IaxZ6+bJhCUR>rx<3F>Wf}LRD}3!@q7C$|`Y4+{s;7IIFLkSJ!!s3||lS0pZgw4S_a z<*^ar2)qj%7JfP?p^L z3_LAvh-Qh%B8x>!o{R3%^F4}EgH_@x=@Ub_HQ}35`4oGD`XM5G;9%53F0Q^JyX1w3 zmB4pfpugI^SI0}9_HU%Z)l{>~Z8WqtYEHE0Q!$~?*S-i8>aO|nGWh9z`i!Ow356dx zVKXjj=_S$}J5l8_j1-n{sf)I;pyq}JD;SlLX?ddzN~DMK(>x-$%2_L2j^+my^+ka^ zu2!}B6$cFf$*DWp{?*c->c_XR*G60A{o6ph$A=7h8&=7bTj{Z-wW?DHzlgQ)@7-cc zFfRhj{}V0gUwI8I)$H;Z4Rx)X69F6&xH(%c)|mtw6~O9T(J<5>ZpM(R(10wgs!4Me1g#50-gZz-RCg+o;F5m!)dxD|&BBd7q~+{tF7 zee;xo#!cm(u^=J$g6xzfts1`0!q}LD>g>^!MkS&A?Xy+G>ejp|RLp0-B@H(#!GBJ$ z^*?Pj@3abAcYEeSX%M48P~{RINThwyxzohZ-D9a!-5_nH48Y`uat#Y4TwQ%Ly)=t( zt5odc^G}45-fy0&d`R{Y?jCwkq~a#NC=_R56ltfCYLVhP_tD z5%mw_`okv5$d~TrmKwC~D0(KB6wRi)`|F&dTsJGDI9;!iDXVo)@^e*7T%N}2FWc1Y z46fJ&3rDMC&L+V-W}mR>XiLA<1PA`68h?X2$h5h>X9V~VeX?ZRFrw@DOUYS2+ZuVO_N<(Fj>EAU`fGgrQ&2Lj?t&XqC2&3%YW7MmcJLcGY zjgi{f@VA;~($*;)@HFRD#bnXsS3{sxMxpKK%?sxgnRDPcG(~>>kiE-uIm~44RC!fb z{b-iO)N!bD-c+oDv#7=P{>x!gJ)KO73L!Bg=ePTOr`ukQPETETzrM5SksRx)6w853VT_n>3^4!&*!knGvD zkx1ja+iNiw|7dMCduLj$Qeb;Pq>UxF;Ls_i6;&{$ydb8zQ?%*3Gwn|vVhxjLC|a7) z1tXYX8|>EF^~veNjzfd?R^K_BtQXDFz}<&t*{g4Vz679{{zi4pjn*DOFZQ`?Uud-V zbpS9TXi@4?WPTrG0-M_vn;>3=SDH6_O&<J&ncMACapGy?knpO#C(Y++mLxBU z%8}G51+rzHV>21Y=O(gyLiDmtx}|Pz`p?12POw_OF7 z`{F-}(QCco`ZQ85j?P6@{AdU|7mjp#Hn+pR%cOFsWM>urKhnw{hGed4{A~Xxt#Ms* z?f;dQ1)`?VK2|w#=&xkyrxay<$lo|~FnmfJi)UYN^3qKhO!82;ox+}H&~4`wf9kvo zMXj{p?jO^)TBMJa`AC4{Zk@xjsMm6}t;_|tZ}#MF45DdxNm%2NbAt9eio0B(8ZoL_o2`);E4Lkp13OsHyP0bDyk3OSKRbwRenxfQ!Dc2BP>m= zb#pWJSC@EepqzdM&%=mk_sm`*g=n3RQ-UZp&juwm(^gW4S4x^5aI-?aNP$y6tjgOn zJC)4M-tFr@7)xf<5ey1!G-W>Q?{z?7*%YQdx8E-vq#xlrCc|Hzls0nYsED>0HZ6rq zGH)Y`X8+uJ1&M*Xzb{71`)AS-)lzonu3P-X0Pg_Wv`)NHFd&d@`F3#8?%Uaz8~>U> zb^AN(TKR9TUL&6m`8mez{20xaj)36r$*=c*e&1SUyNEYrOp1H9{Y+QnQ0hUAU3j!4 zluOqY+QVH^_mTo+1s {@!fZC&((=7%;zBhX28(~8PGCDvB_5WNi*N!A0B`!Q|^ z4p;9VHG%2!GVT3jiyRT--s@UN=&Vx#(>HGakk@>9lo)RysL_}_{>{JKgM-RjR6#SG z<^ofrkn{t!iU+xCJt%CgU*_&jXm$>g7Dg~(CgoqNrD~G&L=80GD#Y3ryz)mVs9_pr zt(IF7o}Wab&=vx_ZDmwD6m5YfiIyS%*mVM?@KUPnowj*%f2@%duXVhjTs1QNqITctw z=CGd^y|ISBe6`K(ed3+}lJ!wGj6GbNq5sE3s@SU=pM2%XidtMP%~D-uJF%eOH_e6< z98B>d&w5jMS9&bZyaBfl2jl)j5^4go7aoGW?8W#OD81K-(`VjJu-Bg$-TbexWDB{& zMg6hL^?$rtRgiarKl9nkQPVOmS9m2}WP>mp;U?`fZbu=mozr96)i6g=D7JI6FO@^_ zZ@!sxCI5=^kqk(d6-p<|*Tyk_86tcON-BqW^b!eoAB1U`h#Vt5RqAE~+f^zWjQVM8 zQq_;o+7rHN*BS7Ui^%$u1yQc`;21j!IQ&Qok)XTV;(@6hggJYYg@7JUTt12AZ|ts4 zyJIsS<)U3kA}`eWbn?;kVd(%3`-ztGR;le1b;bo$U#>2@S|Nz|NQPFO(~xl&-y%-I z4Np^-YqFt8SGzD&S|^ucDn8NcJum7LrncwS#n+UumG8r0$g3B?8o!K& z{}ic_;IFekwhoT$TuUQ#M4A7s1iT`{cgzDj^d)s#kP)wx)A?2Wd$UHZVusK{%?rg@ zhdwMjm)0+3s1BGZ(~)wFl`Y81D*WpIE(QpO(-{XS@-AWOJ$`il&%kBfy#*CD$Bw@T z*GeA&#U#_`jS5xEL`8wVITd{0=Sjuk($VGQ4>>BFlWS|?`*}Dk4KX&EStWA$5+4-* zB=)x>uI6oUCJKr}2ie4nRTv8wWRM8pfL>40g0U**R=v zP^w>!{oquqX(G@xQ;*whc6R2`tZ9fmRYJZ}1Q)B?qn}vWLlJt&z-1^?WCo6f%QqOu zJ+ge^J%oPVZ=PID;1FNa6eZ-6vrGiGY*m&*TC61IaY&zbb)Pv>T3A9v?K^h^??wAG zZ_RX9!|;umq^LnzV^|JYemmXS3c2e`N2KMx!2rlN(9`{&d_{DAT?VnkMVDTkU#pI; z+pEk3-8_T*F%0ny@V{^e^zToQMzVx4uU!-JLNUW3r}PoW-E;H$k-Z-mXXfe9`(?im zHuXEzNC>~O%X&WV_U7j}e&;zY5`ImUc@;M7_VyB~tY4QD zB8ZxBx3n50q$BEOv8jLle($CAvU&R)24}i)OU(b;krTj)#IURRw^0->^(w|B<|{TM zDP)LuPTv)0nFgWoKBN|!Lh=EPaGn189UFx(hSK8SqbIN-EsYhWfy;eK68A_SoB@l?>!xg zqH98V_6fG5PC)Fg=Qu zY766mzvt0TT|=yrFn)hiRy)aN?V~3=K4fi>3`4 zdG&a}F=N}lHy6g~8^l42e)Y4iohvuo5&8>emCcC;o8_?}#WEkszn%tSV7ud_&X;5u zc6VpCj2odbp6L}aHHh&_*_*AVzU{bCx&6;0WW0_qmB*yjvJHu!0r?oaVE?H$ z|I-o=sy7ka`EoDQ1)5oYknrerU?*YX~A?hQIrHW zMp2nj2WO7kRxbz6Iv>+(6Df2$EmM7PbcGIYqfFGIs&f2H4rtJjtn0p_A02DIlCozn z#oJzFDe>(+SknEy^%clg{v>s>kYQxp?H*@V*~-es3=o?=GjlnFJK;~@T(suUrSHN} z;XaxMrwSLU6)@fYyOm(j}Q)MVix%;&|Q0#@MQ)oRNGn~jX} z!WnNf0WwfCP_^0_A4oE!mrp5&QCIvy1k7x@4O^g7XvNTC%aR5TX=br~oLOxv%aWaO zSob_h$Av1D38;zI?A3(!qKt^7A~>UIO7O<6V!3tlLnoouV-7_zG1lsl^k`pHtd5V$YQy&Ie8`2&Eb(+;U9hF>SD((#VYKemJ3VTa~0Hy z@uGNW|0^M@A(_zZDAe-$3;SeW5WAKOECr%MI3rXo<#a?Kh|4BNSmgRT5l>TYM2cTl zIi-awRW)3MA7?s*=7L1RbB`klk{J|_$L2Q-a6|~&NpsHuq|^D~v+!^GQr_Co7(Di0 z-z*Z#=49l1L%VMZm*E)$Y0m4UfL01@VZ3B2^#6m(p?7`^JkBiD&%21jMB)mW4vN1K z&4?7pH@4NKZqYS+&OiR?`#>VPk?AhFpIm`Y{go_i!g;LDPHWrJVU3 z)FDS98tn79?-;jc_-K6hny2cWu4-mh{26~26O6X<%FZO03s~B3uL<8H#U~Kl97Y+z zRAwl{TIbc?h8bF5YRLcf9o>vcULFPUJ@ZaG*s3jUl@V{$+%4K{)P}}-A+v#Wc!bU1 zrRmc6D)Z+lJzz*H5@G9eSd=Lm*5+6euYD0IS-%!ZBdTl>90+u3+dc-C2=&|7Z|`*B z>+S4uE2nF=eX-_LryQGQhq2`0hUSEG&nb8KyxzUJDpLE&&`^=nXsGAl zb75wW2uhzW$w&pY0WSoaXC3COmEKdT^)Yt9!$0Mk%=7a7|KaMr-FwqFo*&M2&L6PP zHP7tq%)K*v&q%Dq0jZ4W-keV%PZ#0J?)#zZi-mW#BV3)uSH0Z!}uTQu%R5z(;P%b3dCe0GiR zw2d0RRpY%zRv+o}tT-;pU%eW=`am{#YtIKx5<-91>v!+kWtt2$x3^j)Y1<}IRPS6? z2uAEKZZ}#8T)5=-DxCl+(A_&msRN$Bguq!m=~>}HQ`-Ajq%l0G&1hI=QnA7|_un~G&< zsR!R?uxk_V9hJ;{5qTAwR%x|A*)zFjVx(rnhrSTLWjkzoZbw+U*xUYBwL{T%=~H9! z-oeK@)3)lt#1!@xS4=+lYtl?VPG?Y#I%K5D;G1A1jn8oCa6`lCYTB1)ZErm>QcgnX zbDzOR@2Bv;MjU>r?d4X*j3zD1-`EYcvV{r&Tja)sC{G(MR7nS8h86DAwwVbybC1-3DBf%vg%H+eacM3oP!#?O$Ckyn3pb zDan2YRljOC;XN|Mf?Y_SqDNM>xj6Wgt9cA zm#Oae6C2RJ_342h1xsRD=u%; zF;uzGT}f=9DvsuQA`z1GfY8+nctq23M}$>8X?f{i%6iFd8CqX%==d_2NF!v}eWcJT zJ!6uVT$~OWNu&{TYlSx7g$q23@8L#tD&*-?GRFN;yhQhh$hVySY>xVT;~Zjf4Eg8% zDETM?9>8Bk*z1<9rtaMtZtKzg?c2KN#7_>ooVnSvm=nLO<*>daH0hq+X4FFptd^W( zuEW@~5sgE}mC<(3c_Gl^;gmP|VRK!#{1yE?jVf`uqXnrquno2SQk@|%2{9ucz!3WwCJj@5agEj zm^m_(>mlgwy{K`LF`bCm!0xsuNzy-XzsYSvYQ~^1*RMIDwWgih!;a$)tuJ0{f}6x55TpIenSsMTl74%5lj7g4%=T99HGL+rYPmp|0kymGN{rH@-zi1h zkKZvYs5MvzlOD1se3@Gl#b3dUR|48}6`8c19%QDDRAzj&Q<4anu$gd7S8I)4>Ve}} zwWRc>)zO-?lV5j~inPw4mr*$Jzs{U$$@Wt29dB2YLvJsbq&~BXwCpyWPfXa~C`i4L z(vI|U3r_nyPg#@PIMe3{*G8x@!u}m5YzdD9kY8LvQTce=b$scjw+cINr@pRg(Pqk7 zGw4sE8i@2|H`MUvf-L@hc1N#eA!`j@A$^wd$=qQV7IWiY(@TW*&f(^im6LBC(!}EOR`6TDw zy@ax@LrtZteR_w1^$YsIyEzefyqpop+H-?uY5vU4VVO_5XaJSX2w|6%p;ut~6#g79 zz&6S#i^7?gs3mg{mFyQa=93QJ^UFHvt}MliO-<4L+OWUQ0pz>@9<)FUrIUO!o%cov zv-&w(*!1dosg!Rqrfm8$exjbZUrSaQLaxcSdbx4!$MFX0=FJ4PJH1b%9xrevfbj;p zn(v=%{R@}4vL%0AM7Vm`g%+vzAL*FDuE#c=xttY}kq9TO@UF0Vl;((8oUCf*Nj^fKZ*CI3!Sd~D8WEckA?DP2vr*6>WM6<1D9xwO?2n!(EFdeA%tB*=NC zJ>0nGg{~nb3MT_9W6Ry=5+GYgvg^O>Hyd{Did~#7NGPXqAY0^C8fsuh&^$(M=ZA$f zXQ>efb?7WrMi}4~((4opUOkx)bhq_CRV6W-A+U%!G@Z2jK2lfg@++QChZFCnx51@W zvZucl+Mo54o+v@rk~ehds)52(|K4*Vqo<>y*X1poG=o48gxN*uU9~;@-jL`-uf9Al zlzz+WR&N{R4g)*5-y6BqE&P9cGfHBV*z~4V(-*0T|*T$lpk(>1u{Ro*aciCCTHmHNOg9Fl*rgxQ>hR(bFO#z58+3w~RKo-p^0YbMGrXa#s;i zZ;0A>?(<~B( z((e)Nhi7DYM0(!O*uKQ>qgYOGl5C4yMMD(jJFV-GWVsG%fiR6$M*gj`X=9^+(WS=j z^8xXj=faZ9R0x8`rVJNyknAE>j>?MNOlFg1bt4Y_-#|JBP#DFTG5fzw5e)$L^^Gt%p;N?Lh#QDb)u<=R5>8{>C%&{2kt*?VN3@#tULRH4pGb zO}N4@f2*hsCFm;Yz5%dAmfbps%W4pn?frIzT~A>^Vf((SSu{97$hy8BH?rkSsT$b=FglWnDZ_+@QWS;p+WMW?OoSx2k3 zmlSyC0D{`a48#_eyw4Lmmz+Ms$EF9i?^om)W7LedJJy1oZ)a2UIF<#McC)MPpaQ@)`c`Fx_+nE7 zO_99uZ=yZTDOvASD4Ji32rK2^)dbL;Jo^niY1Hc)6|wZO<;^l($o#h6RC;aixYT0~ z(g%u*Sy8xZf;=N))aD!^A)Fa@1wal#6fT*2IIugv8qut{KaN9*cMNw-2HCrsqSJcZt?<-*zRP-kj9~MwH|6^qxpdU0qbN6UQK2`)x z|A^h%h_$BnF?@IwW^iU)0WEn&M!ga>W+A!i_;d3hWqfVh+5V^taD8>&>P>K>^QFJY zG<4FxNZGc%s``Rg1|g&3Fk|4`&4iFczG<_s;7_>4#Sw51+sQu;fPlhx%00*!)A?uH#pu zjz68>eS??9rEd)S1hg|CgUtoQ#f(_Y+P#Nd!P+(Tbhw0uu-LbtrEU1D;Q9IWY1Hes zzWL3d^#n}v&1AVqr-#rH1=tn*dZO1C07f7#DVX;T^jp==v{S8&7pg`}q_4 zG!~xQZ#Xx(Ikb!twS8q0B$STeA#mu*fbJgXo=KEFe5?@u9J+5VDJdVnnom5TkOTb= zky-NnM0ZJC4Lgw#LnQX-oM^P-R$=eW#FO7?f5te)yxP$s7M?@S7EDREN*~3z5@u@s zq#Xm7?Y3B47M*VUo|h8Gt7Kme7CbN5knkr-$LyGaN(dk`jL&UO z+C(wZoycxwL>qSz`n9|7SGBY2%m3!VT#5s4U`ltK)8VLWTV1SYS^SF-12zk21@%kD ze65;g{2|Zd$k~z$L(F7f0K~*kMBChMR5Xo``SrA{T{lttx z*7{B5n0ch>CS0?S(rvOwItK#R*C_EiTrENHKNM-X5GJlghW~ zi@608W|GNIYnbX!1wjTr@6r4E7f~EdRT4i`Y^WF$!26in;E1GnSfq1p^(JxBlg6)T z_iMX8e4G!Y`{B8RnCLH#MlCnd`Hrp$vJS`3tlp4CERZLMJB%dsFqZQnb&nMTKfl5L z7@_2`zE31AjCN%wcVTPE_=_BycH~=kVDT3*^i>oj+K#a^g|T$i zxA$CfSAdfVL)N0B6(-|B$#HQ$?yJ39!~fNgmYkAa+KCmlI<7S`G~pN}>ppvGXW_tt z!67g5jckT5^oK8xkoL!0coE;&Ol7zpe3tO@NH_8|@;|Nwbg^s{)RUk<;}*h26T-H>$4`vafe_4X<0jiuZj{D@==nVQ!|QptOmT86mwZQca2J zt0;}pf|cUln)e9_iL&oz&L^Gw`8fVXH^-U-v;$=epQlQoQeM6>+-nV4##f671xR^Q z?rW9?G|}g~__8~*U{Z}s_*vAyMz=frBi6^9eg*zlx>y;ikhSxmj~QBWD0MMNd_;BW z=ITH35}qxWj;X(p&J-&oYoK6REDh0K%vgyo45R7}9O==ww~DIynsKG6iX2hS+++NB z4B6GOB{9x22;lN5s-CDS_q3{jR!H*B=bUq$6fge1g|QR8CwIM{1Kt>}>%6hOC6smL zdzRZeecobPdHu%cb^tb-u!s_G6K-@PaLZ1jwsM|7j}*m8qL-}rCqOExfu5v@`acir zhff$f4+reIbf?wX-kV>98K`f!>N(*UbMW)jx1M-yrOzjh!5ZGTNXORC z{p*ppJmQd;#< z=o|Aq*UcDNRSBUvoB0MV?{v}c`o7hlH650Zv?G&jCDDFiGh%E?14&oPXjQMZE=6$4 zzB<*7T@AD_yK9MCQYIfRlFK8*_*$`&nty*~>0)K?o8hqJc|5L>&Wph)7vE^;Vns2s zt5$kpGMWB66(!@*EO?nCBG5VWXBmBE?{y2dHo#mHzv+PkvR3*RX62Rx4}E^M{@=~! zf1?Vf@wVQh^*+wEkJ0ie&QAxG`#M={0nyI0DHxLG;^RA+n0imZYLwyN15hx@^_^pK z>Al5`+e-;3I5TMzB)hneJBom1s`^iM8XP}gD*+S@5uayZmIB|T$=W`8Lr|mrTk_=j zkdFSf-7V1%C2CnD*7zr?!P2inP#Q+-BuwXD3g8jVR9QE4=mN&u(INxX z8a_`m2sQSpc^fjA$i$!9`q0nq3&T8{wU?U3%Il(pMKk=!ze}DF>#KCAl$e1e^$`CJ zVt%_6L~-VbqiA%Z+B|%}NX#Lg`#U|I7jA?w&D zE4_r(k|)U1O#T&4S|!Z7Xg%wj#WfbQmDl4z_Wo78qZZ=JqFMf2%@+YjQj!vN{{>V= zIB6gX_9#EIezr@UQT9di|K3~A)V_@zujH9=|FcQ}3LVTjG4)xT>YpCAAD|9{ z?DH=OxlWZF&KnGgrs-gkb53>;r&x#+T2!Q)>qvWFKu>zpYRK#YUrc<>iQFl+v=6>g z$}qDO-T>K8dR@N0HIImmn2?Q_52~3;7$pGWDT)TBd&}`DtH07J@ZF37@Gz$SiviSS zFJ7ahn?Q|~GH$&yQigl7f5RTt3WalZ9Dc#-9;tyfi?)?8v4c^s6)?g?OSdHLKJV8#;TpY1GHmVG&K!l<@g)ZPg-9d zDK@B7cd%mw17Y2c-EL=E8Aj3x=c*Z0tGB_fp45Al_;xz!J@2Mm^;K=n!{DNw#3kWc zq}X?goa3UW7KFg%sDFQ#1gQ=ezs54E`(^p%tN*OFriRJxS!aD$?vT!kp*1n-jjyjo2a(wwdpLGr=@_d4WAhrOIPPCc!o4HoY z5Sr`*SSxOv9bwng4@XNt>j^-$o%^$atlhV^7*qzzljxL`&)&6tObjLiX1faW|I16@ zuBqGCLDIoWhMWEM$QHSj9!6TU-28&z*z_X-`-H``9PMp{EQV;2=iad88)|-J!1Oz4 z%y;iMYRSI;pr^sO2aF1Y7UZ=^dvHUoHp_NVWx}!ce#7a(k84@;#Zth0=Il43BB_b^nWZv3{e z&v8&M{0HN%V9o`9^m~U;fjdagX+@(OR2z?y|8nTpy-;)i*$$0pfI_9;ug&EYg|guj z;PKHLV1um#!n1&y=dU|g4!w1z3>YROd)a}Ktw)lkv>yTJ;+_p+ygs*1ov>z`OUJ)+ z&CwvFp>_5{0!p;-P_yJ2C6bb3(bD7p9gB#EWqkWO)atwKp=t$?Fs&*KLWQ%?;9#RN41=%xog`PLzDC)@%uAd4la~CCnH9 zv3I$Oq>-JNY0fEH@MDXAX=Y2bkZ)1q_Drxs zR-w-SI+D0uMesy22-x8OKO|Ldr4zqO-?S8W4zsiJjkgg#N?X~FSndGpm z%aE~PPx0Edk$rM7Me*8{XAXxSzm~gA+7v|H@&*&;vh(7kG*Kx8*ivlm4cvvG1nr3o zkWxh+(LGBzG_qQhS^4Tky*0AddAm03QNq@pKo_OuaG`9tJ_lrffnbA5Q9T@kblAdd zheR_@vmUY`1OLk@nUB2nc)obN0o!==NiIaxd^R(+8mL`yk|UJ5O371jx8sr$KvuU5)GR5_?4DZoaz@ziMI&rSt5ZKowMO7fnMONRPB6nq|2W@4 z9UcF-$w&GRY59|JQeoejAjyWB=*s7=9v&m=ejd({$%~8l7h#WPze6q(AM1(FZJM=B zx#T#N7FTu9Y&X!nQj(BremPpntR8LXyyd!sFfZ2Hx4nITm_QjL(#`%qan$}_UBOXZ zzC$gx)l*r$ohk+WY2dHxnV!7y3^iT~B~D^J6**yD(MbtK(>kQ0i&co#!S=HqZU624 ze;0K`_K^*r_gwf9Sl#%|1NNmr%o_0J{z|=MS51C3{X{iT;IXJe@Lw%s0aq`NaOHV3 zaQ4!+Xx39cMm7rvVWG8FJR<7dP6oSS$@Pxw(3l<(BqcWYY7UY%+y1Yi81g;?l6k;sP@#Q6u+@1_`AqQk zqmkv&mFmy(&BfxsagCLRP9V!&H;dq^oO3VH=_qNpd3p1j0Uj>1LzPmA$?;Qh)un@S zl<)wfUa6Ee5d7b)S48PVe%v+91&5GHk?II#Q{+jeA^EIaF(SCCjYOw-?>ecg^$twq zfQVm{nsxhKlhb!Of47|uT#lC675r)@&KKH#QDsJ4svmGti$ePr)LDa75bf9Fr)MJI zDeK*xgRWa5CQfIcco&%I0>KJ5gJrZweNAM?pn+aLr72>v@m(tP zmU8}fmzAZj`M$pO4=h2#@1CIoUH|wjxxL;iQ8luHUeYgBeqE5SNV@#!co7my=KM?{ zZsgxZm2@&5YmJpPX145)Z_*i=j0hYdRpqSIMw;0myuMZOKTQN?ZZ*^~UHq(!mBv1} zJoU)Z21}rE@NjIZchrXEyd#{4MHVwsn|EbfRa1H;r2rP3!&)b}SNhe35sG{VKES0) z#cgYw&-^y1&(J7z``c}pl&X@|n055sf% zQ&G~{OPdMnZE|VBy8Kw#ciBl<7M+SQ?Trg#`!bqHIsvP;ublf$*t_4vYjO zMuQlO_$0rDZMw&Lj;#rb?Ku~ln_qvdw9oA}gRT`VRk4_?BQ;5oJyUr4gN!wlFa9bs9ThL^&ivnLbfulFXK&zl0{xfi$3 z|C9y|QPtLY{gqTnk41ZLiCHOi4G-;#ev z=h6(V4&a?n^5^m`s;+gm|M2Zf>*Unsd;u%@(^YzTt?5h@e;e{Rk6*9Noes>pv*dk^ zQLUJ*gPokopPc$owJMTC!uat@zk`?DGJIV~&{usu_`brfzIP{^T`1uu{@Zj+lh2M; zP!jmbufTE1F3F=gREXio09OBgX~4b?SNp!k?pH}<>vc9xt0p|CeKKk+KyTx~{3X(% z>9~YI960?U0zK#GEVUZY5;{O@PSm6t@DV5$}g-V+%*vQ z>#HTWJ9r58Qt}&qkudJ%$dHA}J?}p|KHWn}HgS2+K0AroJH`rI!Sj~-b4EE;HYklc zca}bEko(a2p|SBSqH%9!5ic0Ux~{K0>ea9RGpLxeA*ov=-TRAi|0M?ZUSCmXWCYjF zCbhw5i=En=ovrrrh7tc3oZ*2LgH>jV?K|&X36OVLyBuA@**h2;I20l=QOl9Nx0utW zN;%t#a@a^QW2tw3=aRd&|Iai0Fwi2%?L0nwdLwGuYuVT^qusB6%(~rt1_2dobP{e+ zmXzf@17>7{rgLe0c)*#(2@nD0(4n#A3_RtYjQAl}qZ-gMa7#Xo8;viM$@-BQCD{sSv5 z=C>Qt#s|31WOrY_i^^7xOwHjr+VP3-= z(35{VeK3aVw1ra=aWq@YD8z`>{MS5%sYAk}JYDQuYPzLFoG9(~|2@T4F|Qvq+OYS5 z*6`1BE_%sU6F3&Exvaj&(Y4x`zi6@ReU$!Kp_2Mlo)prjLoYI!_q-*v@9~{gad-FT zfTqBLdx&KF>&YW9NGIp@0&a1$qb6Trr3$>el*U$Zfed#l9sAh8Mk!h9{K%w%z zA|hK0$~mR}DU`T*_(9);pr|`GmZ2Sv2K)DVTmK-F=$v$0e%PK!e5BSf|8-}B^bY9x zIE>8oIT22)Qtom58%~3?IXRfe9Za~wCRbzj`1K?7`|EpmNbb6^;X8S9WHU|0*SS;Gd`1p;(!q$Bgv; z1FCc|CKr`)O(OOE9^^;vS%)xc>`-D%jjLziGgpunc$;%kwiOxa)yzWN zPj6C0Dh_6XnIqX?mL&EBMS2(s{7L$aE+4bt;Q(ytNP$(QZh?u4j4Otk^C|58d@yRK z=aXTrdi(fk!iwL)TeZ0C;09LNI6-%!+IlQi%}atJ*fXW>0W z*POH@#6hc7yew5dc&fxr0fLoufhXfvJ8T2l z#*-F!?J>bS;#?1&nbglgB&$p##2I!~TBMIH!46nir}OxYJmyk)&?t?$w#6C%FV*ig zgkU?(Uy1|39lh>EaJY&fU{bC^N05DyI6>7OF~HcDQa*G{N4Vr>U^`G~>e%=Eo^Xf( zw1`y5OsK}|OeGV*4YWVj@MQW_EJcN_hmu2qF>*$;h+ zmhp5ttzCWgSKCud{Amo?^U;c4{dKcn{9ilFq!c>H)c~$GBb|`;UwErs_T@$_P5Up- z7%g#gCjDFD{`SWbq)X0d2vqDpn7EZQ9si=ZMRTA2#;qGn>huqOMNf53HsKK`M>P8_ zX4aF=^(}nXMph+b!)aPR68`9XCvYX}E-{k&=;O%?Jf&@n2v-niqI6^H5fU8dv#7NL zR%qN_JEZ5*?cuptK4wYT3S7-9toe5zU3Mj=s3(ytZc#gUu^J-833OM!?Ut3L= z9KSrt@w)qMA#MNl#f%?q#cMzd&34ag7jyh9?b2^qJ8+y4<8IN4k->%F-v}a)wUyi^ zvf#0SP_oZnQ6x9cMTorjWCkm+ImP#J5aRLkTCR%&_oSWVW}z%^kCk5w zy;j(5w&?qK;hkwDnLu?o`6g3hwqZ;Pt|M74(chGLL4{ZFcLw`#U43w)@$B)%+#mV} z!`-tWzAb6=eNK4JV)&C3tJo0&4J5{b9xL`(ScfPQ&Qwl(2e_h9j zLLy@V`~C4R0)TRl0%Cp~pHcx}1@gVK>=Xx=Y-{$PGgk6OWiZma{xZ|4uIH=x4~Jhu zsZ9!PGUG+KvYV0p-ivX=Ou#zkQsv=HQ$fgV+#QJ12SCudkk~M6Rp;jW-FoWadtzWF z#W*U6*8zwqwBEe${Rlm}uIM-Zqxe_;7v!U};bh{o_r-0@DJ-e-h4xV*U69~o;N#Vl&Z7>>50@-!AUt1bhpV=;uc^aPjJCLK6lKh9EK!U7UefvumY z5tIzpn&8?|rL21%CKuprBWxx6YcCWdNZ(*I7B8-c1UqIlSc=+gp#DnLE88eR!O5hr zbBRZs;7;#`_#h(#e)$ho?~kpRKd$L-KH!s5YV+9VIl2CKAhpUv_2knLJay+wRudUg zov_6Q{|QZrf3f(YO=~|LqZndv_dv;rpQ9HLZdEVpKVN!cx=OuPY+{?Gl;(2K`=9#f zuBmPw>n6Nu+Ws10^i1+>n=0MVao1A%X!0vKNKmNQ(HR>T_(wWk zDE4K#(!}y7CX*(zUMm2d7M#fFae=T%^DzIUX=-pLi8UR0x0avMrG$r23)d%Px_M@o z7hcna%LN9_{R-ag4M;bUwO_>I0z-DJmc#bx>v5Uca$&;9Wb^h&>YKHC`@z=V?b5NL zqYZ04qxJ8)nfgn0^5J7*oRWi(%ZmLKJ$h&q zYrh*>KYb%*mfrGfX;oE=G99z?pVVxhW3p{*#{z*%E3J)gXa3`ekh}LYpak}=IUY#ymbj?^iRkGlB+upig&pD9)bAO)-0BxP zb>fH@1?uTjc@K^G%r6B&{+SwcP(fSS|D>#Y?yIY1tUYupfMj`o!^Y4}TNaHOE<@>*&4W7qewE`X*?(hC;8>O#19-}||YXo7W88wqgua-vb zJI?;g4MwsIooMa+@)52EFEn~_YO5joN5-&hlD31yd+H{@L#+~D$=|GwEMSs6cC(dP z1(-)tWv1cr0r%DUFA?nEk(~Hy@<*v(+U*AK)&>ftLjs$sK{_(fI?aH7`+77dlMwnx zxYmXwS5tAEyE)~`H6kBn8zw&XI1$#dOH@pn5+qe_)wQqFa z8cW+HpT^v=WuxCUkt|ERBI^H&HsNuES}W=Oc#96iz+eexc>PKBf-A6?=GEd0Xgm`P zEZ7(E@DB2#T+>I-bQZ_9$bD}(r`WG;SX4;bgfK8=&-IRBsDTCH)%P$( ztXE_1K$osMW5&_>qd3vZ2eXtWQ-L{0s;t^^9VtTZ_-_EVf9gx)(nsq-#$}td06m|O z5x~kyFMvS+VfVX}E%N7R6uoOCng2isF!u{a-jSur!V&bJeCD**+!Tu$0V(q`)kTkCa_FLd6frWczN9O~vnyr>&omz5_`t_qZq|A2hHC zSxF%7JPFk?owZaVi82o2{Hn&quMoT*+h&MJBQ=)R2Kq9qiuZ#ljb%QHbKDg=!q?Y= z;$9^Qdovw0jm(M1&sp|<`Y#lRT|eN}U^g7rRbsf7ZkIouKGjj11xp{wOfj`{yHOpueNnhzk3K z-onPG$K#>w4?jCor4dsn3EMFBf&1YjGco)^nppU^*Wl?&T>I4kS`d_@o)Roy*D7@{ zLH(99Yjyx0EvC?NZf#w6uKzU`37T=mZr%3EssgC&Y>YN#-%J^cTwzI03TUo7XkLll zJRMaGS2FU*gUn_M?UR|zu4aXE|B52d4!o8xN;jAQEefZfEEJ7q7n+C;k|qdO&lYq9 zxP4>m*nfnQ2OjC>9NRH{dkM=Hu;B6udd~!7LF9JS6C()L5Bei5`}OW(`|G|SSa(kC zx5f-|s9imi&vzJlwzEtfg&`=6<&P#yLFXbdr}~IH1eE}^R1|$UKePOzU%L-4LwOwI z*-9*>A^*c=*2*P`wCe6(NE?jXN|#ilElI$F)2DOtkB2X~?mc3=6Pe)3pV0kC5KT%E zi9p|fSg!DW_Cr*NC` zGd~yU_=V)tdzfNR1fdq7XtH1<{F)xY_8~d;%njFShlI0N(?)e-fRY0MM{rOxpcDoe zwIcQmh$SD(;3FfhC0nlF_(lhjqF8L*<`}IVyM$88U0)+ z5OGa4?yp{r0J&rzf>Sz@2Wi~&snNY#Ku1aQQy1B%fywc>?!$OR({w+hoWH|09bod` zHWx8eaBnAQGYlX!ppR7phN%vKzo)TC?88%v*;bR`Kxq6+*{*G>9Bn#YN;So@e_wsZ z`Npzu+X$aUEQ|>c)jJhQ*b&3SxO7-2p@jI>H|mVyI!v1Vaq0K4gA_u1KK@5(acUVnDuLuWAtG|A;(Lf_`<)P4U-Ws)`siQK0`bue!kqSkvR5ygD6#`M?H8 z%Xy}3(2{iJPY@}ZjSi1TAL1m}eG-o93DZxo>(!Nu{ zz59~3XJE_pCZl}j;YFUz#%JYqK*$Z+fOI1wHpRcRiJr@i*Q%7=lj8(NS)4ky2q5)3 z%Km3N^DyO|)ZOeKHC|D1IoQ}1uY`OSXI4O))-=-hg}0Z9k!G_m^AR;v^)|0NVU1h@ zuE3FJV}%s-tP46!CU0eMgePO(P0(2vXo&3*t?ZIIba;CR&jfTP3b3843XE^veQ|Fi z-thPdOl)pqIv&s_9jz&wnigy(9u70q5gJq<&Y2uCG+irFHMPg})(vs-sTt~KKGeKm z_F*andZ78O<$MY#BfX~^^GEb$>=iI&4vuy!nQ~zmA?fyXh^RJLUhyL&tTU) zrD)UQR`_bfQ0i6N4=C)ujPA%HbE9iDT5eOA9jQ0 zLw_}shixwvI4IM73N6`+%B4a^U5$B%M z2;PbQ_sQt+t&a6?)Q)aO7PA&Wm>Bp4*SZWJAZimPi1QB(iREuSQA6&}ayBa3D>Uj~ zP0T9WT^7VX9$DTetb>zj(oE~hG$XU97k;oDy)|s0piQI5t4)?h$9*Kl z^#?XvT2{6d$VBeyO04&tr$oF0il)H)>rB#W1o}ze_FMOZ8kh^b^j@DHxyCVf++92r zu5nz&&Wk6g=qakZ8b@k@&Pds1X+B1&CEh(B0JYwINLx_E*P~Tp^XH+y(j%q1y{LD@ zoK#cTPV##siSobhD8Mcsd1W;#{nK3O7I=CR>Ltmkc52<=oE<&wV^}8yGdqpHzF@{We(G<|lV&=h+9cupV3lkFin-(d`F6omSbr>=jv@MrdzAk!6_TX zB&mKb)MC|Mc+-rE7IG~O555wX1ng6w2K0K zg0=j!gr-FKL@E}A#oPVUV5^AHDp-{-IMl%_v|s47>pbs>15!S--_BfPDz*T?2B7}m`1WS0dgLPO4vDm*4!_7LBszI}I$lD7u0 z(OKVbHrYx92d$|;^=Cb5BefiYw*g^%8MV$Se=@8v!t^@!sGud6%<3IBuYIP7^rg zNO?gUKi(YqUY6}xs%8?l_LdWa_iJDtQjS+$3atF1Bjr-mt^e6Hx9M4jR*FSjVVdxD zJl%`CZ%>i37yk7rXA}*QtIw&Id6G?g>7)&Cp=xC`*3gyBM@4H13aRYsJ+D9w$N46| zqnalN>jLASJve9QtDYrAJP3`+qQnm1r?@N;Lj>bcqNn7s%#zDJ5}p^8_97q((=!C6?#Z$>4@ctTt1;m6O+^3SY*@ zdDF0`$*@}Z5Mc{FUMQ-y&OSx+I+wYmfDDXU*prG=@ctg$hog8b&Nld%f+CpBY2N8> ztJ@fn#u8t*%auN!o0#2^#>?m%rF>DR4RVmwEXlS`L|Qdfvi0p**g*FDK*fN_+!(Q3 znV7eF$tfp4YTW+bBWSeg5m^f9!UKkaCNIwClqwIJXn*oejf%*fWoW4J4?Th9WBB4Z zsd`&B6zvsaJ(Tu+Fex|kbWRg};Jnw2EO{#Zq z3r5};|9JH`8->XLdGHlu&3|uY2~DE!uOBS8t7@pIyFugY9^=)*68kRa{Ymgty2^VC3GF6uM-aLItbO z?@#_*Qb$G#0zT%Hyo`%bFKd}Rlu6mB`eVX*0^xWvT<|wu}>A|Iw?Hj(JLw#5Y9@q6P=kR8^=2GuGy8Dz2 z9-s|5yFs|#yi?SH?)>EcyK-)Nj+yD29zRhOyvSMA@r zc9ipKsDqpa;#MZ#8e1|Fk|Ergvq{;roGEXmQaMny-<|a$W46&X-uTSHzud*k!_8g0 zu_e)+^AEW6@}G<5^JP<5DbN~ez^MH|-#y+$jL#_X(uzxz8@eZOC`G**uB!AENvb*| zz5j4Zy^*WA_6>`5wC;z$r+qja!m-*&=f|?M06pudrSU)q#nY`B9t2IXe6 zmzAm|)6xGE8o7*MI9S7B)aS5qtK=!drPx&K*s}pozY1#qMbM z6gksFz12iLpP@y=j}CTf^fvm%G~_k`4mUeXSCWr zoWWb~crxYH=PAeFdF%avo6iRloHpl-(A=9#+l5q&vWaIvSI+eg;(CN&1!Z zVt=p>>x_Nl!%0Xc(!AhRgFs*C(>>apOr2Z?#Qz4ZBsO)qd7%`72Qu!@T>iXPojgzwM;fglSz!T_7|7xwrxdNH6zn_apg6MgDR-}7==U2 zoRvB@Rz*W=4q_A!m4^%IN?_#)R8m~mXpM*R(^aj`;M8Zc~Fnj(B zFh8$h2%y@XJXpQ@sFu;WqO-UQa~?|7C3@BW`O@wD4lNt77QT%KeK0dnB9)gn;Hd5Q z`ijarRda11`;hs2oS}NlFvp4&;NG;n3r|hr&g;^uM3o*ZpFT5a`lV=$Wlx{cAAIWy zrJf*8)PzK~37P}FR+}YhJ~1%MZq*Q-1T__VhZHfiLa6!Vd7t;3^E<_T4>7g3XNPf>jSGB3+6>})k8z~r=^MsoW^&ssH;><$e^F7qz#C^t({k8b1fH^uhXR}f_UeBwmZN((ZCfDa+ zvgDrpqSgU>tn;lI!&(Yqi;Kd|NTt&HL_i_soBgz9( zG#|7W4@h+vr^Rqo_?v7x>M^_5&l9(W;2qr6P>!?7;V(%ES8<#4TD?-aq-z>smtbvr zDD0(g_@M|P+uTwW6iZo}WcTk5UC3N8~KLF4NOt8j-r4A1hyCG^Cpw7dGUeg7FoyU|48fX5S34e zbl$tAIBT+&4glhBH(hJL>ii#Mo=%%|07fNW^EOfVZj1A}-e+%E0aSFJQgV6JF)+cm zz3;uzR`Rd6N}?n?!p}s|A$RjHv4iLeGwAO9?juu?sl|h0T+SlZdu6z5a4dIt>~OO# z#G}RH2wt+mO4J%N;ym9*9qm{~$Rl#2%=RA^T%HxQ_tWJ9G#!$p9-a<$XXk`7#!ubuo)n+b!jUg=YUj>Q z)r;pdDji292Zd?%9GCM@52SsgPWle$uzzp(vE?ows(Z436EpfzjnTBDILw!YM+_uJ z9QCJ2m^M+Pza%5NxzCPt54b)@&2t{JySanp?P{N+>(Q&r@r>oe`RXR4Ga5Fvbg)=K z+=1H`pCR*J_IX5|IURG|#N@o5{A&NBPwoUa6G%&7qI1(VZ+*xPtoRp|X<0eDwLP;^ z7C^|Ktb^E9DOoayYB-3~{eBjI>;mzU;*ZsBOJDKNIw(q&S(C-9Q^>?iXbEbF;gD0r z?>pgq!N4=t12P8SIktj$ZQS<1>h1*zlugbo!X>_rO0)C`&csaX=IkzFu6AQpW^9L~D(wCjUs!g0WS^W8TYU_L!_L)nH$-|YpSX#Vld7*I| z>j^@~oWmE^+diVTj4|8tBI?u(5=?H}Omv0D?|CeR9)1rs{O7dKubADjqrq;Z&#yU4 zUS|x&=Gmjf+2EIZ??5|{-7k-4eVV@&^(0Mg8RD2mK4V&Gk*qH_iD%eHAkGX+><_1KlrOJ{`2Y|{2sNar8=Xlo*)3tJ3?n_ z#JlB7<)`k0sqT-!rX|{%?1n4K9Tb*9+NC)6skKl~L|UAK^8Vr0IkDmAr|f{lhRvS{ z*UT^GS0WW;znAuAN+99UwSt}Z%4-xQloSzts0%^9FS4wvyg|F?O)lpHv1Sb8NP*WD zcK~hYQ=zKhqQKjsYiYaUa}%r8I+Hf-^$sl3mG~bC5}~6`qiZ^Z9?%-j&>)c{?>FDXUaD+myk&VJsGBC~q3qs)#v! zy_LQ63$Gma!Um^F2p+9?zQ)Aqzp2`lm={V>sUk^yzxxxR_-?H8pPY=X#a(H1*OrLo zQaI3ej>;!^ey{jQL~F0Ngkw-D7sXbXN`mN5HiqApj;7GuxBeMWYO&*LIu*0Cc!YVX zTxb1Nr?Mn9V2y-?Bnkx@(ES%K{ek4)wDL-X8kZK*P0@aXZ)>E7dy3v@Hc8o~LX}CH zsQI!lQ|M$`@UfycsyWw?-9n5yE6{^2=*GKFq{WV1(0ImQD(?Kqpp20Umrph+)vPd3 z)OX$@Jr7EoZ<2ga+u=XLN3IVSiIel`!aU#>&sSs8vf!`CeT4U z_f_!J*Oz|}@=tsX;{L+arx7g&`U%*KZ1Fu?&o<;>Rf1gpe5l^p(Grf(z`~E5#^~3B zYQdWOtD*5zos3)At#oyfnt~RXmsPM1tI0MlsC^};MI|k~GG}|b>rh!8em@5$=1v6- z_soH?&XZ`t=Q2lrahEe|Ei+?Zs>U|UcN{FtFilKe$wFmMYdBh+dgQFn8XE|(3A2{S z6hu>m%H>*%h}HM_A6!&^a9l$BbB5CAMC+OFEw`T&`Ly*VA0zTVE%Y@ijP{1Y8GPLHB*l7X8 z^8T>(x6niv)!F9z@X00?&>lq@<-Q)7_}I3c|LCyn-7{)srRuZ#k3mnFeNn?s&fC>3 z$1;ja4H-8M0|eYJR<~Lk*8PnNkR^avGFttTXKjz-4_<584x%$QH#3;;wr-SxMP%5Dq}u_n2n9pFuh!7* zP*|3on@N(Mk2GNE*5iA?>f0T4tqBfg^aP`$4|nP?dYAD%QKec6X^{+3T-lo4sn1f4 zS;nDr4xi4ntuS|E*zC&%(Yv2a%d&oW^KJ};wfUazX{jsPA5DH~=dV;#81xfW;x3`I_x~Ux!WAS*Vy?DbvT{J%h~Ru<*H~`LI8#LWo5&F!~b9H zv@MSE*mKKXhA7XHsPz^f%+4%hqPV(~+qR5{_@Cnlj53aimMaAZyM zmFZilMuiElDpGB#5{wUAW`&k}5&8q8z<*1Y|C7!2N_sZwJJm#=WE2X}92c2h>u`0{ z89&!}`fkD}Fo!1`a$tE^Zw%)?+orUywP~$RzcQWP{+ngcZBT5|=2Sguf45;cYK16C zWi&kw&X6hnxKH&C*w@g(t-K0I+LtH?Y*bjp(<}z0js$+}zTPZ3Y9vIz21O5O$M>F{ zKAm^N6UBpx$1g|uS@;iJs1un-mxvGA3-olTH`-fDj&;9fIFbKN(g^qVF%uVHUhEs+ zm?fMcu+Qr7<@yd|V84_wv&EgI8AeF?8wPWHz6nfc7eB zn8RV*ffn>C8>Vjg{XOSTPT$!@UzTV9V0X_h*B5i={r zKB1%yDb;C2L?{~oTs>rgdVyN%K2X`chC}7F@KO1`)Zf_QSNb~IvcFAv4H$fiT)zch zf6|*d-(+|B*%2O^^Ef15dSmn|qu(~gVL88T4riTCTqL#d!^oJWk$itxL)uz?7F_VP z30BAHb#?P1_JM-SEe=)S zZocV#dYR4#Jwi0N=Ps>dbD6FY(-EF!=X8CGe6@?I2{vaU?W^lC#A2qNOM4P{WBc{z zSe?!V^B;DhvCAHSC&fjGI%zk?yU*P@GY5k|9P%12wtBG%v1w&Yn(kZw6c-uVKblDw zfvx%kXMTx|@^4x;Fz<6komsy^=58K9A+0^vP*)wi|^GKluc51 z6G9B_y2HjS3=yaNt<^#5B@8J zRrWV5k6I)v1+VVXrjN--1lmL3fl52mk=tHtu}W!wuG&%O_pT+mR z%DDBk_`-pgZ-31rqUQ>{Ltm8bz~lovX~B?KFzOD4zVYX3j)rfNrgPjy2Ty+K4WeF( zh&j)uWm99d?V~mpT_JlHa-Yk^f{sA4xaoeZV|>g**vzPMUaBLoy9`| z0bZ34*nF}AOj)1N>U$qBrhYuxvgEg_K&>iOj62tLYyPCVkMmP;Kvtg#(tHJ}W6l$X z6Xmo$J&W~p?*?b5YwWGvo?J80l6+)Vv~XjjOnGz;C0%U}GVkzg=IgOYG(EsfS$V~u zBiIA>HA?i8`n!WIe{m;G)&rgCMp6bIRg0JR|PJw5ZT6GiuvdS@I!Vgq#599cI;%H{h9to2Q#LTF2?;_7gNL=)$-Mj`QwLYH%u`nu#S{V-+|9*#J z8WpmA$#spr0kJrt1>hiD^O$V@_}gwPkLQtf8Jcx>t+#+b;Ba2J>~!w$|C4;SatNb}kCfAtO&s;)>_wcT2`+>gy$->p8h*ad)s#xrs8T(@{u zD0FlJvOV`#FZ{!)=$A`A`*rc2b}!^)0yV0;2{D_A87-LZ!^4H6{f*u!)a`6`S5du_ zlibdqyINyVWmzEBwp2v_((@a;<$31wk?)u7BZqIwgG%p0Lk+18b9XKJ( zAoDyiKINR*&o849BUL|w;bN};SZ#aajJqrL(ow6plcXk1$?7dyoIL8V#v{DBsN2}E zpT4E#J!TZZP?XAtxu;i%Ni4}Q#%HdeR;^+2&5EZQz2Y{G4jy%%MgiM`UVO0n>LlL^E3FGv;j$3Rxuoi zEd_#!crV1gm(37PM)ZzXi@dbsQakslhkbiC;9^PPz{jB9EB)okR!-Ly!|91NN36z} zfiXvXcJ;0l(v52`rXZOwp$THn?b$JMM9d~W$7~Hd-6qb0fd{SK(z@o&ImRYU60GrG z^%DudtzT=N5Q_Xem>FApMw~i0^E_3?fVhaG`_(X6Xhzl`bAHGGBDouOAn|R$kea;H2Q$&D7XrhvGJAE(@Ri9 z1!yXr!}q#b4Vqa?)SQBbh1wHwrhs7_+dw|l#x*o~+@g7C_@rM6emUvO6X@$rzbS(T zv~F&WC#zfzAw9x^%Qu$=`H)X%@#VP655Wr0+C+=1ow$HUC#mB;NdI*f*5oQ9K2^S@ zE_DGuKxs5qTx#ngU5&da;OC=SH>@f}>@#_=a%_GcoydS3(7hZG_rGmm#OsOqh`uO=Z1bi5 zOzUGp>jHRKtVCYbE=^8ln{@k*Y0A~*a{4>9r~rNjS%mkxH{Zl^fTL7@SBgs@rLh?^ z0Rc^27k0*3_dfWI4%D?&`flgOIy}_vF#q_$DMSgyynZ_N4C$0}=40+?+ypsxRO>ZQ z{W-S~GrlQ@z36a3WKmkdTKdjKX|93%`H2)20Kda*QG zVB#)HZd7>7_q&+}R;Tk%paICN6C8c3r7%sG%kPz6mnQ~ioK)WfPHDPNSg<5v?(zXg zQIN;t07!6lY_m;dP2oI40>R%uC`6lFmV79sNIzBlUAp_%Nw5L^55npmG@7CTe;VF1 zWZTd-@5rRqp5gYZaiIkw2S=@Cjm~57YCps-aU!PqxR@{w-X4YJxOk^TQa=D@HZ|kN zz2_$skj#@LLr$IZ9m-Gxv#GHKd1{kaIfs5C?0SxCN|~zdtTKi~^%0L+ttCkP=y}b{ zzEqo<_N_IMtCS^y-sLiR01g?N&=Oo&$bu}5ep$(3zyMD)DW6itosu7}w0{(?wO@Jy zV$sv2O9L!IavB&W?iJ?m2XxD#$WeX(c2LfagdVC*v-%}Z7IBZRQi5Cc$j%|_3R7Gn zhY0$gUY8lCtY5ntoEe-~%!n(VhJ6ZqV~8yqQ3mZT_L&8sEpx%B;73G-?$%S9MHdH+ zGF${$Uv9=?wh9s2MWOn!lVf^A;6~qf9dNE{F1lUOwcmA>_)5#!i6t}|!lYTw*R2`1 z2;0kBE3~7lcD*_8#Ht?y<4V8Ioe)t{&2aj1|JRP#?wyLExS%^p0ZDClbPa&<3@e3a zkL)#g=M)B_E>8bCTW@j^HN1MwTScWb<{0Vy%M>X;vVg2z_Eru@uG;+dH0`5__!}0i zyttu?yH<7{6J2@;eLeNUIGj129Xx6)xOo3eWnB!jzdtWy{Dw8@nL;ecEseiupH2v* zNIo!YaSUgZJ)@KQM+}quA@y2WYGIwBx_&OaR>iFxKisj`N^Yi94~1ujuy0OIGH=JD zD(%T_l^ObCmzSPMNBZ5*$<PB8vM2?|meCIloo zh`Yb8#*3IWBE|CWr=COS{`h)7|I`DN=14!`LV;6J>;sx0#{(DY|y|R z2#j`bS^rjb`0@rmPt#OfHLyQXa5O4NLF>q%vfAPREed#9v2O$7eEq0>NNH?2&Zhj3 zl^i$`Ft5=7V%5OSz6(i@;Z`>?urYqWwoV9Zo-KMJ=NK9{2XJDmHi_}kj} z4b}q$1>mg_%)3-lp1~%}v%yh!y#QgA=Z^0J-(3?Ghku`Qok$5*yC_z>8SAMndO<8l zIsN}#K?&@$iu4MV!|9($+Ow!DuOQch(_4!Q^^<6;$UOB(dq5yCoS~=7?%C4R_Rte) z9mf^ov6uQT0X=$6CeQm7jlX%3PJmL^YNQ*(nv;!FaA@Ele@+^Z?+zFE6A?XBIe;95x`KJvmneq2)u$A9u#-td65m9p`mecFN07 zfaJxcirv4LJNt)dKkr+4LU4=ev3fVCve0!cA%cY9AZ> zc61%vF(Wow9QRRnebBT)$C%M66;>|S!{&J&p8O}A2R{=50ZILQVPMtMv2ETp@ z-uk{x$bb5_>6dZa<0;YtJ8Ws9NOFQc#lPs^YJ9DIAXyE1YNX*>%YW2(;Fb$4suDD) zm2Q$%XEBfV!KK3UBaj}c%7igXXTTsWuPb71+)j5gb4lH;FtA;^dquIk;By7~NA$Mo zu# zd;ik&QLtp+Dm=l?5)G3#s6d~U<#>W-esm+EI|x0Dv8fVBN_HQwv0 zcIR2hZ0X3seL-F<^da{X0`w^99F;uz0Tn_R*9$vxZv#-LUoWNGSsvhjC49rI*{tEEbNQCEFZ6E(10NPgc4Xw+b521Kh%s z$1_?+XRi1JyUJKkf;-1UmJmAbSWMZ#(g=^I3*ODaiPnJ7NJ*n4tQMH*+g zsmWB4@@1^Wl;6xpxJD~69BOg@g2a4#@KPriK~X}#H3@gCp*fe(<_=1ORQcDF;NV=+ z+Uh(cFsJ%n()=qekK#9soXmO6#_*m?FcXlUyA?L)PbJUsp9Z!`k@E<3lq)ZIjU#Rz zR2yHp+5Z|zegvi3d5g{QFXA2khe(4*#_i7sQ&*7{l7qF{XfXSIOKjZ7E{^Q~Q03iXI1gRn* z79fI13!?s{L{TJ&N)V+c5ReiQYI;u~WhOJ3OmDCJzn|~g>)dniyRXd*GV>?7#J&mWqeTZ7OwGn8AB=@ z(M%o&#AQ4sm}5eMiqb)qBP%c?O(}V_O9~(XZ_DdIG8Gho)26KuS1)jV9jkPzGV-$K zQC0z>aiFj#RO(U^C(FS_mLhNnnZ~?^^(!9~mUR-ok^@7EM-|gP4X5B&gu7$QjZGD(xD&U#b2Obw(!yfjQ9h z|Bx4~YQK%lG}q7&-XWolo_w+vsU0|(rZ%;`YEnjdk%oYj7#18>gP|}c3aGU*La0Qz{oF65jAf$A-DNU6_ zGR6eIHiq$)2NV|)onSuT5{y(+=IC@`G-g@Kmz0oy0}a5$A)T-eKLL%!6*{njm9BOS z(Fw8)9#97dU16^fSV~ys#?0z(N(*Mb!cEB;hpeO%o^V_(m{#5-Gax-_Lssz4GUxSQ8}tnIlQr~KU(Am9xzm>?Qdsi zpz1Gw{sW%)p=E|uZ2U2+(5|S#VkYGsYr_kLn+DfF)XPa2I~<|G}g7 z9bLx5$|#|NnI>)&RWW%5$1N@fD{p57LdA1b(}0sVpd|O0>&P5` z_k;p27HA^^Q$E>=&gJCQNo<*>qe1NgU0e3AFgRH(2?fUfgz;bHXZxGAbZ%FjD)4c$ z!mvG8!MLiMDhF}kH|-BkAvS!KfE9e(A$i2Sq!JJ+o^f^2R;fax?ot2@K4=1vasn_# z0!2rw8J7W6K530aIU&sIr1=F;8{fcx;1}33ym**D>@af0Aq8awpixqrwB{2VEb#g3 z(7Du>cQrtA2#G;!8MXi72$O zc~*R{o-2-Ns$OW2IuJL!NU*x9v1wec6rDthY}vl0?ASgcl!~Wz^It`F=dJC3ANEiC zDHe%4ouvae7fWFG76@8H0Z+pSH}#1AL~cMjjFv51!r^slqcVg*L)2=2VDVApG%*2q zhF+YI6$Ys|QGv`TZqoo(p1O3j2G|O!8*Ckv0M)nwY4U~#tX3)TDzRY{BMvt3)Q8Av z!MoDrYu{;$gT+*#vW=#pBfgP^P|0WRM%PmeoGYcy?WX1a?XGKQjOr zb*2pUI`gln1Tfq(#0PZOAq2vga+C^jAOw!{E{g?@{T#HCkG|HKi}2}V!AEfQL1?4> zjczWk2S_F~$oe**Ob}Ontg7qZkvANqx?%`wM5u~HEy!0XSq?%4S<_5=1fhNdFyGK2 zHBy<9)$T}hK4fHv6OLqmHVrF&p;=zxpZv_h`DlP>;J^ec4Gg!`t3#ih(K)%3Hf9}E zmLNxIebQkvc_0j1*Xj5?KF^re(rV~bbXPEVOJ^*8rP!Q3sgZ`RoOTvR@3m`2wm z1&ztg+kY`kc;-WY?-+Y^CO3&;;E=_fM&YT!@HteoYK@jbVM8ED|TUwBk$0J*PPCHY>Gmv=i)Y4ij&n59rU&1 zHGLCVoH#&7wZYAV;<1iGn*Yqm<+dhS(LiSU8k+|ol&QLZXF2SwvjEly3}D0+z2@4a zDh;~OK=l;Sa8N>}PbwOyCJtCFODQ4BSfME8YHXz2Kq|5(#E1)#jfO6Z(L+fXQnbbs z#7Vk$(%MwYQ)G&IHONqKA?w66eJ7toAz;}pRL(9=qJkPM_!ERfqf%D;ai>hh^gZLL z990#Xd`$y?H?2zpuW@>Uxpv|Wc}h)Q9y&az~M>0am`IRQ8}4bflHJrG0TvDbXHQdlrVkWg&fM@2xFF2x!1y&i9nWL}4G4MmaN^=DwO4H&1 z;URcVHUk_Jy1Z70Q9!x5!#~pkc!X5i)&zl6Lab)7mN=^1oa@A6e$Zr4Vb&5yNoYHN zOsGRM@M^kNa(qY-bTN+(p4Fp{3<~)9Dk8O83{V3+hH^Cpj*pi_GpHwGkcGrJ{!pD% zk%7TEVN3Y%#LS>7mCC0f5tH&{f#8GOkYBA*kpeR{?8Kqp%&$?ylpJ*Mu5d>!SspC{ zCfUpdO^-=6-SAO!Y)cgJ96%92OOy-4;4K6{IBR zcAEYgc}m@|iUm<0iuwxgWeiNE{)g z1a)u;#?=IknB*UtBu;WkXksOGNQ=@)qgwA^t(zKYSLuU&%m7N0^>Gjh^27vH!yQHW zoHQu&M`c-CGXOWoOi=r3dZl0)ohS+uo?OTqsq&{1(1eg`IpJ*FutBYVt};3$I7W)N zPC1nb;$S%@TsqyBEt|@x{^hUAx4-e(a`Neq_AZYdANT9N@X3qHS3Y-fIcUXEW$Cj0 zgRs&wOI2+I*Ni1zak4*9mMmGKe6)YoC1Q);jTcu`{L)apF`@(mO&@w^qccUjDOC36 zMn5B88zG}ENliN^;Z$*d7v&@ws?F&DG1avK5tHodf-1K!3RL$$@{p#TCXI~*VxJ%( zgX&XFuRsam_aFZOH5V;B<1`3d;3*_qYAWN*wk&zK^-no%7p@BY6JHl+PkV<+1OO}N8p)2k&>VK_}`TG zz3unQ$)}xD_SyHqm_MO4Kh>!6U474;W%I^$W!drrTo!c;yozd)b{aNu4-d%{Q<@lE z4HZ>vwwTW903{^~BGflbiSfQ#s;noZvKq3Qt z6OXdw2dowBC$us)ZDo)hKl#O^nmNhE$#GN*m5^O?+n>CD%8e=8Zl)Rj-48*`2EVx`BOi$&Y?WvHBIQfojdJPB79OfHP@1RG9l_1@(;*zi**{@Nwlq zR2`KM)w2RMMn9Jck5d+<3286dDAQRI@vdKkck+q(0a+8P7JVg8Lz1G2smes6rg%1q z*wu-m_DlECUglxaYERlU=f{u~r!wQ^YhU_g`N;o&dsF%8UwmeH>i_=TGG~syA|zcI zif2Cjyz<1S{6_igfBZwawwQbX&Zlm3w}0bD6(jQQ2qzgUca@9aC<(>3e1C z_RVE~!MugRM^~E$nTAx;S6zU$9$Hb(%77^hQ|RK!h)kuTm_pYg;{aumQB9i0*1aD& zFDui_r$q`qQc@ZYHk=cXEDMYdUNn1@2VO_lke|(zN0VkI&dD`vmT{eCESmv`bMjy# zR)7+oY*r`(r&+>O9C+EO#o;&_NaDIfRLGI%6j#sb-iB8z)y z#17I?ZR+~7s{?N1M$PkB6Is9+48*2ztQ76fF{Yy1Zv1Xpsuk6n-t*bAcHPQy{dd1n z7A{^=PI<^<%5S{n&&v*d&ETr9{#SWm?W(fx0fz*w1!~%qcXn5D4XvCmc_ftiA64vs z8cGLMs!M)5PdY>3$Bp?)n?;kwGejH6qh9fe@t_!(}!(wcP)2le4dEn@OYTS;KF(fpHhd$OuJlT<1LwME!T*rP8=^D6ESK|tj z+7t6ZJ>%8bdcvPXdu&w#bAU*}ZQ?6Hh6RhKGF{5_C( zFs9d-=DPD2cBh!ax_gIDg(a8BY+X}jRrh*-(<^o=U`iuReQ7%Q)M)NIHc*TaS;bFG!zJ z`i*CzZt0Wh*$Wp3RWa^=;f^LBDUAL7WDL7G!N=-=86O+bnu~sv`qsPs(vRNCk^3ME zs%~|5DHjVpaTjm;4JQ_;iaaXB}5 z|H;oT-?;pf^!{*-o^m*^q7 z5#454TUOq6M{;y*exmux7d~FTuHEP>{`doB|NRdstM0v{+`sNVr--OPw)0ulZ=6K{W?;(2&s934CFyBik9` zKq|gDn_rM4kqN_a@CYimCP|n0nOEG%*N%>jfS-yJsp)io9>CLVo``@6h>Zv}iiGfZ zWE(0vU5JN6#Zp_kIYsRHE91c`6jtpF&>YxIOg5A-KX=Ld%YR<{zOs4Kx^ndKXO!Q3 z**nUqXP;ZXcjcw!_>&&$NpZ_&-5*q#H+O-^Q~`xCO;*Ux328uGjuo%9(HmOO(<@iW z_r!{GLctk-1?n|@;OSDF9>-$HLJm5G{gBUO%EZY;%tJXYRX(iOSHe`Hrzm5h&lSJz z?ujuv$0+iLC0~w(Ns>vEknP-8k2bI+x0P&TIPDh>xc;)U>Wxke#gUXRxt32yP#7F! zU98s&ND&qUW89Oi?Q4X3nXJ_&gR8|&ncvwQ#j&`5|49~hk}NbB7-E_lll*|O>l@P_ z?S>8EKdxPZh^r>O(NxyxU*`T=+aG5~{ek&OW!TF$*dQGpHjO;(D_w^feHv3ZYbKj% z!{k;IydQSQ0_g~;!C8wP6DBbg4UN-5a75nXXNSU{2 zX<4=MF4T9Zk?U#QtLTWr7SSqH0_>b7%;|1I;nJU#>i?izFX;LKCSSSZyI0~J@Vna&>W3xF+(?zrWe z^5$1OwXFQ%E#;&$A651{=&&-RN#W#&JXR7*dHWwd$I2m@ImnYdPR4vkJaN!Taehpo z{4s%rvr$?T@60~nPXB=~jyGf>XHXx1Y@%I(>}=`cGpPDXMTM|Q*uqLN^VRVwD*&37cslA&Zoa^sXqe=4eWR&Uzl!F_Z{gFZKHOQeBZMc zK5ZGcl^W80P-Qdg%hUIHw}4Jp;O_+-w$WH=t z+mt>KPB92>lH+^xYo6(2Ov?CwTOnMW)5eVtxK3Dt?n@Z~iHHzXbyWhe^N|E6uoU5O zrN>mxu7f?GCozxEgK$Td+coFlMCZ(3SVnZ~1bOp@c|w!#^RlQmTUbXxK6=z(ot5q! zXH`o!3U6(!6UeT^zbqcFn2}A ziD%vwlCZ$4dYCrkC>fHg(?}@+Rbljfk0TnI5R{dk@m1k9GH&DCg2XhGGad<+1AsiA zOe-0+hObgMF~J;+qdFuH=T)=1t23s`=vNTY34>Oh(K{+l92_@_E+*($ks^Pbawa`Z zEp*5W-~57d^L1BQ>=S-}@i}^e>lgfR%A5b@((;<$I=5W#2Tym|Q=a+K^2A?$k5OW!(t|FPNkLB>8o=ym=ucw&=W?}wIV)sMTN(`20y7*HX&Vch?4m{r zys!_0p2?JnVL;zkq>wMtGYQ!Fp;dK)ld;?xxL4Ye#p;~N6vrx69PDT+%vM7ejHS*( zG%-vBy8R|1PJq~%s{TVG;z2V>07t1lma}tZQRMxUEHPL{)OpZP-?S_v*hT$`S<6Rt zb~qD%L{vc7d$Q74SinmxX@Au}>`z;S6OpkDr_Xq|B%67BN@v?eG~9iRGU>LPg5^sR zV+gz2QE2=GTtd(l9askF$zyB*o^8T-^B|}GoSvwHyCHZHVqU7JWlJWG3rD1OL>ruI z9CL;)1F|D6>C>Y+{trC(2rXQ%uNtXLaHt7?=RD?#?PJ@4VxNvUu4( zWznL=h*hZgt|xhgFqy%12A|vT2QE(?A z5%TT4xCbVPb)-lkN+azn6P4k@Vh2WGhgE6)bR`o>4eo488&hzK=nw?YACY8iu;6XV z0Eg~MKm<)KmsQQrU$YS0a z{@HT=Q-7;G>f9#_hk+jT-1opkVN#Ah_B2&_u$+3cLH9V!1J}MmZbIT-^VeYc>vM&yZo!@QiU zz-jtmS(5LXazU1C9qk`_(?;PB*?Fn`nczXiS&POy((J^Oh>RXONuj}Vtejqi0bG9; zXh>q>wjJDynb?`Q>B}q@q=}V(QC^cBJ0Dp|#*DU`D6wUiRxo>fH?))sxnikJCUGO&0w6h@A-Xr85qh zd*JkW7I4h|gLb|wAgIDW=-%VW>~Ref*w;~tOEo-mmh zuKMa{%aVQewa(CQT)$RNfFCNprEJqn=;v}DQsaZVrGAu=Hms8nkXMVLITyyC+p1z4 zo@vDxR0PI#nUdry$lR5GmZ<}7Lr@r@K~uINZ>I%BK{{A-H7xiwd>iWEoxFk$S%Dvl zwXbl%WYJj36^tlRsyeUC2R=M2SL}nG_$YLjyE*N!qEuHpd&Py+6B>oeXgtKkQOf~) z{-`oMsi-W*uyn+sQspd%06sPEn7Zqq$tmg(M`LtKoCkx2f#AA-i?N1=Y|d4zwbQVJ zu~`So(|_}|<>WISEne``5t#m<-NI8*w5`(eUghR%zboSLa>$`ampg8~2CW3vERnlFQPE#){CcD}&X1!Av>g8iX? zIvUXYD8ib<{@%y%aw0m~WWf^0oO(4~blg}VVYpd`NwoBTz*WDoeBrS3Rp0T0HS`y3 zAzc{Y^dV(c5B*tjQn3SM=Q`l;p3Yja2?Pu~+&=XmlA|AG@iZDc<6-q}x`~~ihNjz8 zooIlEprjP-&4LR%pZ9bByIl5P|6Km*@7`Q~V5BrHm{OR^AK0f}D z_ms73@6%U*6n-P>hK*6g*t8#b$DakDCJR4cxpNO(dU;rL-We|wX4exy?=Mveh0d2?pd$Dj`- z>QyoVW{j#vz(NZ;78vBvFR&vAk47n%CU2-BKmlFEz+8eYT8_w_M|s9TY8V|`1mID{ ziXZgK6Do|P=~}p{LDC|l2}*-cn?xOy6D>60nN7^o0aIFiHaeu+1q=-e4T5lJ>V^W* zkz+M~tVxWOp(YWg7bZI0J`zGWMxMqy_lf^U4>53rh4Yji+HvvSm)mdnPI<+1AFZd* zv{q92y8Bm^6^9>RjydVf@^|lig^oVRaCT=y?`P=6K-*j?mtbE1{NdY@%?4-U#|Cw2s9*=Q?qlG0y!}owKpD`o|{g`r;v`v^r(23C^2<=M{ z9?qtJm`soU@K`L><9OAV^*O$jr?*GdkVyAER>83$zfkcLayajLU3tdyUZtlW_Z2-(n!Wvhc-g*nOZnG-dRMvpGaoC@{H@pO?JI{1iomeD zt3=hoW*kR%3LL<$0tik-TLKq8jSAG{ikB>1US9V4zbWti^WQ7CT>l-AqTdT?o{lg1 zfPDW0tIGZdtSIy6&eL0;wv;%C{ zzKJJLr*_H+hYo1RrFM&fkCcvo=1f}bZtlKkrB~@pYRJO@#UtPvFe0%d&}0)kGD)ce zntO2f-g#5`qQ1kkLl323cP6C+4?U{>4OhBDB!Aq;q|+HQr@zu0ryXM&}6p34n}sN-oDqN*psP$2ECmLI_S@O zS-vKIj>y=Zs_N*&f?#4uyS7T|zycDr?oN>#(gPXD#i@9wS>+FFwZf#xu5(!OdBi@x z_$^y$tg!;mUzUK+WPv_0lR(?0{1M5VgELWGSsYP+%NH}IK2M|AU%Lt2DN|c$5k#A4 zlIs_`pnYx9q2jxJ^s`}q$#?xp3ugI3i+&?nlWh2=)s3;lp=<=ls@HTO`AvzZ{!6_%9iSg054r(XCUyL? z5cR`9UJ0n5f*||R|Iv+w9g9g8E?0l&E9FD)eS;q`d&t9ox*T?YaU{wb0tVYO0@;9KRRL^O2;wkqLyLWq=PDfFCjaA?&8<+ttFR{r)c zUs0~p*Ct;4ns=4c&U%cBlL{Fz-&3bM_0;B$?c2+7C!J-KkgsIrnYMvH@(2_SeCnI^ z7tf8TB9|)D*`T#pZz%pH{sH{4luk8}j zNtva@aK_Z_G_a1GNVTz&Dz%EZR-kb@;e_Da;h-KAj*t5hT6Tq0t1;nHtrGA$4y<{d zSK;5LcbOrQNq=oMmP3?JKh;_T^+>b%2h4;-@}Ug^;#C;lFA*u4jcYzN7=ysW?U+R@Zs zeOCPw=SrUJaT#U%;-!&qM)W>p=8)UVWu#eMM(ZVdac;OD~=><&c>iwMdE%VCO2RAt+L{X zg6zN9C+Tp_CW$&w%G1p8>q$N2f8H|*YHcCU=ptQ=2U?g3@liI=q zBM35X(?n^9VH*c1h7J5ofe|I}ZX2(YR)17~BP;K?gKyG$F;|uxoD(RdCbc+gpV|Jg z_|{qA_k7UkyT+b}4=)nCSWqGhr$jVWSsMjks2)g5_~PaC?>CLJ??ZP;gt^;Zlp9o+Rm9a2kC*|EB^RHm+5NSU~HL5Y);gmh*)EBWI!vU7f zI2RqX4nm}@B98{O#L1cmM6yOUf!dhih@6IoWILpgO4}|X-D2C7ZpxVVWOw?Ik|wB; z%+zl#me}SuhD9qke@@bo|pgx+*=Yhbag# zL5i<%aZi5gkU=IS^d#7SZ3$%*rKB$t0hQ^&d%2vpCf0q%_fm1Xb zqyv0N*OkPY8~?{dMdr)LeK183fUElV~QANLpcKNvzAjx_r}8qmn@FRDL|Y*76GxdK-CNKpNGmUQa;Jh#Bjcus3j)K&BNo?q&1oSM$ z146<=l(!V%kXLHTGDv{oa5EhRS+3YpD2fJl3`*r;T4=GBD0bB5~86+)skg>C~v{Y8V-%% z4(OIR#1%|`d1w@XkfAGZG2upa@R0U`v0LLiIy7k99}*4@3N9*BR}&pJk%bt*+gRDF zNW@J3v3t_)%!7==CVkE=kJZQWsTr$NzSlQ2q({5Q;yXFsm03C$4OQ)b>fpPBFH1`&CJP3o`PRY9!1TQ* z*}`9V_xHv`G$#I<@VuiCJL*L_lMc>qKzK=uUNIaracPx89lbJEy|L>Kj;gRHA60#a zWHOQ?!}VizQNe94~UHfR|1*iq6w*^jDXl;;;~B|tRTk10w){MlV1cf0Oy zpor~%&jUy3r*acxi; z$BoECj~&961~?z(-PUSDcK9hDJ6JQ>X;gf@1HztZf3ZS7Xn`B!!R;?f_dj$Mg8MPC z@CUU5Y4?ARah2JDO1_lw;-_7I--lBhH$3(A2;0>9XWxWBiza=8QaaPTv=6o8D@81S zKt|OchqMTGzfrjnCDJv8zfy7kPP`~i98$793lA))$n^%lx+%{&Ifu~x;1GgSq=PPK z&=I!8$gJp9e(3LokP)iPlm?Aiausdm1JE*n3Wq*M(*JP@GE+AaD^3F822DkE!nH|) zD2B?j04;`+F6L6{or*LaJVK|eCBUSAfsypE3#z0H0puzsR<2kv-G_v@TA5njhQKqi z11B-T%C9cCmBrtyUJ5KrF`Wgt;@AOF*h!;VL!)%+o-w$Dh88~7sN!NrO(+b+fjJtM zniPgK_-IY^#);U;$&u|CU=RzcN!62;V2kE2D5KkVXadpdoxvn!c{#60U2Cs@#3Y|V zteKP=&J$YNe6x4p-On5OBV(bCh@{SyMj1Xm43oO#vI*eoP#3} zktZYSNhg;aG@(k;LU~r7q=lzAcCMV`COaKK@3&czf~|95y?M#lrm%{^E<{u|veUV;5@zTnOUjK1N!! zw1?juBjv`CPyMNbc|j>Br-xj;tHS=Gp{|Nl#v(!q`ULf-f)3&(I5euTbP|`!7yAcR z;BexC{iFX8OSD)Lqd9_qqW|&W4*fjpPfW(`told)1Bk}z%cMA&sr}DKs_L_Tsu`0r zH_K6T@-a4~`bprZ7K=QUOF!|~7lei}!a0X6=@nD}awM6R^zZ@<$)40y4lSsZ(G7*K z|A14bl=H&zxOIiZoyv4Xc~$N?{E(9Hun3MeOd zuqqE2KGc!{j~PLK0_ME3jZ8T(?=G~uRrmHJ&v&L=Zze?1vQlEwptHL(tK)9ow7G2F zvRPjc*Q#9ta8|5X9Yp75kq{;r9E>}zJG_`U&ooXU6JTctgB(u=Di~CnlS{ZLr7Fba zz*A(b=oc)U?_XS?*)@7yUsyU}I#!_Cp^f9Tr3o{oWV5}`iPUl!(t6Yrsp>vXyW&J@xhEiH(QlX?jNsIt zG$`=(!yHO~_}-j2BEe5Y$~n~PdO~CZZsaq`qbYFaezH|` zvD2)(63M4ayW6Nt6vn6i@sJMIkUT1?a(FpZ`80ul`wF(g9`t|N8apH32CD7PB9k_E zUs1X;s_h_4SiZZ>0MG3a^>_c%Q7R`S$lx>V z@7;w7qrXYMSTly0Lu@M@w=>_c81e!LS{p)i7>zL``+8wVn+w;+pTe__Dx-bO z%7(x7{i{{!xgN}{xOl3Ej>_tD?W()V>bq|(cir}Vz24=PvPMtwjE-z8TlD=0dw$g> zk0@fLArXT4CaOexQerj2gqGTgH*oU3u^W?r4%6Ve*@c&UHTd9%35}!9tp*oRozMp& zRX!#TImU0v7b+!9>_i7t8FX2vSnY{c@T{Zdgy?Sh zt>y{-3>XR5zgy2y@Ueaje*V-kL#J3pa{ndedtyR7e;O?0p~+yK=*X^9w2g5ZyC>#< zgYknFc+diSqXlq~jQyO?TEpM-O3W3^uXUo)*!KcWG!8KVI_Y_&&VMX^Ac3CwDdrmH z-ng2BZsURHm|C1yiE74Xy z5@}YOVGkPFD@JTr4IFJ9_Qz%%W#0LNJN2Esd&=E+{ji*R>S<-gibJ*QT38l;E?B4^ zTGX!}n~2sS)P`thvAJ+)i>?4_tBm%cqq@zMrlUG20+yW^SW^}mqjg;J#*kk5qR)!axAz+WI5)i95U*n1Nzo@>8HqrkIPd} zCJ_Q+RGQ`FnqZVpS=g0221eC?uG4JDQCS<0blb+7rs^V($s%-0ZmL``0DxXq4%tkm z{efku=5O^4$*k=UWw-p={+_I6F25tIU;m@x;x$eEqe|!|9@rm%LbjXRzp1DSYMLl< zymN2hk!a{FAX+70t+bhi?l~7C8>OonmSI@y5*$G5&}Ijqt+#6)-=wd98EoFXx!k33 zeciR!>J3OM^d$5l<;WwC^u0{$A|cc{`>=0=wB?9dvuFn{-j=O6>|FP!JUkC54>U7B-#i$#kF94@ml3w$w`m}o>(#w z(#2{9ndH#j8)rZ`W0YOv+%rsOD0LTPPD2{SGeYXCYnze8ExMs(iv>_4Y0tU{`4JNa`u;Lfm^Y1J8NcyT=bd{%ma- z2Fgv}`&zm6x+}`Qi-*bq`^>eY+qiL~-e0C;!w18jV*y5PEU`K<+9lk{_HfVwrH*zS z+v@vPmY;g;?EgQ5ydk?>hNr(rlD+MMso4u#bw1Ihv=&d+sfr%`eNCC|G)#w^5y%wkcU~~ek9JX z?sU_^<=m1S6W*F%vS@FMhn6ls@Qq8B?O!&pUsJCA)}>{?CBtR;;=!_R-MX@V!-fZU za(iNnW(P#KOcU3%k1bkyb2Fb#&6Am{SFO|$_3`?(<3;6%KfKEiue4BSl;O8c7K6UPR(X@01aT5@ zL#BjJciVtjru$CX{p9vVoTV_M?>0sgg-^k{yQn+YFq#CM<PG`Os27E7n3xn%d->zNVH_Hv*y}WGHG3Nfu z=9Ud|benVz{9xDvEl_uA){G9At1iX);7J}$u!4*G46%aSsE3VsI`imbkJB!1b=jul z%bf`flARfhNjICguYOZ8FhRF=z32@}H(Fm&0qk(-q{8$z!~{Np)yAEIc~?;vO;-lA z4xp2NYLWK^w#u7i8gKf2Nw%vG^rsST+TCq zF~D9Nh93H`hraQe?_F8$(vSQ-{&9~f8};*lFYN3d411^rvh!tUoI*_xB6ZMkfaB%< zHEYU|M;%kPZ67HQJn+C2l(R2tFVcUML!vEbN-Z12Ceat&rBkEyrK@Y=uCo5Tx|w(9 zy2z)@bkR+oF^{fdAQJ#nis~tZm=@h7COtfNCcArk)wJBEMd+>dnzs0G@qt9H7o+wQ1zaDtydRGRFD_3O(4`|t0c)~F=AEsDou;@Z8sMUUBh zrdFvRr@j=4*d==+rqOi4KGkFsk@`Y^QUGxTLniR)#_P@{<@9kEUEfTHN4wZ*U9x_T zg_<}Ws>xDd5|UzP;@~jzYd;w?r|yT@%h5Q!z;?bLlgWstFlw7+rYB{+iv`KQH)by2 z$<6&V_BU);-$=<@A3VGQPUaf~d>amq`r+{$E17YFxc4N6F$9kwAEZZD`Hi7}2Om(` zgB#DQD|NoQ%JSO|l;$WiR%difHK!-1s7L!*+O2xhcAkDAc-zYRqHve5A#a!2bTMf2 zoY`t}N{TErtIQykOctgnnv7%;cuQa(jHye1`w&bA+GU3A5!6T6F7xlhb07A3yR{iMTbb*(+K4tNOa^AwZ z9xfBF_sg_*0EExd$G9=B=D-+&gh5!xiWnYB>63nuE7 z<&$vA;z{EvL`_Pw`wKc+bW+nunK9xte0H&TS~aFX7}3|xco93to1MU%!5kh>akrfw z^97+fH6m|^x3jHpc0cyZFb3GXc}rQmcu`q<|Jo@PI#4gIee(hOBD!7=f8|kH>wfaw z;lk(V7SAnzuxfp|Z-!1~|D~n8`RVJ*aX32h;)}2{eK~o>=5mTYKYzx8^4g1+l>0W! z%*X@cxBfY*&mr9{zwz+#vf+pwWzji9W$6d?Mu5HX7G-w0oI%6OxxErZC(WZw|5Q$^ zk?6So?xbR#+T8ijZQGsae={4(w#=FJh2-9*1lH_`Ca`H^whCL|**U)?th|&pq-G=P z!Kq63hh~i?kCUzoqXEXksH@DGR$V6+_JuSodrFhjWaT~RKZ5M!k|!_eQDer4J#;^! z?3C)ba7v6utNijm-?7aFa6YX`-t&WEoL=N1tu2}p3?S0XJ$q>wf5ZU%g7t!V^P4Y~ z_p8esRq{>g>=?RKI&Dp?503DH{g;)wv1sVWrh)0*!R0MaTVIYlP;bfCDkDr-Ct~q} z<*^5CDQ|klrZSheZA}x{*{y%hSlP59c=%n4dSBs)W;ivmCc1&L;oz~d{<%8-6YgFY z+MTSORz>Kn%}T6Re=PIax#jJ4vC6fPcah7klkb`yNzJ(RM+zmZquWG?%MuK2i;nc< zAu%gP&9-HCbL((Q%BlHx@TW^Pbr!>pd`=4b|4qlQw>P&cktr-He>x+1*is=E#?I){KGt+QGvDN# z69kF6SH^>QcvoHqXnXtK+B{*wJXKsq3OjXTypv(IKzx20+-bsjr*0Nyb#THR;C22u zzv}EFuX5$Voi1!X6Njd^ZHq_ze0}_m6+63OeF*$m3Ssk^j`prgR6(|uoqJYW*?D_c zaZ$nOf85|0FZoPSds>nq*lOe{uy;>UO3QfWTxU-)Deq)Otk9k9&ILxj_7wGGyHGn^ zTi{C2q03YYrk0`<2OTwhp>e9cB z)hcL%`_#Ez+@hLMk~WFquM;8ctB`X?%nxDqe=ayDLlw6YINDn;-m=-tPcso7r+ah` z6Yv1ws1(~U1LgR{ox&<79IPL7l+FFK6X5v;vx;y^a(<30{qYCIkAqIZu54Vv@3hf~ z{HzQ*p^tvoOHLX%ho(3~e*SQyc6p2PrvRs7x<{2!Cxyn&kquBBPf+iU(^L1xzL-RT ze>V0fveR6HsFE7DN}q;q=EZH^{iu4&r>vI{x$3dak510=JdYy zX5jLJ3*On)Rwk`|ryjDH{w5wfxgC}t_v_fyk!5#Bnq42iyF4s?=S+V>I+@ZxUUaIMjl)t^}f6>>fhl-kp?eL7mZ{n*`+kB%x^ z)XD>^O|#22?AWu_^wV%{(@cswBc$kJZ5~bcYXg&4-@-OxrjsD^#C?d{e`tM+I|z;Q z>wvmg_6H}l5A&*8ws*sSa@2NZTH~M#JY{Bvtf%BLlfta^OmO!IoH>ii&;gGqW#M6E zXrB|yz`T7m@5JqrXqOysEVflU{bNh&$q>fTjEpc=`OcLdspHsiOebDj?kgh?+)@^l zb!Eq@|JKjG^NhnZVHU=-e^ZluJ3eXT>D&4Q6Xp${jxlkx=bFstgffjvdvb9z^EU%% z5^XQtw5^=H40J3;S-;o}#25NJc!w zGFHBL&Gh#K7wUU<4;-Oi{FN^5WTG}3J7@0g@dJh8t^C68nmGdYe~yqH4sF|MW$C0j z3HnIWC=4c*>C)3QaM*1jdBd>RqV2NXCa@2qwwzuan#%W6un(_pP;#EYHPEs+iF(jF zQ(ht{PMQh0fzts`g4o5X;;27&=voMUfvxfxBvXrC)SYEL8NXisdqN&vsb^bfeoT7c zu>VyC4*l7(Zu@ZAf2LLS_Radqv`zX|@|MypxO09qJPoGIb~(DmLz~NK2alA896C~t zI%r;5vS_}3qjI3ES--s8x%%vK#~N)K4tjhUTKkPMddGj+88+6 z&)pmIL>^~?%%VAN3AITf%1X51dtbV7c%7-Sex=lbbmjEXk6=8NLlUlFM-$(&Sa2H(yiCZDT0qV}Rf6#i8v~Qokmd>er2dl3+dic&PJ5w!t zxh381k2|#_)m_tGEqaO*UEra`N0sqYUtaDRUs3K`zqM@Jq2tFfJ^rR~=p_cRhnX%U zA`?3$rTKc)yt29c-s3ixH#~J|Ir;DfWke2et0qczeC^LxO?LB#ca)zwYiaqNCoU`} ze;#v489(X8WnljPO%3{JK4TDlJHGXd!r9kl9TiX?FPuT@4GGKoxsHACdg=DCe6z!2 zPMj{>r`OZG_}+Epm-IcmCoGy*jvUr*PlmWd@Goqd{j0@zgUX9Pw5&Yk>>cF^r*AGt z?z=;+A%6Gi)nZpKD4)1WuNIqu^Gv;Ye}l@>zZxow9ywUH=xYj_4j6-7S+ZI?yKn1! znw{MY-vX07_lTIzl^w3FPz#QH+oGORO+&fWo#$Dv|BEH}QXyzKE?$`c>D z)W@LQznd!X@E(qDza<0Zg-;kPe|_n~a{Z3yl!5Esqxox++eK5ev^XDlNjdNE0%9kZ zx-ehx%jw7;%DII&)66HNo6t@-f51ID;fGe*6XSH+@~+H}>3EaR?g+dq^IyI>M;~>p z-4JkSgSy|l=)VW_)r0P*JExjlcZ293EsN}EEhpA?f}O7<&Mw3{3BZGPRjyOwM7?WgC?a;F9zYffF;e=egl^Ed=e zcdix@WkD~f(sv|wQZ3ZkC;Q0jFp0v+PzWOua%w@l(G$0()5+;kb$`)i+h=H~KqXD=yRwVt0%7~#}v z{&0EjV~5Ly8}}*qZ2HAAe{$QuP3RYWH18{>gU49IMeE|O6a9xdBYj$q8J8rm&9xt_ zUA~8=y~E4Z{~oI2gKF6VbXNwLK6JJkCUX=F!1zH}5TipAWk*i>sp5{5iCAZ!d3t%s z8E2HadUE%!yYDWSUH%pQv=oy({*U<)(e>+uxxlf7s#-dusZLI<+Cx zb4V}Cef(n|U5-2Mc)vdHy6dkm-}vUYrPr=-?nt4g9Zr#o+IMWw!EU7*A|;F(_zhU7 zyyp;T-!t~R~ZmF?5le^2NyHlr$ygC+}ST+$dxX*EA+wybD19~QbejQGo zn&R@5lc{a(YVvS}f05zJM()^sHk4m^#C)Ap*_})jG7SW$ci^(Y@-wH;FCX6U=raC; z&+F;U_RD~0UMDu`G5)1J&RhA=Hk7EFcxvXlwWb*e1F1E>r%`>6Ebbg>wubH=sp(I; zQuH@^XRG7o&jPhgLx*lVHTowc3ELh0_4j`F#pPF@`b%Y4f3KPw867F}=FKZl{_c0n zU;M>imTPagQ8(k~S|!V@`VRED8J^`#CdVIltR2+gAiMJF@IrC(YSTA#;z_#gWYZR> zo_s=i`Ac6~PB`%dgK!{x)#VePyrjJQ!uRP%a68NGJ2$h+jOhWB;R7CB){iVu`)p~o9jzK6Hrnj&)V2sAR+RD> z8$Wx}Xj#5QKg%=CF=l1iH`*cEGmoBEzIg41a_7O1e=Z|G_;g>qnE+#~q?WPM%Q+$Y zVerA})tZ2ig&DVDGZDOp;Mn1L!0sUmJ*Y;n1=Q(0nCPSm+0oST`c3qClo`l4zet%> zmsYk@FpJc4pZ(18^k4bqa_60QmUqABJ>`D=!p1NE@-LTPddic_t6%-f^1HwH^0HaK zyEm-+f4j#YbBum)cHgpk^_p`1O*i}A0HSczbLP%1C!KJdk6u@=SzQ({T2vlbx6aiA zXIQ(l6OTW(EZ=7z?I7cGc0F-KeaumZ>oM-Ja`(zr<&+bSFSp)tXW5|LWdGI)!_;A@@KYsat|L^n4f3H2`8TwU_`^)?O_WjavPMIrbedSKe2*-M;&pfbi23g zyL_K=5XQpT2?ShkGl@KTEyYK+8oOvbQkjwtgoM@OE6W%=-i za`ur!enPWtZTQb7aDRTuf`M}Qz60gff1!P6L)wLF{iHDSJvDN^>fVK@Ovfj%msQAl zryY~t%R=i&b1&2UCq`X$D*6?kt4{~(bj&b_?@wPPW*(_Pf!2g+Gzp5;k@d4iATa1@{Wf7}&vmn9PM4*%{Sko-Hd*oYOQ{rO7jal$Jf91)#coCAB)O` zAFsUVqQ5Wy@Q?po237XTm%qFmbkKoi+t%&n@FNZ@Z+Y`u%D?}|f0em&A3X_ z9k*U{&9&uiZ+};LVC@5?+;EG2f9-igdF88KSq?n#AUnHZO2ACTy@pA${+pdf1j02n>Nc? zt|+g2?dx>hdaRu*Kg4|T#UCplx%gko8(#O?vTWHh+xcy8{qu6+h3_pN|M&lp&Yc4p zH*2dgTS@CV?ZphH#cDalHg25++n{OEGZZ%sm;brqq4~?pj=2YvQN703$A1>hQKmu+ zDi5ZxyRQ28STumsgi`ihe>CRf$ldXr9J~+TPmXxsQ_I-xR%X^0bY}|j;VOF8`J}yY zZhr@_nQlmAosrrUklDKeb(j8BXzx<|Cr;bmPS%^R2I+Nx(V6MpwTWrxo%x7t#UTfj zV~#%3ySuBex!OCEdD^M*Bf_tL!ylCwzUal}_B-wzm1Uc+OJZAWX5gg&gAG^3*^!FFpAwBs? zPqtmo)b8^6zx7+?mRoL79bQ%b@t^*wJo1r`D9``R=gRzJf93aI_Of!)Nhg*|F1e&! z@aJ#UF8IFkn<_i**rQ~V_t;6ZyL`KjiNF5!Z)o8_&3XoDs~wq-noq@S6?Zi`oOU(z zKQD>?ZjH98vfNIJH7=zE!lR zCp%ARs^SlZJ=6kpFxscx(P}z7>|~mU1I(w&Cc~N;e1VStnB-ULC~M+0v!u#V>kM zdH6XGe=nc<&rb<&u-vBI>wDh!x2oS(?MRO;PkriBeLTvsEROf`%P%hvuqz9MV_f0CcC`VYu309+qARYxN)O*p2z6;^O&QLDLh2;p1=NE)pc9B`kHHW ze0fs2Z`FO}-c_s1#?70`64~{#FaNi8j+3aFhK0ldc;C!^oXzpVLgo8lu^E_Y5F@T1l^x?@(zvMz`iopyjMY}0T zVyk(p{xzCbL^FTjpaVM{UCtxK?D7)c@Pq(*K1tjZ?mdalEYTQa?@|6IPSfmkt%`?3 zf74*=(|(E0p!5czMYl7_4I4L<_3PJ{0}j~V_tr?`>^QPKS(Ej`g$q4756>Ac>r}Qu z6FjhW53DPjb&SWc;h~2fT=v^{d0C^~(gT7o(7m(!?z^vS(xlFPtpg8OUY0IhV!r2{ zcb@O}F`n5ejp(HZ9*~mB@n^Rq=+tPfe~vK0WB1h_n~d?(w0rx(UH9k+cCn5?=h`tN z7w2`ioG>eQ$}s1kR_p6@Uy=Hu`$p+l=LriVjUG5#?l0oFioRWgL*4Iy{j4u`VaG|J zo~u@^(r$LSogq8M9eN;z`<=JmeuwT;-XZ>yzhtrN4-HSjZWF($0~VSrv^B0ce=|WB zPCv9h+$Iy;7v7Jj5qFhwue1q*CIEUVy7J>A+se?W9#$D#sJ?(J!-2&ooemDoV@zv7t1$p-cc4U77xDMwu@oydVL3PtKK`-XRObZb&LlbwG@?(IX;+27|ZRD zPA}sIxs9}!r5lHL<&{_H9>Z08f0_CZsykZ?)B~H@k_msjth{e+x$&l3$`j6ee0kc_ zepSbS@A6KJ-OL3SyhY=1T=&RcEvLAy9J=CA?P88D|9#Ir(?)u?fW#y_hqTf&!$x*)M4_;fYf7dSPSUvp0 z@!w5)@P%C$y5P`q@b*^b20bZ?b2;anbIQ4odvv+xS^Bb*2+<2vDl6MdYD^E~^^Gws~&md<$#B}FbZ$wV)4jt|C^y9AtNOu^`d9Tu<)6^dIJ|xK*UJ0gBYr~7uoqUFRh~y#Ot_sIXk=;i z76Y-4GU1Y6w^~mD22lrn#wmo2mw)^Be=iR^`^@t6r#-D4dBhQA?E??^sPf>04=x}6 z$Vc?GgqzA|KKq68f7>s5L3#b_Ug!IF4}bVMb|nAtAD{BmhTr_=H_Pd#ovH^{-XI5h zqrT7g5Z_}P(oSbgtLe+X^riB`7rwv`wp@Mn)#dE7&n`zFbyWGo*S|qKp&!Qao>rqd z+4NT7_B($lJwH*N`e=Aph`@3?sUoSuX_;br& zzWwdqEk5R_eyZGj^UdXpU;46kR`Fg#c6J$90kab%=EIR?dsWQCHc>uQuDIe#AKUWK z3U=ZllylF0T>1RxKVRPXrVGlKB;(hf@w9TmoBqW2L?16baGW3f;6<|4n4JEma`e&1 zlt2BW*Ow1|f9ONyo8SJ9capWyC#`$x>aLNQ8O4)Xb4J9|>N7JgKTfRf`j2j3SLUq0 zzRch6__BS=Xwkc!J#uh*oS3l7i=`JjLyN^V&RB>kOK#)n(sI=u>&i(-#ILsqv{Qk_ z*SgJm1JxS+sP*Vt{}K$0x;35|YwkNP+y9`dy=bItf7FCDT2BlC?q4<$d(Vap>FKyP zvbsI`*yGE+x;OJ+*h?%>J12gx6LJ}uoiht#Xl~Peq0{7C*!}EGfZGr6x?4M~oBU+r zF?!1K=%bF(vEI7!k$?VZ`QSzWpdHC@`QFvnl+9ZyiBTf8c}VlmGc? zeH~$5fBBZ24Ugm>aroh7sT{$Vzw#9w?Tu)sv#MPF<*$@)fA{-kRJ){8PCd08CH|~R zKcGqfv!B1zPlY~04s`vx4dt>gUG4{6s0%gD+LFKWTi-5QHf<`0>R9vmR#XLJV<4? zf8Tz)?)Tl;rPc4!iF(AJMl~WU)C-xqc1^n9EO8K`h z43rxbCm*wn9_M#sQo^gU^gNXKOCoJ#Bp%KPf$)xBPLDj~Zb0KguOn>pfi}vBe_3S* zFXtj{76IqooEz?Wf7yEfXF&KdhFTFtHPz8YbqHAQ@UZikcX&^K=Kt}7A|2$}rBW7l z+JE0=c3i7;)VZVHUu$z~2EK5)PZn+5!N`1;kq`R^PSR5jJn`98k#FO6xOS(Y*?-@C z$~rmSZQEj^NIqb~BX@D!t0QE2f3KW`r`)``Zz5f~@uS~$_>bF3HvDV1geQNadW}`` zPkzuQPt#?=&ct;7od`QeHFH7PpJq<6vzR*}No_Lk^p}^l`<-9b>VCAR6tU^$gpHWi zVjhZe+f6p#wq;{^*2#C4=bx{yIcR&RCY@Cn9vUdC){T}6|8sp=`GfD3f9*Gatebuo z{7ydkq;l~;{lkPE9y4L+3o~ZLm;sY@Z@)CcUV3ScG^f-5Er0q)a&)KI;e->blT*v1 zd2`I~+X==L>&9iE)mCmhJLHWE*YBYtU9EfaQKg*xyzz3~?E~e{-yahjP9_~0-DynQ zatDa?CUTsgdmD1q>q36~e@u{DQpFsA@gi3xn=mHaLBTktNcl81+yz$XFMq*`=ji)c zca_nNS2w)==M&Zb69nVrtb9{~v&+z3lF*sQNzWj-f9?8)u>Jj=1}LMpokIi9L^XOt ztK-b%ZxotWIKbM2jwd8%8lb;#jlSQf(B_f!z;LMj$`hWrSr_B#f6~qa>&B0sM)yy6 zm;Blh;o(Iah&=)Pwv2Y)?LqCfTW=xxYU_SQGE85v$@-K`j@<{j1)56#7TRu1?xI9J zh%|EZ-|PPKsb%{D-lwTA<>`2FkT#Q+p5-ux^0bwc%6k5&wV1nLN%{D7`<26%uPMLy z@a4K67;{Q3o-&#ve`mK&FXjKxmqyE7Yqpf}TQ8BUDM_X*oX~F7LmQsnx^hS{E~D>E z(MP5fx`!23&oQ-D7s%PMyCc{UV1Q~7CR3aY%^glISQ-zw@hY;2Fkl#1SHEA*Y~gG; zT?y=yj~XwpejXE1(01%CI!%mpI=>v*^3};gF=3m#%b-m-f4CH(oi!wjr*jAyI7Mhu zYA4lhZye%KS$5b<%h23|D*gX-RBa-$U6W`(HDs4?;ZYcr{SfyaShUiQSKrj3Up!H> z#F|)QNEQ?CWN5SKO;5|x<5^TQjP0a_+m3r_+hv*V#p%vV%U;EII-hmfb)KVJ?=NFl z|7AIF&f2ncf8io8(3wH}Wpq0^jV~?0y+gox#bO$ zigMV~agxtTN9i~Co<|2#nFbh+Dtr{XGFDmwRBZUxjd_agmJhBz>;}CfRC-(t+`M*{ z%EMFq><~Tp(WH-joXGH!vh;{o>9OVc!uc@<^lU=ytg*@nXge)P*tQ|q!xLGzh`A>qTaX<>P$hstlIgR@|X^<&6D)(LobJ2$dT>XY8^Fp5wBaA=4@klX~*N}FKJLaz_@4e!{@+Ti(U%r3qIytYAGFOg| zU0mm*ub~LGVas^=^f&J>@BQ?qa>Jeap6PWLf0nULcTJ_@&XsxI%ULs7c=mI~Q;RQq z*Q}8>=@$?4T`HUj;b)%rr{%7jt}1Kqxt(k|Lr0jW>%&ScRxScoTz`+FPCx#RNwjUu zNH(MCJiGt{kd zfAkpgadtEU9Q45p>fg>#*(eFV;R3*u3>AcfgK0(sxFHDM*_MFyl)ODqmZ@qPZ+ptY~7j*EzUGky>t7)w+b`lO==%T2e)K`DGn_DSrdEO}{O?$~yBosu42Fe<1o6=R36F?Bi&eAV05)Y{|@!iOVuz7ZZL(xrUsVDfw_P zo3nYIq{(*4TQyPgvV#WEP5UQ-Xm5w~BSUCPcA5;qm3l56IS12Po=r&TQxL24-GvA| zzNCGcA0+L&+$$5`eO#`*5y zN#C1mzT&sn2((7C(e|fu>N#^Dsh`t-ah%)kGz-heZ%d&x`?iwtQW!)X(zPItb2HuLh zak&u+aqr~i&eGE{6oRNG2_YGgh#Siyv7B*$U(#sF3QM-2`H+V=^NPl9@x-|bY zFolgN-B-1xX^WuWGJcLa_gY!<4?1rxtt~nqDH!mKa9x$zv1l`-Fp zv1B<3lW)d+Ew8=yx-@Rq7=nu1Lr8}SQOTggJ5#x=bkeb-4#t>2J5xgdkZrnrIro&x za(nk`(3NY9e;j>anhdzFuJnF>oow4mapk;9+~gupGm8TW+@IdQx*XUX7BRlt4D(jG z(&5BvGJeWdSOb9%GB8d7&L%-{=g&37a#|N43cN}Uu9e)(m6DUOz#tdvFH2+~cxOX! zb%eGlwJ(MX+3(i82~+QW@rey942R(yfQk}I47G6je@=+Dc~7idyswMPmsk>if6Dv#b`c3Pfz zM7m7-J3}rxqZ+o`=g8O}w;Dl*aub&VE|5Q%f3;haNmoTGHm{^NxJI@v=*^&F2CXg4 zgN;%LA?_UYWiWs>6vTC-?8$q+-f$qF{xRXWo{&N&^H7QVF)TZ@OE&~-9CyJ7H%iod zOU+)RA_pSn!gBvS6O6^T$>iQT!``?wMoe+J2B z5bGL7+vy{8kQ~}ikksA=T3Rc?S-?%J7s)42T`d*ikW;%me5@*HSaL}EU>9iasqzbi zfj|<@khFvODn)1g=?h4b8PjLTr=N}hF0v$^Mktzg2&7|#Ai_uxe@d5~1%RZ;rx|RR zFmiwuI!#aHzh`I3(?iw==u*bl(SIf|mo8+4* z+vV>5%XP;TD~5k&_GHwWf&7&*=5AD;eB8eBwcVu7L}m(34Jt1-%toOu^%fPNBM2?E z2dVfU4G&t~?zbHwW50q#KqX;IJOWBVf-SiOk_^c{jJ{U@hCGKPP56VGe;4r%lXJIM(T@#<1LoorF1s7@@n(QZ3=mojfFOqrb$EG*I$WnTzDov{r zt@4rCOVC6j}wY+@z(I+r)d%3C+?fwxKTMwE$Gc9)d_E-YzfR%Lh)mO_OfBXUU zW)=j75tnt~5xPT%4yZb%e_*bhW~s=<1r=`jP#I?XKd3T|KxRC105BO}4gx47r~Uyq z9=Oh5k_8tpx$?>2I4$L@E zqUT{^6GSGStio{IdF%W0!*CUC!9v1gLT6AZs^Ko(4avuG`s>n$r`Hh+anG?Qr)odbsEt_ zGDwSb1e*QQJLZS_*Udu=!1L^diehG52;(cznH{cFHLh5lBcFV=4eConZ^Rg@k_566 zCBC6$^5`WR2v5z4;rnIJskX`1_G`!E+fA*VXtQl??+ylSl+z(8+ z&QTG~tJZ*))DJ&$Sfm{MQxpu_pGH_{0*EGnL>OB|4e;`$P1T2};;|umA%+l)M9)hm z-e(^U;!PR{Uhz_og8>J{kB>yxWRM%b+}y9)FFu&MV0Zqs9AU@KOa-2cCK$1@!4+Dn zZ$oBy)4q6Fe=1k26)4d3e`cyDX4=r?D_6ll!Gs5P`nu4GXTBh;|NZtu zD%`i_)dHy)fuU#aoVUOb&4g&|o1M|dti>X87ZM3V3t^n8F&h#knduSFB@Rq7BjhLy4YK0!$L3GoSjOC}e`T_L*+9fNg$Q7l%o|%n#l>C1 zYoUk)i6kQ|!q=@k zZ>ltnFk`4A^gP8-{Jl)*X-TaVuUex{mbCVKqRIoX*E-(w+LWjfO2RM!}4dc{9JhSUh)jefWv;f9moNB zH0&CVHOTX0W_1kIB70)vdyGsyzVwB_(h4p_e>x{g5tA1_;}VzBv=hsgtrvT>;fcT6 z976L?d_F~`X!$HEoyh5J4)z)uGg^$h9xWxmYcv|B53m?!FGcikJJ@Hq^S2aA=T!w> zVC^hI8G1k14>P@)!A_IHg1A~=MtG?tbiygf7_YM^+)%g~fH>l_Ce{etN?cY&W=2l&O%j!cdctKaOD#N758xxavp-c4T5$`>MaU660m=T zq*KJ?!TgI%sE`HN&B<&r?X`Hax76*me|U0`;Lz#bp}l`elNf6Nrc zC@gJvZ{ux#oH6Rg6jKNl3*?EkNSk-Z?1s>xT@fJ--$R_D@h=@G~a}5T8UMVe`d^tdUK|{ zKYT9Q3Gg=DvBJzty${ef}twH(PGhRX+p?87n-jm3H#7|XvO>O`6M+#N}ym= z3Wgma=Y$Z6K)a~HyIVFCmEQuTFYdyX$ERe;P!{!WsDc z$Cj>8Eom&3X6Y)@e}P*xQZ`;~z}^;b8n4+?ihIoX%xEqV^}^06dIuFfHVz&%3;e`6 zwDgufjvPuK3JYofN+0(zIQ~z(!5DQ=$mk+f9R|eI1_AUpFi;NX#q7rj4=;@&(M5Y{ zEYW|0=b%7v4ceTNqama5f4aW~3P1OA(W}UOD3RzeHabS6l%8-3y(oe&p=C-4!yf*N z$e9KC-otlM1zy@2 z%VfiMOJ)0uOQZEWg(^bY{z;N{bB-jHFH~~hM9CHLexBYcDUCfZf6f=Xcmi9uZYiU` z_(Vp3KEkRmKa`Ew7@_zP zpZL*fRwlpbxkjY2_eFRsBA(alO;e(3r9?AV_?n96y~LuPf556CsgR8-&TP-kg27Ql z{&fRuWsDQ4I1a)t60Hd7wBCK_ z!C8T9I&ZC18(L3bH#%WJ{dKu=Fo>O#6F1FVcrRYOcxl}DZp7qdM@XRzeoZU|0#$i& zBDrlv&Hn`We>C2aP4#ia#+|E4l2R^Jvk6spx^phfY<5_Kl5j*gARzls_;@vbt@N-0T$D-e$d2e_C2yDn4ILvR0b6xAT?tQXb~W z(pFWF+=nX#if7VomvWFTYg%uR>Wgbj`DfB42lk)|P)xEky9oY$nw9{qg%vti_!;i^|m8)WsE>|u|jy~#esaCC; z%$hw%f0iv<5i<=DZolP5>3G>ilADt&Gm%b@YB(u-bnPrXx^Tycf35OuOB!iDXj zn7&LeSK9Q9koQLI>F2`MK=iMv@HOklPprK5e{>!GXYzNyoJcRljZOC7v#nt5{ zr+d&MN#*~mEfojWmdrY#y38b`+tPf4B#*2tb+1IaZOM{+nVDIoHBXYY7c7?sPc)G_ ze>c^WoW_}wQiKnSWs0LNY?N9 zk_+_Y@kqC?5sJx;^QGpjT9WxMHrXR>g-?uuYbH)>ktDlnZ-nnUQxtWEnCRHGiS@B$x|>}S}iZV`nt@R zWlgMw)3PDEcI{HZa@93A$_+Q)AtOeNlq-95x6u)=Et)rz8OU$K#2;kUsx>nBrB?#N zsZHxv^3OjrWa8u>W%Zi1^1<+rr8U9`&5>@_tl9FzlxebR^%{9&=sPlJf6iP@o0*j< zd6+HMs#`}gz}JQi8;iNw@}r3SVeqp_1O(`KKj1_V-y<|wqClx!9N2KbI2T$WZ{5`Q zru#xx#~x3{B_#a=C(+(Vp&9kA`#VBTzgV~M3(J5Qy@*t{%r5=^TIe=>^>SVQ%v;T1?@)a8N`LCMEG{p%?vv&;-q&^TwieeymK z$n$Co*9G}v(mqL-9anCURH>=iNBa5W&*^eXhjW~0^HOslAPA{gOWTP>^d^BF;uqwdoM+%=R3|95W4by};Ad{Tp3 zT#~VL#{<;{u-cU!Y}QGb++{&f#saGNcJ0b^{&wlrZcfQTh7~GcCyWF8wQVg|_3UaN zBVJp!Xf9lDvrq}^f7#iwXPbGsxgq&gOy?O3LnRk+=@o;Ue}%|bW)?Q5AfAJ?^z@+q zwhfL&bnnth8!9)dOq%qgbnA7U+|#>{(|9dsyW`4tVE+=ApQ~koJm~M?nO+D#aTk-R z3YRiG3rnk{T*EoslOf((*^|7u^G?*;chfBReZVw7lG^^%P>DGaDtKmlJVmH<@nI*m z`A*%TDVSi?f5wi@SQ}PIg4LFR^|Z9M*dbUBul4k7sk}0{ImKk-FOaYgB?OWkW6rW7 zN!IPZQBs?te*?|Tvw}08oVgvY-0vUtf7rMY0yBH6!fTRF7d!BPc+@2V@iOPe;WwJSaB zutVhSp|48gMh#^-Hn>o5608V{bXBB;|CrqPTU1B{jiT~k(|ek$4UTawT&fsEOHee+ zS8~g^eQB{`Ond~X`y|Pl7nZ@iqZv)4e!N*~|Ik3P1{qeeVYN9Z zg)!sH#l{uT%bc4p^?$4@Yo1;z`8nmJ&cYh#f4I%d%2^}QVEDeW^5NxJ_`{mj>Kc;r zRB(+i9-h58fBrRHMt(6$1`T)?YQtn%iQSoZ-S>bOE$n&8(&h5{kfHK;zlVfJ2o^0` zB7I+fJPgkp@cSQs$=GodWzY-#RUj=yIy*C*{?EVi@h6|k^ZlQ|@q!fW6kRF**ZU!r ze_Ve6vOrD0{r!)O{Bo2Gc=jp!;gzM!mP?<$k1F{;{`?!lb_QrZ2Ni3Ubm@u(*f=^O z3Y6k&<8Ewi``6>C}SJl#xf_+S74KmbWZK~#`- z`Z8J7GheD=NhtM+#_p)7_vLX=J>&9il6-#^DOVLjYa4b4#yH9ll^`NhYGJ*qiAc^I zR6S&6+EGc8bJ`Bcc_^sPOXi{T@^H)=T+)g~HfDS})Q9C#alB&t4n6M{Gl7GUiZtcZ z(tohN7F>&qCoS_M?8GJ$FI1>dUQ!|OwquuOKGd=1D>Ym6mCqEjo|lh7gZapS1E^4; zqNF63ljTe1nvF{#tUkun82s zvXau0WaV8rmN2-X{#zx`h^gW|;;_RYh}OxN zZ@*R4b_X9U{F^s_zD)V)Ck<eEXO8^z>#tstKF{vh4gk3GP$BzJ@i}XMZ0fXFayo}bOaymcKKHKlg|8_ex@AF54X{?tJ1(yJHPp*0+_wh8 zDxE^k_8UwQytiFNQ1JOuELDNUBJi^2pjPK3UKs8A9(DVDRkNm7s;ap-mq%&&I;?*t*dHI&JKV7^H13VH9GN6Klx;w5zLUyTee7(Mvdjq>C?lwJODGx zwk=!94jkJU`~COQv~gqIP{i~+U$_iwn!o+=hpu^^dFp9WrE+DNi+>qs%jV6cJk-Fe z*Q}ABe)&alX}Yc5K?g~*rcI?X@OD5|&5b{Q{WD!>%Zxwbu)}5F2K#{b%F5UD8UJ9D z&{E}#w2wUeaH(Ifo^AqS899sPu34_x9(m}Y(x_1*-Brsn{yTe?%$+yiDHCfltj7r} z?|&trtTuMu>8e%6v|7D7b)-Jl+9)9GIlz4|%d-M-{rdHKhVrZqXGqf~ zO>m|$L$+<-jvb?oo z?dM;l5_Y1tKmG&=wJH!Mvn4Y#Q}%1sS`L8_BTyibCRMK#y4>W`tVvTj^`w(Ez725O z9ejv}bEj*CiWTJ0Lk>|MiO~MI8C601CVZi{C{OZ_>ngrNjdS@<5Wef2xUJ`4U`G}lGGIRZ3K>ualmXqWgLI_;qN6- zEHk{_Q8x`6?xU7NxUTh|2CH|_N^lx0M{9uw99lcc0f`qH9{LoyLs|iW*Q{wXsSANu z(K-M3%kRI*sqNcK!}|Nk=B+SL09Xk;&gwWz;!J1sw}0R04C=hI&XQU+YYA1L8#iqV zWc$_E-vpiy*nd9_Z@h0KY0<2?4lutRKMr)ZXgWqxoyu9^qQ#3P7aMqTaMscNT~7aZ zhWv(^WOdjxz2yA!l^330T)J#2)*#oK9kBSA(<`Q{T9!O5S-Mo3V6zP2n{nf0^QO%z z&>A&tsDHCT3Te&?c~CZdM2zd(Gs)!>{2y^B)f58GRGlhK_cx!-djDxYV zIj^^7it9P-t7ad8HthaxyedsSmJXb~1xlsBJF~-S(&yfLMLru&CX?l)pniS^zV_qv(t&~YW zPI04_-3MwwTX5jLhqtx>V%be1Y9MTIZ2Aix1oM7-Haqi%%&!eA&DW?ZO{#O$nduiJ zWYlLL;jGsa_>j#eg_bRv1>(LPJyOPe@qe-Ox$kbR3mQuv11G4*9eb2Md!3om3&Hqr zzLaT`#>tzn4i3$q?jM5B4!R@Cs?eR-8Hd_#?2^^u!~5(U^m;Id~U2 zuV4&6gV0YQ1G5zhCkl`n)oW;nP9~PGST3pX14z{$ZFp|jxDm6xtxBp|RoG#5fHN+_ z7N`R)gqFcQ#uQyMB;K({AFb#VB7Z+ln@Z0PH9*~J$iHzV}C#-r_*&udp0&-m`Ekuqh{ck=qnFNXA4oni2v z{r^GiDP9j(ojYbD05yO_*_g1$sDCeS_dkJ^_kjG z_1uF4*Y`ivPdkqX@4r`xE`No~o11UDOWqmwzN&C;y6tWm`0{HSmzGvuxRa0T9R#;C zQ(6*so;t=bk%mmC<9q#X;=P0)ce8 znwnZ(ZUR3y-*zXicgxq`jSa-}rWMRZrKP1p7?_SbB2#fi*yeAL?JSQ^%U7(>Kzro_*ZC-zC>Xe&RjX!A z-JEi2`;&CNE+;2DFnuqtlzblcA&*? zWHr>3%#W(t!?8K#AXvY-`26$a!gJ42p?dO(C!(Eomc!e%gMYyUs9AH2WgBjcDT8P= z5GwS_1YfkeDtsKBP#%ndMAU`S}s=?h`3<%`+>Kob*R>olV zmK(0qROKOas()7t)!O)xOQuSSol``>Y+*61e$aUGOUUTpgAP3Hdd=;U_E=cC31Fxs|h zC9~(wlhNOdRm&#B-Wv{cUabQ0_J*d#+`w4}9rS$p)z@0~sW2>E7oDdUyy++~2%|=S zW77Tfi`3Gs|Mq~nhk`T1Q-PRdcFU%~$g~A3zWlBh$Ny?lP)$eRrVuM2sH$XJB5<9L zz}XS5<{$Crr@UP=I^3Ka%zU+vP^g6o364ZZ`nO9(knB9J%wc zLizG?44#`dZUVu&M!1gX2A&J#tc+`PoNbPqI1zay$*qJD5}?n7+i&@TPOq2&L$b3EW$TM0ew*q<~2=pc0&R7@5~lWQ`5G=FQ=3LRPKW{Ay(KML!vuxa@wT<*Am7F4!u zHjRaKanzBA>kd9Q4te<>W^>(o1xGyODJLsi&N;h-O!$71oO<%faO*S0olMK{zZ;S6 z%3!(`Xq?==c=lOm$ixYg<+M{za_S6?6crmbU{vw z*$5tB=bwRm`Q^9YAOwF`Va8(;d}m&mhuI;IBJg9&#s_Q5v8xwKs^|E5f~WHz+4m1cMIYwOD|U&gUbpLG0jGJoM>b$h0G=tOS5{%Se@ymN4j zr-IC%w?H0!@@aKkP3P78zHHxf%1I~6k5i3-iBxEixPESd6P`-Yh`HgBhkx#u(@r@_ zdR%?I(36Y>I=jB?(u;Myd^X&fxqF>n-L8-x-LHUA{7P`qHcR?G@;HR_DoI7Z>&-76 zyQ~c)Ni7(xc>9f4ux!;v2d%yPJdE}CMK(*l2IsIL!J!ZNs1O=Z?%DMUS+HQSN7zx^ zd8?TtiCj#XCrgLZPgP>p^?zs*Mj5v8ay{;h(@v3Njy_Ux*nxDu{4%VY)R#{_7$%SP zdk|j3gR`WD4I9YZxn=`FD2D>oT~+DZr?*@T;WqsJx1iHKDoC@_Wm12idZ78PJoa#( z08JWd9{%oIGJM$Exbp8UdGw(^0n(RUdXZ#9K(Pbn8m0wKNC>r|wtuDG_;NT|E$8v_ zKyzN+YT34JySy;)CAt5;yX4c4-^aFnk@r6Q*bsAmb<>BZI=7-Dec{Eya{vF`B_lq9 zfPg@Ie>l>S5z9;n6`OUEZq%TlzvhNpwBdS4ds3N4;>3d&3(HN1g2XNCd^2WX%#0m1 zF3P=Q=MGDX z&OPTW>HXlta(R~?7`Sbd8{kBmZ(#e5?egMFuSoBE?nK}7E`R#0a)CaJ!mw?dHo_hA z1oq|Jyygb>_8DAuz&B@s&aN--c&R+t?@{SYXV*=eBFe-cuzy>*{<^E>Q8=`|=&~#H z8@%iG|7p@?%a$X**3iz_{7#1U2%F2C)G(7NBR>0FF1WOltbyOq+x~Zx#<_oVhDWtc zL_nu8@w>0I@PCUJFOhzahi=|rfx=TG1nI_pH%@H7}W_uYMm+{wUi+qb7n19{9^^VRG+;x->l_96s@4odq zMoNod$hnX18YLk*l^s0bS-f8}mY{Rl=`;S7>u$PL^Vx>=oRs7gj9w~0{k0lvJ!W>o zbTsm8r}#g?87+-Q+jLtYEIAgqEf zIK8OKRDXmY`(C4`iH${!=rlYG#7tli{LrTS{4-{6sd^N`Lsi>pfDcw2+GwG`FXKWH zDoi3~?hhCa$+L+$%9rP>=xRNyLzBSG+;$3~_gcmV5n*YxTkoiR}V{g1!Z z!2PXv{-3Odqw6ZwszPn~vz!kBsC4lPc5K~_et#)h-H=hcWBaV%e*aT`{^eKs;GH3| zZQBl5SgIw%KmIg?iN23K8S=~rI<1}wRX?3w69z$`cEUu5K*!cIX3mrelT6&ZSla6Q zzvA!^&WE|zsZ{Mcxf*iyL0{?*stDy~V_TkqXJD)6F>L^-=VuT;5`eGFVVRc?RU**dajpxfyxY0h7`P~NpPvHo|*ZHDGC zM;#%(?=?G1IZtIV7A{&Wcihuk)rQp|e1BVEt%fdwc+Y9}uy;Syd}%>s=!nnM!}$If zmDn1fQ`wU{oF^w9f2>>sS3Nx;gze^qhOpkz_rc!s^m8xBgb9-{I~<^+kjI~TR@52o z8Ry7}CmbhN_v|6PuDu~FUH<_u%7pQgWbmNpR9*VyGtc=;5=S4;8aGaC&HTEsW`D)( z2W88s72y6+1P9BnEdVzr4L3dBGeN`*g9w%`Lx|2nMBGp!gkH>oI>m99G;#@92SLnu zX-B44GGanY6^o~WVaK9Yvg3pAIzB$F`v12-|3cr(ei=SNv8(zjOc0Jb>PQ_ZR=@@j zZf@adn!EBJ>~pU?`{F>E2mv+t#ee6}XWb}|KlPmU`}g#I5CW_zHh3K=w?a+%*%zZ? zGQxp7)%_M|44?fkfpG^PaG*@XE_MQUm~VB?_EOH=jr>|5zhju+&7t|ZH>J@@HkaSL z5Oapd{ST}pU!kVdfZ%bqs-GExndZCk6Qn!V!RF0(RFSacUjZX<3l%YAv422(yX%f& zR6E%j51;9x=j=1j;B5%-h(!Anj+deDK-~vWr>j{;l|`EFO)0vu_Q14}#`~5XKi`!g0^-x5+oy8PAO^v4Ar~_L&yQi`5h6VJU3ZoY`{2 zEq7=%@fhz0PH^tRZvAh@2J>Ud+<5Hwa_GIl11bU|3o~E`cKn5=|*1(_P-% zL&qi?^E-Cz_aVLVQh#jRH~`zZ4?FY_?1W2~|DnTA!-fXJ456Pm#*Is>PsMFOM|7hF{>@IB!b5bV+XkLm95c4#S9tOCPgu+Cx zxL?0vqsFbo^nUn9pJK}Vscwzu*|IU;j@7t@3l|B^NO292m4Dm`XHgbl7U>4=z`Vgx zW7=?@U*nc`>Z!SSe8REE=$SSf&nweif8%ZLAjRQ%J$Je-&c>P$#IT2a&F^TekyeEL z%XD~A=ZszTpi!@3gaQFk9XCMqG3a{a*Js~9K z5DcU1L5==sQ-526_8L7^wAkS1ZfQruF6nW>(!|4Q!LZ|@Sh8>m^G3P+#6G%8r7Cja z`RAY?WZ#N&r|8R5u<4Ibt7Z)dyUQW#j8=%&kZ4tD|NYu%pLN{PN2vuOMRtC;LGNHJ zN4@dp+wNe!@BM#w$~U7vlcqSrV1drA4{$lV=7h)s?SI;~hVVTY>x!IM+;Htx^2DQk zZM0sQAMN3~^W*#G?&y=SNs?t`pS;wuf9p;hbA}iAn%${z&odDp3>%6&R(+m~`*sx8 ziY8*=^AQc9sVBmmx$|^eHiZ&_>t+)tPgZpveY#DgYB~fvBAX0|#boD{JxKz!LJ9`L=uLBjEw;_D@ZU>^aIlHkMUqfMGTJ&5Ao2$V;By3jlgGk zYM+oWf<8DggGi*fAR!DE7jtpaWot2;mf~qQ7Nv7{J%E0I0>>Ap(MOa44hBlADzIDXN&tc>QeJeJ| zY~H*{`ab%kO{!Ps_tluM!}8mLUEkdE(SHC-N9_0RzOPS6IzA-fOhrBo{hoH_xnT}q z!>|(=pwn24BHSuJOP)r0%VXB3j!r$D5Jw6_V4Yf}V3D#AKD|mpR!tzK5u%CSNzBJgk8P@i98G6v*sU;>{ zc?^JRPA;nu>==+6_O$Q8;GTVQpnvZ%6r%p}hz|QKyHw;xwr8~ce1NiVWuI;Oy7=FA zG6X0+w{Q*5u2;JA3}4lA#}?x~E5J|KF-xx9ZHut2n+FbW!;Ye(jyOV2J*9mJhn!VK z@-QBaW(bi2JdpoLd`2U~vh&PJK^hL}anp`T#|D6Ak4sonu!2q*aD-nT>VE}W_!Xb6 zxIhpapBOQ*3Y@PGlTkAXj|>m-q3I1u0G|mU;pHl>XO`FRFPOMcH4;o-g6{mvnTW6w zRK&!y!iI?sgKoBhF2jriSRQrSi9W&#JIx%pwk>Pl;~r?o$sGl#pRi+c3OYYw$2j(_ z!M=_m@j)K;i17HApRgl^IDb@{@ny6-vHqPoTRL{>=`6bL&YVkFUTBT{$V1j(x#p@u z&j>qRgUvUaR*IARFv07WnSOBom|70MGzAJv3>70b{P~QRohU_(mqan)C8~~!iM=wi zQEPy2uh8I9J=j^DO2ELOF@MVJU);yC{IN<$XzWcZCA1uzu@!WNRDVK?QV6yv)SSPt zIi8ucYlCJ(hVbKPuJ9j9(K`O{!WEtqrB%e_UVePZ`38DZ#(uV=!p$B#8psYOOFAuqYYBJfq`;3zk7?YW65-e7hc4&kdj>kXrgST5PGgbo5M01GU5Vj zcnGEN3#u^uOO{omoqz1Cb(CbKH}j2WhwSx4Fpu0(RhgO_3o^5e4&Ni?w;L6D{bQA;;X;J=UYaxG}k~@`aZI& z?g=Hj2s&O241bKls*l>l;IJ40$ycJw6c2OyMMu)n(g1TjPW7T?)V1?v^1`#B=O~EJ z!{y(N`CKNA|2p7biQ(P4c2b9ebPh{Lr`F*t;|dFO8b}9*Qzy}BVEj%BJ6>|3d^dWe z&>m(aXRYcS_SsPTqOtg-GuqEb_&cLDasJM4E#2erUVmO3@Qe<;BE35^7V}Zbh3i0N zSJgI_bg8+zztvL1bQz=nnHtztt!=`}S{(DL;HH%rgF4N;I5clZuMbfNhUzrXG0PQ=SZ%nPxD)1U-X8WooEu)HHr{T! z{+f^^ET1}uojNTfJk~=OSQelO9X|)R^wdr>Wqxs%E*&P)fvL(auK;r-fsW2>0{7KN z7oSru)O3#VqidJd@gDZ(eGaBvpl7(Mp>osk}MBnv@CKb$>hHad>NONzP7-P9Q8=F$tnxMy9L= z3SR?76?k;Va}ord)BVb8ZwyuE+x=m9elx6y6bih9UH1nR^+RGGVmT>P&Lt*M)Zr`7 z;Mb~IOWu0pWtlSZJNe|pcZ7zT-9QI_lg5ov3)b#%>t4rr7YL3HYQXCNUEwhPV1KyS zq0KaUr)Eb^2Y;Kk80V|TvFZ_$nqoRncIbE9dXr2TJ6d_6qrWDN_jP9}%FEcVLwKQ` z&Mk0nVS$GkgH8iQuIklIzW-*FOqukpy!rY|@XWoBqRyQ=A07*W{*;=~Terg-5p4P1 zdGA9!1K>j*g&Wj-^! zU?#)}@8g2;rRPq+WC{~MU?|<1pA;}dR!&VAVlfmorA6eF@x>dt&2SI zWPiB=aT&WZ^C*= zOym0W_}v-KQxgLE@i>-Nl%R@(0O?>~!Mo_+VIWZnzfqNDq%}MiGsLOh`@`j7cola8 zjTn1@v!5u-@&Fi=_rs=q1lDscXJXWL|Y^>a3;P6~hhP2PX|P1t4I1}BAe0!|8B zwrnB)&YCHcewd=^UwZWoIkeruAv9^5&8iXUoR*;F>wLM;y$=PCOQr?V`thO;ZsC=8 z&`Yn%-*7KiMK8W*u<|>bxBUB%7x;STySA1`Ni1p5(yv8V3}G1*$Pq9buOTL0YNa0!gj8 zR8lIhle~St5Ep@c>;y zr6E4U$7P5IHNqp?u+Ju-2-s!8Wi?RH8fe*~xpakLWCFk68t5w??e~y+20!bJ)77pd z4J~thFLA)75!*krecOBRz>cj5{YBze~q$kja>(#DddKJxM(v zw8SQq!+#EKC)MF`yeFKezT-GgWqRxX$W4n?T{*q7eEi9~>iU7}Zx%8#GUbMAu9Rbs zIU4SAHmmVvdN+>+6jt}#=W-f&%MEZ+X#CNoKp1E2$Ow@52d;$p=Y|>o&JzAj|7V8W zdeaRucH9JY4qF2=)YdIq$`LGIwd!HzbJMW*`+qisgWClQ<-HHhjPOl5qlM=Of;zuN zoOOQt=G*VUbGwO4g;1q2R0})sPVB6l{sJ>xar7k2S*Hpr1m9Xcz?!kTrKC{k6+qw}->jnCc~&jizd?Cfvo%*1uM3?a_Bgt# zbbmgkx-_o?Zy4*d<>Q~W$Xoa$q~rU4=)AgeQrk*WrDC$oT(L`@{A#`Yxja*9R!EjU z7u1oH_pc!=HEdw@ej;gjwj(@1SRH~Jmha9B)C0=^qCX~;0`|e8H&f87oDdLfb-Nv3l_=Rb?YPp>a=wp;lvFw-fowk!KeRg9IMhhH}kXJ z!cRWw1Zms0wOo987dhv`%VfZdgCnu+8&20k#~**JeEZ$FfRn;pSV*V<4+IwW1O2Od zcGX9&Gx9ith3QZy)TxIJ8JMlAbAQ-@FJTtC2o4ibKBy7bInH4%zTcH4b{l6^XS7sJ z0@%1R9V_L0&dP+NT4!*Pi`}Z_(=hdMfV)H0;VfDy-v@VXT4)Uq?OidujtI!7u#oR; zP+@w`?7YD4J+p*|Z~9?me%zbZl2yNj-B}eYx=PDl%c_b{R5plQgWECa?8uC{FmF&eSxYtv*;XArPCUZwTbkqT*ILAGB6v zRujf&ApYWde@V3zIwQ`PDk-~E*iGKlN+v=Omcf6!1}J>^m1e{6GJ$&z`t^NKhQ2+_ zcXOa375c~7{N|J14yw(y@co7l={**dLZM=P)p@yK2i;Je>YX6JLVu4;0kFj5RIm5{ z-2tb2C&EMR?eL>yIzj6^mGllea6dV-!|8CIIxdvF&*BT6|5?YXoK-QuMF=BM(3DFy z6H>16b?e$0I~7A;!u{Z-y@wZ@DV+u$auC+Sy7sg|>W7Ic)WZ*JCv)e`lN+wPT5_F6 zR-r;gx$*j|Wj6fz@qdUwcO0Wwx_Fri7?zKYVF_FZH02u`pvxP(%j$+(ZVS{4b@2E9 z-(7kJ@$bK3TaiUMEH&DfQC)R64lWPb>~?NTjur;J#7oUF_?g1B-@rW{svnT%f7O4?LfCmS#``(kYy<#!%t zc>7ieu5-4nyauY&G?}`&iN=rL&`NG-JVjbS$j(O|gb5p4%U|2V1+hk!{glcYDCnm~ zdAR-bS%g39NxB90j$3Yo;ASP93feKg?@SZC|nxs!Z>1KYWn)y$v2FzjeTjhc~pp^zB;^|v8T z1ONPMx_^8=@=F;s;91P3l4Rw|)o>bU$~OgP2dTb%84JiM5VmXAt(OP-KH@Eej>N(O z5x`>&-g=>f+c(~POP+b6pE{~twqm*5ed-{n-5SJoLSI|+dY3!U)PWXP3 zyztyJsLMQA2s$~r#>NTDFly9j8PNYJ2;*Ehc7I$W4?Nf}ge9J(tb&h(BekM5TRnP^ zZd8q2vlFN>y^y=h>>wj<=y1e~8GLS9+ssg>uL!DbXF)A#1zyuSX|iNvwhB9@+76*d zm1hg6H(S(7({;O4HFe_gsu_(vFunO&=TX>!|{we)ZKrW|<@v&63;DBVyC8%!v) z+<#!zna8BBP{)`nArKAZsp>HuyHl$`C0Qe7r)aIyOYShTzgporu2e9lYIjZn;su#_{Uw>h1gR{<5sPp_Bz*er5j@b>J6q=6LokwAL zVSZ^iXXxkKjvG`ce7SBGk2=@#6O9)xI!AJF8!axKUW4&2j1MVCoZ!Mc4ubw$dog@Ijged)(=<8&wX0RH`BMtJtNj?+BOh`2kl#4Mor%#<$=o&w_;kdv~QIz zw|1xvO@YXMRx zCcUl6WchIA5t!|nO;I+k>{_~OpyAY>1uDRy?GBOWaO@xwP~e>m4HhRU7k_uWA`)7} zaCHDX{C%lbyB-F<&TP(E$5WB!+VG+Dl%wNsw#SWk{_QBj^5y&h|S*ng>tww#e^h6CKB zvVY}OGIGs&%jg8(Ruz zb`*Atv2lg45bDo^t1K6uy_>xQ!nw+7sfEoc6nKRBh(84C&r@nFm6_Y?NMi`nQ)3DQC zIiYpBY}%P8Z%*1Q@BCs8xZm~hDtQ(gSGpcwP0FD@HegfB|9!YdHtx!km&b3!CYHML z;6-(GhxIb#)$cPi6RcN$t2C*&Q7S+!$u+xi>-UqRA;|cbnU^Ayv97itqq-LF<5h>r zd3FAk9u0ri-G7&hfdADxvu5|t_S#Y>t%V#8fq7hwg|arYsw{U@l}tQyM=hzjp^Y3_ zZLyqCbDpOEZA%lGwWFp~#tg3v%4&c$5cNBDs_4n5pG|mWX>Sr(&(p74^Dr`wBF_vE z6Q1!NAedCUcJ2K2>(@tBY3>2O%Cd}F14iI!4-ULwbAL1{OeQt{@bJur%x8$HE$i1v z=Wac8I0fW_LEe9O@<4s&|~gs+hrT#U>L! zs0iVh4Sx^vkjY`d)sXa*9NC@|+)?XAq6%|+P6~20@5LKm_Plr00MENNnAWR6g{e}d zvJ8Ls9jWNrIqoL#2n=LQocyCZs6h8T%>K!T?|D?3u62DK4Toy{w3Y59vpZH6x`;9; zeKlZ(n4@V3biQn?K_RE*LL+vM9JNh*3Y?s~OMmiGf}1RCw2#cuD4o4=rsrCtBMYIQ ziltJp>xPV=(DtUJkXsA2Bxkp7uw^m_&I6U-MiGV%;j+tKPy@-i!3|jP7Kh75S9I?sO=0k!XN29bdd*r{xoVX=sNnY;@dbeG z*MIaEw7DDlMFo&_FiriC_nQl1l&(Ab^_DKK%ed7V;J_=S4O7?=C;^UM`#(G*MI3J)uNQK{MOkav874a+1Xu40m>fAYG4o7 zK!q)#-_#yv$7JskYPzPc{e+$A5hW-NIHz;Z2MvMJ{~Ih}gtBl8>~&Z7l+mAmEUj8L z55uCvQl5>Ym6CYi*kg`_Q`zU^LG{IX7j`!H-_L9o@I^>)pmkI`dHk3VN3{vgCVwt+ z#~*t%9MXO+-+wzwdUWHq>#~gJewl+WR&@->MdX1{`+Ta&qjxALiv2D!uT5;V} zNzGV^wJg!UxR@zJDyxCA8t|=ww2W0!ab4lp+I)#7BG9D{3LHxSU7f#C__^YloytcI z&X*I8KSugL`G{2UF@Kf_)^-<-sDA>Fu5#$eX>*xsXs3HY|CjYb8gys~9CSMxkBAHPOrGFB9Vi~9X{7b-5t#=eeOwNM+9Np7tB5Mu3FV9 z^7F61DKBeb!;?;6Ek8U@8Q7cxsYb!Z#puda(&fxEPm?!>z7tB!^OUyfFX3NNKtG&# z2zW-4@g(uZi918qg%`p@h~XdVP3wA2R9Om@xr>h&w*@X&40?V8r+;Wk_O|?OS-mpC zhL@hJGic)s4+hV@YEAo|KN&Gn_~!@gTiKWS_2sepIvejtH&U4C%gdoJH?~H-c#&wb zk5qw20)8-i4(vK?h>TbE= zTH(9M#sPD>5i;lQ$V#4^clMdGZR-x1IPnMN&F?*GL=k_Ai*7`=c6)}AIN`TbVwj1< zB-%t`xb*KSB7bHZh_LC3T2q!zfN-2QHeFHkk2AXxkH$4Tw^}o>(alyAO2FIXq+}U2 z@+0&$0@I5t<@%d%4=LM~aG>1-E{)RD)3x7y@ZrZ~#mZIakIt8S?z&CW)1^^U^mSjs z>Gfkz^_Sz0JxV>n?AW;jOJQwg>C$D=8|Dz_!&^)&xPJ-ujXS{~pzX^aLSH`5>C3$` zpzkB5-zCl4|92y7314hhDFZWuSh>04DbYJ$4riwO)R&Jx7$%SPdr&wViUpkCRj*!M zJ{d`;vYlc3aUGbsNt6Bc_uuMHsD6E8ME7L4xL*N7&4ih==Ezmo-30ID+vLgq&+Gl` zZ@i^(2Y>9}R<6J1YOTzRF1td7)7`htGn8Vl4> z?bW@6_Bl^dN3~amII5-ap_!lOUKlKoKK=~qqN9=|FV{i9^2J`%u`|qoWyoE(|4-v8 zLLD#{Li4pZhDue~w|oME?xd62$6^JIr4h<^6@PfCAt#=AoLqZVFFE~`Q?xr~9i>HLr4)j~jX%{&SziL_fa^|?$`z1Ub5si*j*;jt2 z!Q%}fH8o8dHEtv$K6M<6uU)GGNh72Dn7+KL+#l%6SE=)BCQ%u1?NuQ%;I2DNzpDRRW$ z4?dLU&6+9tlBLUa=5yi+$LU(xiE!sLX|h@TrO7RNDd4nu6|TIu=CoS3ZUrB3vt$Bh zF+8tA&*W_oMljykMe0K^)3;wAj6moeJQk><+M8~ZRT!-ddg(Q}4)?=9`OK9pUw_q$ zlVQ~Q+i~AZ=kC2UE|iyb@*a6<6Wq|Vdi5HiSBOm;H>%@SIwp+;fBrQ?^QZIMmtP&C zv+j=^^+K~|O>|Z_Y4Q{}>s>3ay*31+Dsys%{sd{j`knVbl;`2DiBtVVPe#1$B4Szg zprDJJ$B4|wh=~0PV-P6bXv-e!p?{!t!L&AgBDsv0ZoGH}<*g`m@p*+UcNH}MJ%i6u zd%>mB^hV*ErEFNWYK^R0zX1xP(AGl@*vy$Tg?%9VxP#VrRy#z#|P(mrJn~Au{pDDRR>B z$ID!}Vw(QDTk6IDhCuhAHKK&PQ;3tGOAL!@jpMj$CpXdAN0 ze6RW7#rv)}i`%|qM*x32>eH^0(zVlKcwrbhaumYyWY;#B;_|-8K+1yO#Lc?TJ1cl@ zt`u=+>`GJYx2)I_$ylJ|?pGg;Q#W29aWUPv^Rhg-!-^@}09kilHh=#>r0a%4uC#;g z7g!EeX=R|utE~D0mBlK8&_CIc*fq%DSYOe!rS1;!zvz3@6JFqNcflD+gwCElK zv@d^_efgE9FaHC5`7@?3Pf95#2OkJum6IU@04& zQffh+nPO2eHsptx&VQjUHqSM3n2Z_Sp@$wK=VI;Y_dotpN$#eZkCG%imD3H5PWRcT zp6(KzIeWI8a>jXbN{930obxZix}Q;#a#r*0*ze`+3oeyj*IW;0si|_$9Y)>hrW^4- z%9%Dl?+J9wN_D3NmM;gpSS^G$88#x@2rNeAtNQ;7G*Z*OvVZO>L4R%`T^;rsnt9l1 zxhu;UoVK~xtHm$u)msix$a;Qe{zT-AcMqcPEl>PrH9nO`r&qsfM?Ts3jr+gYm-zuTJvxZn1 zGoI3%syd!iqsyMo9WRqvbLPg8G)WrNua9-HFdD8zE4&p5GN97vdi zCS+c`p=!KQ1)^iz*cl%4Wg`?U_`*4Cn>Omiwo;{vx}Iklo{jg>xo}5Hf{z?#_|i# z;c4Fqpn0>ZjN9rT)0SDwQdO+Z~$aLo~R6Xw6Bs|f=w~jfwrnNqh%6^1Fu;) z$7R7dEPWdeOZ!P`N~$bd9&~1EaUxweZQZKtefu=1FIzTmg_GByQ(0EhWT-VSx%fi) z;m4_AB;J2{?*r+3|9!exIbqTdvH-%4Fk&Pe$M%0(lA#{mylJ!U#ANu-zy2NAO+I!^1p$1auIL>0y9pEE0QP@L_18BaYG&&Sh%fs6;UCMr_n4hk z-MV!TCNJQf;S;|x8D@R)FD7CEIVc{I1!p3j6#i|dSn9;ZP9WDo|br2+5p+|JV2^-N`^P;P zmVNer89R5$ZP+>45bJSUwrmYJ!sd?jBQZ%~UYr1SWSdH7TCcbP1kFfo7PZhQhYh2yZ#Vh}d&skej;8#c(P9nO)z z{{BZ13i1=pVQDlvJQGm~GebY8vNu3Sd-PF9=y3!;8XRGi!@8JT4@^9&57W|8W&5`6 zf#U-bpI=qyX;7M`$xA%2~y zo5b$o!<{GZEnj;&e=lDp`MCgfMF+5-eKH)K|5&*eJ1^Ix-#6eGk5+*f98~M;z>f+a zssSQ^b&|b(JDg7wIR^bIxpryN}XLqN+VV?Q27&A$G z{EVb+U(Woz%S!BbDU}aYcV}@VKRRg3z+seLNR4i%E>wn6OAT(K!ufMM7#aP8R2JEoMU~@ z?s)foqASesU7R^Y=2&yxGg(Jwsw`&D*FYlMgpc6Z^95wTkgvx8tgs_bEOwHrG(Al7 z(S9sZW_YAg5a8@<&fIwgNs=(!5-0Gu#s4Z;Rw;uxYJiO}T;d_jHe7!SIeQNZtRQT>Uh(E_Nz~mCKZ2QI{JBs_94D_r6f@2 zuZidcOG$b6A~wx7;V_k^qt1*Za*C>oPp$@w<84wTxf#YvRk1M!!jFHvXPCwTr>O8q z-#+rVbC$BGw8GNw#_#v+GdsvPUwsZcvxRqw6~@0SfbTidyLO?K-R{mBNWe?sR{dXL z6>8(A;HHiyO&Wj8vrj)ERjL%=9C6HYo3;JmGDt9iekOs&6}Sl^l)}5Xd#0|PJK~(% zlW`ZZ3#_1w`{oOn`x-ApUmGO#>luU4mOv!Ou|2wUlJCZRE>kCul_9STlmX&J z4&X5v{-xlEN*R>ZK(rd*QLU@6CI97t1N1=mYnatN`oz<42lktuEt~Q0zp@o)&pN{p za-qOG+6}DrgPM;@GnfZInxcKrL0WtEE=tc8WQ-O=dUo$BQ>OhC42*JDw{C5@6;@CN zz4W?t?$&<`r}b0h%AUsLj7`?cW#FxW zVB-tdW|~)O`gLdY=1Lw+=?aYVmTF8Z>}Xdnw@tESpI%OObu1^D=aiFtJS#xxDXy0; znKF!dWAvG-b*^1RgYxAg$J3)b4QKlS>8k3U(NTYRaHow%kNGe@5=7&ZX=0VrZJsVt z9t@9_Px&I$4 z5$na=u{tFL2=s?*7i$LFwg$Cs9VfvlC_&Oj zVYD?oo}%Y$Y#3b3U^_xw+%I3Y0v$@WoO@OW-DPy@DJR2u>cT);IxD43%u#R=;1B23p<9I|jSd&R z{eO4M*=L<06DR*5Zx4GCrYMZ3AY29^`632`t5&bJNkguuxaxo771ReeG29BD zlBc6S2&8kw;fJb)iec}5pkY^E*-NiwKc%Av*wb?GPDrm2jQZ?jISZZ$VgYTRy?_t4ckjEQqP$LB^}P4t zN1BYYug;w=#UehOzWYxP>;f)>|xlJyA3+)SWpoi(O=64!-mR9Cmye(UE1G!=#j_e$0=q8GaClT+0FhyfsV*Zanx( zp}<+18~&R94~&qv64L7nd#gWKdN9IGdQ@T33eec`6I38EEscdg`PjqQLVmE$(wJYL zhaZ)xQ;qt+O4WbLa>8-PYBuc-IY^!GzCPqF`Q^9Y_3tD&5kCF&Q{{t?K2aQceeTou z5xsBOvW2w9p9OYy-@zszO*3hIJn)d3k{gBk_z?Bc9tPRReLq3bY3ufhr~2zXdGCs~ z@%k{@zB+;swRx0X6I28IPJ)u8P(;tb0EYdR1#Vzr+pT|GIeaE)aSC*0XNz7p604jD zVFHMD&4G1+D3@5|r~;3(J8nK1ijIPQ-a>2_$0*Wv2)z{8yR3xuzcp*t2wgM~=s31| z)fzJJoy&&_j~+lt=Iz&+)suLeO(qVS_GrgedPZ8?un6;iKYWWI##6` zYz{go&JceIRPFGEa>non4m#`!jdn@pmoa;HAr z46p$cz%{&sd;v~V*Oy;v!+r( zhgacQJq}SCUkBs?0-Od881gYSV+XKpcv)=I#?90(5C^atm?Zlb@6IH|FluuNJzjsJ zkiM!DgInSBt{ofbE=srl;*Uc=a*%4@ExK|i5tcDkNO6^wDE4Rtkk=^~K)SNzVQ00N zL1B*-M!<9D%Ej)S4oLb@_Bc_bP$5@3_G0to$-~R!d*~1gN(6W{J}veEiI?-9l;xl; zzx_@>^Gs^>x*sSn=YQp;vb=;?gZF<6tJCmLKBYIxSCq2GKQF)df;?K`-QeY=Z98_# zjPgE%V7XrxxWMtI-s$pj`WbkL=8*6H`{jy1uqKc2ey6VVJ1=J_Xn;=F%w^w?3F$0HJi z@w_SDre!m^M_t!+L&k{skDbV-(3hWXUcW(quwm0NUrdy|a13~z8r07(ZdftNbTCR5(j^W-<>_<)Rs0?S^yo_!X0n$q4HE& z7*2Mc?eDlL3M$v~MG=-UF|!k4x0BOZ*dec7uz+-B!7^fxZXeQy4ZqT}WlD=D4VAe5 z=M7Y*^wW}#`=?8mC2@mt_+rG=ol`~{dFNQUWvAln|Z~pH&j6g*kOX&D6kcPno zl)KuuYr`(07EpFpK6yzOpr{YtNRaovwykLzD`cf(g@o9KF|>8tc6y&zGyDzz13lOe zCFq6wr%IKSrPB3G4_|))dxTwh)dck~TlqbGI^ttG!n5a|>`J8#ZvzzP!9tsM9YvwW z7pzEn!czbyxqkR59?C}^pTBWQQ+>0V;1d=jP4p;~@C^@Ajb7I{uuM4X%Xv~&u}6x~ zYp=b`8k8$cRe$(?JK0lc+N!O~k?T2LYtF?>Mn#JhqCI=}3$uSZckY6e`;#5Ve%P^{ ze6YVUlfaph9juv&fF;Sog$v3?|DRj8i}O^(`4g~fWqvK%(^I92%YI7@b6wPyG-Df3Usfi;9vJ*;&6&SIpZQ90)1R|v|DrA* zF+5fRX`|r%p}|AO-hBs9m(K6=K6Et^aG2RaA?DO6Z6JTxr~e>o#YbyeG;1PDjj&8# zw20=I_$)8Cf5iKHEn76D_AF50V+_LX`SkguViY^ zVAfE>h`d4LW-PdNOB(jidUWkXukvO*?y|(xQ4-J`3U(F604eD3r&f)sA}CjUzlMJmRHCV8p8+&++-Q2~^|xiYXF#7G zjN{Y1^yhW}yMdhvhI_F8K)3^QJGFkTjo4Ja3ujJJ^;&glTLAv>?7mw$fX-*~q9F8i zZA|RhVHp!MJFBqs_{Ny<3Mb<`z-$~08G_0#Uvg;0Ye5&%-z(zTAFn#ABp#a0(Kvck77`Gnl*JYrDLs3yU?poe_5WlOY&Z<09~+u;GvE z12DvSyLFdk4OKkZDidDOatAgImgv;*m(>7YjD>a7N2K0DfBHlck;)>Lm$t$E}2EVvmF zj8By&`o|5Q_?C(f`thwhQa-p8z`lPl3@?Nv82dabz+jlaLv#NzFKNnJktX>J^Q z^}2_b(8xsU^Opiz7e)Uy%MEl^ZHDK9#G^Ln*_10+oNlb&Pn>{+dm+D=K@y=<5<$b; zD~~UVX6Eg!rrR;iNL>t$HGaW%kt{;bO&gTZHb{~OW!>W3-NwjWm{0(`);xc~FwF2& z8+a7wbk0U)Nt~sQy(O|mNT{7s;g+- zCHZukh`_V&4Pg@NupFW9+a=9LK-wg@#Bq_r@?E>)Z>yW+!@q8;*B0-_8df93SEld@ zHPZuhw>U`LuQc8*bp+sUp<{oRNJ7%1fa;D8M+{tCJt@6jg>xcOlTiQ`k-!v)2))~{ z8yJ>@jH^D<6(IZ!=^&2CM~O~8Url5WFD$#gn_FqinwAdfn&Dye=FY1%6?rZBJ!oJp zwf|7jxR{ z&B|xcJ|8t)KTzyX=S6={_xAIr1**J1dF@4kf`DsWf-0Z*!Y(e@CqNm0x_H&L47vn&9BeGw9>G@Hqck=tUSZx<;}_!-xkNH+M#P>H#R~Kg@HG?Do^AZp}_4#0olVdHW=F=u9YDzL0F4?Jh$j>wryL4 zEoY@=b9mt#yq_%q&hF|FC4IAn7=`-9ZJTbs5*v?Ot6D}&= zshkxD^_+$6O#+t6})bRt*SkgE>W5$0>^!jVBh-(}LCn;S$sTn`) zJm7gm#1DS_N~^aLKPW*hodi(|YWpIz*5Og(DYqy;_B{>BFM?jd#n zKN&VyEW0dSwt|K2XvrH>rxw+%8%-J5+XCzp!av&4qndSkIDoBDttvYfO~VF-4^sEu zec40xUIR{^w=ia$KmP;bjP%i>MaAeZtkEm8OAUWw>WJ^OBD|D)SI)a=%G8-OdhB=s zRKIRbs>j}m)ARIy+xBhrQBMsQII4ZW-FrMsB-+n-z*pZQc7oeyfOq?Nzy;R(?_hIO z@Td&~#PG0lRI@;@fYD>yWM6cF1f_dtWK2+?%h_-|P!+)#|mLjZDNCyZ#h( zbP|80lAvQ90T`h`h$vtXX094!kx!f5xs=`XL>6`sWZy3MlKCEc`(PvEO>b)cCS}){<`2RdZ%)CXeh-tX-Q+pO_VekscwJL zxFPlFKbR_3jTZZa9oQm0f^q;0gTUo0R^yj(jQ78M@5SZIcONy0ZO8_7vx;NQCb2Q} z>dP+*O#FeLzZV(5q3lDgcfUatU0w594P2TwisctOh$=BI|FZqY4s6LEL332QhsH48 znR#EW-@u`4c>6g?^Y%x>C58?CW}$!Q*lwv2mq&{gr7D#wP_KRiWhS1Fg{acqgb%|? zY;gImJ9E&e;X~N+f83Xnz2O%~8e~vJ4h2H7NZtqqfr@5y7z2>Z&78^%!gUj3;;^_q0;zyH{XyXF-q9S_d;u=}ntV<&%zTe*Jh zlw0w-)Tz_Zz4zTib3_KzV1KEn2m;AhF!J)ZDpV79TlIBb4!+y0G4owE-d>?fO}3kR z*~NF^gS7@bDd^U-k0$V(W_aEZ01zzqee&5DmZW_MOu20lWD2T~trugjR(oMa30 z@Pv-p3&O!eN7&=SL3+N-GqQhI_AFo5to>2aV2TW$4z95&v*YZ=0iGO`75D-L9-^(= zw~NQ~6DJuaThcF7P*b9T16cSQ%gqj8;rSXytoyNG^c6O5`GcCYcvtEi`RN$GZ$q=@ z%r_PLdBd-?X5EjpjNK1K*Nu_1f(0JtS)Ib=6isDZfS(G-xb1t{ZOwmQ>4^ zV87I3)6cHL5X2?8po>V=5WK|*$Z)2B#uc;{$daaI&(7L5+f6P%`G0C?ns4?Lif9IwL5nd=#O zd0(BE7XaU7&m^nXtP=qWxl)MZ5b{5ePgea_tzM@sY5Ft6lahu%aOApo9~`-UDY5W* z*Y;h_k*gt_&25wj9+dz|oRU4GtFU(65A@u#WoRca*=6FXsXu={zEY=5MUNJJgx0TP z*G2LYqu5v3c>xI_FnH*2@p|3<-B$F=#!b{e5C^b43UcC|r%s)cI>U`2hG+zuhdW~h!Zl#UJ`JWTVy z`j&Iwq^p0!3xR)AYkbumSUPIauneqRP;w_s6XS?#qERJ|_(qB)cM70KYF1Y)E#(BT z8{t^Eu}D1dHSlj0{$ST{_#t5O2wtVZmcQUwS{mPB@}UBFB>0%G0A4^ z%Xzi$^6@KD{t;g7TEBLsfB|oW8!?0wjHlwsZjmcVUjAihxVkc+F;}m;U9DgS26rd^ zgge*;j0*5>eT(;^R9LYvriiJC)d7kv7}JOIgY@uyXHIrTUGUlTh6he@Jg|?}7H264C!@8u9`nQV?NLOTa=gg z{mMT7ETw1W99^w4?LTzLm2LZuU38A8z!lypPixoyf8>f+Hy7BEZ0V;;ib6@t=1o|* zq?1k20uMbz@aBBu#!X(!ZAkrkb(bPfl`Kx*ESSwx=KD1Vut$#(TyXdu)F{PTz}-Uxq!a4=eez07C8d#GAjFL;x- z?%2t6f67lKN)%^5bLk}=Z!wfVPpkuo2?k;TR2Rt_CKN!Y_xERQl2bWrzF1%sb%U5n7HohX?m<2>TBnq+&&jP;s81 zAii0XSms5A4S@93F}(_HWlKxW!>dm(C>c<_e=;3pSiS;y`L{-1&iWXNmxJ$OSyRb? zrjP->y9-G8T785S4@;JK(*Gq*Nagf z==}Nfyx-THa&x0EoWDRF4dpO%T=T%;gQx;e!Y8^fd*YJ9l2RcM$(GJ9^!;c?(US%o>#Zelg9(o}=G^J1@G0HC+48 zJ-aog{r3BxG<)ts8a`y89D{(8OvldMTv@J4Ii4N!j2N?GZm$1<1)P?h|LWz?w(nQ3 zra}FCi*_cwx?leH3R5`+Yd;;8{mf1sf7ZC7(w_swll#Ot#TD^w4 zbnhb$z2`0bnntoh90cR>gXrE8w?8nGZ4mN{<(rC%5mc$qs0wf0tmv z1Vp$x{!DJsrN95-t*s5h%Qu3TPhnopYNHn}T%>L~F9+Y9KOcOTWdIB2b38HX(Wft} zr;VGo(DIclMRCt>kLOa-=8rc=t~S|SQ7KYH3E{DRYhLmr*0^jyrAw9IWl8%zL5wi4 z?`QRKhj`#~>C)e_e(b7@Z<&4V_)yG!6a zsnevU)ZEdJ9o|pXYSjtI?$^F7Ii#*Eqw z&liH;(%sVATQ1Ku?0-S{*o87rmlBOjyEMeXL9AU6$FUA+R^ez@KA3T#aANC$A3CAl z!IZ|TF-XoYt;Y{<{v*%3t z{@|^$wjDZ|;;awec)2$jaFK$I|sU-J$-3`L` zgWqi*tW4*&Cy1E<5%QSn5#J~N=B7-WseO8`fiVM>{X8>p`8-{Jg7+dY``Wd8m$|Y8 zdJ=tkf0R}d2p=&|(~u99M}iLu=>5;7GHl6?WK9#x+EJ5$lt}_8Sjd&gkJ{TxWF#Wh z-v$&o$Cj~n?%vH4e>WZsi;(j%C>Hi%5T%gbI0k{ zS)M*>bCxN=?Y!PFv`uqH%r^V&6N&ed$nPep5j#STIJ^CHICwr3(q?{ju@UTxvj8vcRuR`Pi;DDvc>LHdYfT_Z;;TuO`Gi>^@2s5n{HlV zvAW{HTl_;TN#`<)bO%(cRjDABLB9TGu|p1XG)!w9Ja~w{;`|5LBFi*3F#W`1kD4-7 ztx}O~e{o(VBg{wGOwB>g|JB!vv~q0m+FH&8#PWb64G4CINb&Mg^Wn(k6SNL*J6K^M zhcTI2-b~asYltK&3X#eq6o^p3h61vrX`JDi^9XNYHe+2hGUp1fC!Ycjr|4@(*PzMM zW^l{=<6ddlvd2aCTc^UsiR1{<2&*o*i# zfAhbz%i<9S8*K#(6rj9$@0PSzUoI>D-&97xy!PL9f%)|UH_Ssvj))&SV>z4~@f9>@ zgZtRADx3{wp}qr#xSS2bA!e9K?DPk&v>+XS*!M zNMR;ci(T+cm^ek238O6TA)N63b>n8*@XN0>ebRU~{CSCT-IbGO&YmOLWyZ)b$Llb4 zD{hh|T${`s47=Ey;_X3uk~#uTqPRy{~(|N2Xu4Z_v6+H6EA6!0T3BNT{GAZ`@!kXzc4rWUREhsHbI ztiq#E-0DsOrOYDTg3!w^y}(wrv(m#4KSZ@zNWsE(j32WjFkyHIh;7<}W_>xAeX`zS zC#2s9^p{^M>w;M)nqg+iN@~W1e{vmExmgg9DEGAmEBi<6TA5#q_5`Pa{fB%^jbiH4 z%4Ofupnkn6OSUZbK*B0klPcym<8^v{rO9hSKCIqXu#h#s@nAtsa&T^wW(G$0AP~i4 zFsg){!oRbcf8?}}g6p1V z%REC%mMmu_mcx<>?j44W{FGh4@28Ut>&lfYbjAWxD_5G9EL-U+XJ3Ok7VlCyKUI5! zP-HPV4Gh=;thtV)^Q2i?@#w#5CTBp@?d;JI8IRlh?!AX<)~HT{`}LvT-MdkZYE>yM zd#MOTNWsHQrHm8`_;G=WeFtl_Cr#p$s}i_zE# zlUzxFIqS=L@)MZ#gEM zOVh+0TI%%fT&OWU#BQD|B7Qe;@HpnvPpEN&dNT8Wv{+GU8XH5Od^}j3L<)5CD~MNx zZYmA`}RB3J5oG@lE%PPvj&11o3-LY+8xH zGw%c0qXf@ucJ0ode=R#@VMUQU(x&w&ShrR*eY@y8I(qcDM;amk({Aukw|D+c z7>+oAwJXP}>Me-_*e$=&40Zs!cI8rfhCQ^8n>fi1M0m!T2&i{fhtXOv8rZCHtjy-d zO`J?sYu2GE(RJv@^*__Y59Mbs_RpKDfFs7VJfk)S>$5q{f3pv%^t5R_)4ah_A zG#AdCjF$u0fAs0o(>L>G)8^lPGkG@7lqnO9|NK+hx@{Ycn=n~sd;9k77meFfr_WF$ zb~ahQ+#6J_az(oLo_lEbo;@^i$~5|!6_gN=R;xxex;u9+I>Uat7JmIL&G>ST#6!Sl z;3F;nvY*3c%T~~cPe*%&rRWAnz|$s~q0Uqv4x2^!-JV!}nF|FuwC8RF34sV}nSs_z?vTVCT%7Mp<&? z(kCx0++=X99KfOzxCNd-=FRbzz4l!D%4Q^(7Z!hc?*? zJ4rBbuOKdIKFl7^QRnKlKcE&@07I@`yB_uIJAfH;TFTCQTqTM>L5r6x<@1}<>A7di zQ2REm=r2BEGWLrJ;+4KFh2CcRjyKTvekmLI6QO`71>C|;G`KCRLJ+O$r&h~6 zVT4~yaxy&FqJv8wm*E8g&LOAzhARX@~S^)|2jvVx^|cS@FmMv(CisgWHyM|ZpY4DDFq+> zxr2`bWngoB72YY&g1?Z=6rU;m6n(#H4c9Y@LEWNXHg2R6Pd*{S?X_24e-iNHc+cBS z-w&C-#p%GogR1KAxq{P1Lhvbo(W)Mx$QE@ALJzAz;2Y-IofFe_jIXsH^6Me9I* z73Jj?V{lT&@dnFS9{1~if8HR+X`bfU-J``Gp;4p9Qqx8a2(!H>9)C>Eir$?&H>Kuf z`8|8~x{B}Kt=-?pGd(5njO|ewK?j7b+jqED4sdQ1wq%=eJ@`RMYYflk*0I1wfXO}V zHzQoQaFI@)JSEG#c&^RIbg;Y&d{nsWt{gI(L);EtVpU-~&wlYke<)M>X}Nwn;$yk0 zKlk2yueyy~BNSj(;N}}fMK%#?!Y?@8AQaWDwsn#`M4$W1igbuDqJX>ep;M|oI#&av z^Wq7kH+NoFdK_=|^33PPjhj^N_1EaTCCfxX2%s>l8O+OTfBw0ZzUA2tf(r${|KEmR zs0hzgYSwEYGdbX#e~p!$V15q!dJuM_cs~kDr4WL{2Ae%-0A9*eDRD8tYG{C!O_r-A zgW(UBL?Kj~G;AP-6V|T#flBhMs7j@Zl#>Ms1S0m)&?X2s71aIFqsLT~?ez$!U%!5X z+P&L~jrZv0_>y5sy0+E{t1so^< zF{mBq!c5N)Bmrr`;~@s!w)*r_pI3QBnzGLZ@x}L zhJPZA93^t*%t=r3tgC*#IywNYaeuJ@L8!@lV++4oWPsIyVXbCZkL$2Tib9HBSS~kt zM7x2P6<>YjMcTG~C!ITYo__q_&xGYc2r5|i!E9?Ye=nQ9`NnI6eZ&jA9|<9hGpCz> z|6P<=j4+M&U(TIRoA_Mm=ud`G&yTuL%Qo!@N;btAr;(qH5n(HP+`PQ0Lav;5(MMf6 z(MH~zeBt@$L>UEEiLT$cE?F|OLQDP!9-z9lYf_Fov(tWFQpGam#!Z{)wO3!JGiT4z zaTb7ee^`?_Q>Kip__M)=CT{vdNhcB_6mX`1F61ymbuDjdxpjHxTr><*MBpVS2_A;; zAdV{Asl@wNQ3MT1 zEEQhAhVl?tztGNId*~GwS}E9ET>n8s8P;94u=EN|m;doB4gzmI3pxZmLlFMKvr%YF zBJ}Pzkh*m0DEoa;DWc>U&5&We3C0%gyEm=mxuy#wJlx>6VRh&x3r)=IKI+w%h79OS ze;<9=i3WWZF^q@hR-~Td8HPj)yl{E+c`O00e|BPH>hK2oThzAw2dq-Q&(yZ;*|XAwF`tR5 z_VyjSOlfuxrJpWIb!*k67VoyROZFKzb>diIdKj@MM1UH2k8Yh8B?| zwYGHtm#)0ubq8w>PM$oSCQY3o`BZnr!_$1&ze=SF)Ui`H78fk!c;uEfOJ*9_f3GLi zsZ*CKR;$6YogKFD1C7kdvlS>=U|G;M$ew{nTuH{j3THN=}7MOkZze-}-RoyZC}p`XK_uZDmwO)XUd#eJ~k^hu!ni` z)=e3OThr{G!ML(!&1{$CS6udN*}NX;^$SP z09?GX)~gjQntH$mGlW_kmc4Zmy> zTv9MC)IudJbI5yS^sv;aLdHxPsN8F>@d4{eoUUTS@e1U_n;Z5UI7F6H!;$(CoWq|2 z#?N1tG)0&p);+5;wo>vl(*W}@RJJ)q$u zwX;+8>ei(CywQ)Pq^;X_Q1@Pa=!kC8ta+1I-k+&L>3At{e-AGy_26mwE?xE99!J(9 z{W0kO1|j}Ki#tFbTd&v*+OE)TGR!yQzt*dv0=9=29; zT{b>|^jhKCF#pW%WKajmirbFVM=>YxP)_=B>scrJ-L1Rmy_)35b7? z$e_T$a!Vv6V+v@jA<1@8bgAnzhn+v&tPC zQ!vxfHJ!;k^pSqAJ^U>&c`n#QsMTNN>7xfi^=x0{#OXBkkhb}h<~F#~O5T>$K@ zD;dzXb#qZh6VdK8!*i^a6Clx}Yg8p1MxM)uXZ!HsV$Aq_2i{ZS<U zo`0?kHHmE?7Bo~jsFasw{5~E&lKS=^!~(UNe2&_^x_N8Dv*v>;L^BZ3}t5_{QNV|@O=Y%`Ngt4P$52FR+Oq#tRM;u z^&2&l1KVnXmojB4s>JK=xp_Ha)bJsE6d)_t;oQL;5r`y2C=j85cM2FkE91TQQb{6J ze*~dx56^%SCt%B@ar1X+5G$qKqn|rfIiOq>%SPmT_8vf$s@39SO4sOJotMM5@-7~f zkL5`KT+H?5)eU89Hx2JY-*?|VVsxTHag z=jGtLO&d3488C<{R*9y6c<;QOE(0EWfAkTm&YI+1d-QRb%i7f}RBQ0T+xR@z zkKA!i(P$Q?Y6BXbATuw?PMkD_e?Ddl9cgR~eEX_9Ug`4TTRKj>0HSc1ag%tr_x1u1 z)CGZrB3-z2NsN&7VAu@Nz9{3tS$8HGRD;*T#w}0tREk`&0=9MgcHx1@^V#R)c$T-_ zl?Q?hD_LjH{Y5)j?;whmtq^#TRQT$m#Y;s7Oq)529(+KP0j|O}H^R$Ve_tdoYe8$g zyf^jJdAac2tvlQ@fcb7=T?Rb(-~)o!Dz+=mz+KAFdV=k>0!Gm*g=|Gj9Bqm6Y+S&&O#yfMF*}yZltM^^na>S1sI`?maquQK{SBW4dHVB6e5Cq-yx!JTSs0 zAvS}4q1ee{^5A)*>_7P_78wIkoW?L8;zrj;o!WppD0`hDCenhJ#HMMnmLS-1^ZfsVrS5VWWqlV&x!9SkvJvv%yH2<$tY$A3xH=(`!-7D}F&(%kVum z1K5wivc~2Qe5GYdCFa>DghvMYBoh;{rfH7S%TMceDhNfP{B1a| zJvXNL)6?^Gv;MuIx>}A&7gaZh#gIC=26gvv8f`5+la}Dr^+#|VTrp+q_ip`_4xbnN zvV4M8U9EgW_L5hvP>%Q}bE2W5_rN5(AM2vCDXH4#D^sAi)cp zN*>*lg>+_R@cicctA}Sbs@xkcS#`;GxKDeN8!oE)q6>83DzQ{MWt-Bk;ALh@(%~N(_pG(HqK9%OsM6|K=O$!_WRA^lyyqsQM79gDKSWTY;_zFoFo;-F zrd=pC9&?@U?lOT=SpD(fZ~li5@E-o>qw=r0%L^Kj$)eOg4Mn;J%5+y+vA~ zBiwrZ6W6ov9RUGvH7WJ_4#&;gDWZ&y8MntH%XkWp4ugxZ(n7KRdY#IPR@0(Qh~OEA zff6RUu(D${jHOa+6htJTL?)vQQJ0V0rk21c%gO7sR}8~dUXOnu{OY;ywI!JL)ij{| zyMHKy&Mo#ud`UPwc^xUN}8nmfXI83(RL+r-At{>i2!y z6wdOS*ien53Gm}GR6(1^BHy?cLWl)k6RW*%A!y+vCMM*qHdY=uwVY_{KB+c&Ql9LH zL-TfLq8CLaoyi85BxYa)1*Y1Yk{TUnad>0}tR4-oEiEN-JfEOWuq?H<-o{^?gnKq2 zc8uoy1ekW?NJn}{UR4wmb6}%&RswqApI$*PjbTiqV-HhR$6+2J&YJ+& zQYHveqzD4Q0yWi96(}!xR`i8di)qncTp=oc@)Vv3bdvi9jV1=NNo+Wq8b;ssJc&;B ziUI8!@_e8yx{}a1iF>^_>AX^p<#+5Qxt~B=kO(T> zJGUVT!Uj!_tKj0+=n`&?cy}hQrQL%ttfo51Qu^98T?p_s(0Y~Q`h0303q6TL*G&`^ z66EJg4uPMX1Q{((8vhj8%#{|L|6(EhaK3)R9CF|-x8!^{Q3DP}$4EUYp~#D{WPrJO zT~Q#e=|}pfWa-nA?PNUiVT86mGSSER@LyFvwprIm@osOgk(w*@-kw)mRF3acHVCKD zI!idxLDEQPpFmJAbYjA-=zjHs>s|~~VN$qvsQf7<9Y@%4m6(b->Snn}JpNYTE`W?KTd%Y?=~Tc#uOLL?>J8B5*KOqE}H@*!t-+R~A;6)Olo`XHrDp7iYvT$xP&< zDK;(}cP-*X%~@+#&2`3{mE}gx6pV7;ygZD={)$T_@R5H!;FTNB4G4j4p9+~rLn~xk zJyTSSB@)|#F+o9pgyV?ga|yq+R_5RrOl?$jWEXW>pl!q{5t9YorAV}hNdOMl|Iq|J zGCu*RC|R!X3dP8dG%+-x+nf^3%oPrc<3GXmzT=3`KW?Q*dPC7Ga?QXxUKOM07=C(0 z3&>u#>K;*r7pAwL?TCh-b$ca_f|0VqFA?oS>Ea7=c5$g?(+2Rfo=g$#w=1(v;KxfIVS_>fczG>+iQyQk2{LNO!qXR!5&`OGBf!n-TorgJO$+J@K%|# z4G#K}E<*P5A0LmiAFoz@wy6m1Q@_xtm+>=ab!x4HR5tn|7DLd&)n3jt?f-n$qFjqw zG|>#K&Fu*z6v~%n0YDh6!Cn3r?uhd} zj8XH#BysK{iJ|Dfgec*-u8%njL%o3xd+GDAn)c)P599htFCt6fuG1NUDM>tS1-tJ5 zi`e-*!$q^kWwB+FU?YaV$Uw&r!exJvIq|~^(jS7({%5^V2E{bYx?=2Lll9?eZ+#0F z+hb(-?}afOngu}oVvWJ=DFB>%3oGawsnmH|k2^9*n<9gYPk)dYpfMb%^08K>WXF?) zRl7>wR;D>D*7}BtOc^VjQmYbO%mZ2IVQ00U&L;;SeW@p4W9wk}P|B5~Ghtaz+h6X} zGhoxmgQw$Z2y(>;Ecd=FO~u~Te<{i7BaN>i9zJ|Q%mYvpU6eW8Pz*GGy4dd?ZkJu2 z{XQpes;87^)V@NKzx-=?tW0A=Yl?H6WEK{`_)o2Wd@Lh1zC>}d(wEnh1EO{+B6=x| zXtY~@Qi$+tf|Y8?*t!3jg=+uluQOFUV{3@bv5y&up(jq_7tcUAhw&OK^?zIrEPUoZ z`iNbYhCV=J>oSZ26ul}34~k0&9xClhvJDmU(NST6Ej{EmNh5?Px@D+JoQbLos04lM zzq9w4vRhcJ7!DE^aniSkK7X;4@mxL7>(;-P%U;Z<%!oR$Z$$N1o-sdi+`j zgtCwK5O7vz@k?zobDp@&>Mmloi?E4?i;z|g)i7~Z7%-w9_f9Loj(IOLbAruZpCCbr zja`L@`Ha=vPP3I&Yk9WNBow5Q)+oAW5?gQbB7AJKAMTjoNw)Y!<4GU&$1-h6hs)EoEORyAu)aR4Ls`y8yXKUTV66(*)F`lEo0$F2p8|!8iFv`o z&;!CrY|OvmxG)UFy!zFUj2S7ea1qu-z_o1Xfzws{K<>ZQzhSp@xSKAMkK%$mXd6!0{ zaA~RfeSrOI=X+l{A8ltmyq&LdS08B;H(GB(2v3sT53P*itT*mqr5+7D+Y3wVH$;C9Z?eVCLk`3&ZWTuNHsmULU*b z-`#RhS7BNjZtCwSo?T8;%x!FKq4f=CF%mKhm=*GAI`+}x5f4V>b^N^nEc&MWYZcvZ zmmL3DI6znjInsY?$1*NI6nJdq#@qX{UnxBd8n@iSN&D&v$?U$dXZ#7bM5jtHOSp#x zwhV<8!g<>+Wt%p4P~%mWCKrx0^it}`orSAQ7kM-FXY4_OxbHXQKG8lGQE1Ls1ErZ_ z!LKdhn~aCuHRaGozsJ1AfCc9D;PLFtr{PTc*Yj5=h?DVQ`Xpq>a)`roNCa~C-&P_6 zXY5n6Jmr+u9s;#pEH@12Z47($+-D>x0bQ*Q2Vd06v_czgH!%)5cl0zWRQ)4xXyuOC z@{8KN9;O4TIgs*gq%$h&Ww!Pa#R7nD*6cc&o>Dh@W^9beZ zm}=q*L(fzOMO#)IlVxe4;lkeDIF8&GB_p_Ihq*&&m3hK3`V3h8Hjm)~3<9|&1T-Yw zZ!gQ&^ozI`izg`Xz}tJfm(uPM9-VG8`hT2SX;06^jdCxo`?>BPKbOZL4BqZKx6S}& zBRH}~lv|`0`q|a}(dk+V1La>t?kzhz?wHW|kdkE3u8<~cPv%{WVOY{8h8RDTi7nGl9112#rOURov8jk*~OsdPya;Po<$=reB8Rc4aGx3W2k~+R0 zfmRE)!gt2$Qz=*R@S`b{NuD97B)sBZqtm8K8_ewQG-3Q=K}CL?`SR%lw98mA_*7;# zjtG{s+{~?mF#1c$;e`0T9n0IRep78M80L6ibUNwvCYG;LSSbH23be7<&e{fWK1jBU zt#IIFhpd8ta*<0dz6L#`(B{^Qg(&Zj_m}KSfmzQR+)opV=exV~?(43&(Ih-kf+Sz# ze6>H==;2KNpxWxF$TSuPR-2unsg{a65(s(H+4@VK4+dJ&6XJDUTX1e5g&2>Vw?EGu z&lZj2Iq==ofv$1n38pDxuuRX^0n4`!-@@%YL5d8Zu6(#rf79a*8>5Q`UZ*_1<1qtV z^zfg#!AOy;`5IPOlP5J@YdUJ#SCqHU;|mDCPO*?FwzaicOpf{Y2cg$%h}%e;WA0-I z8UG>9>9a7OY7Mgr(t{!{*`^R74g2w%ffw`K-*RgD!Cyx7OJtXU4q1< z{lc4o#-_h$l$7;~r#OyF9O}HW-jP-kqbdeaiEfqr{+>Q5y^y2J<2(}*P57SzG(TLW zQ0B_(e0cPmoEEL|GxGIXyDat3ovT5QmiPKMafMUWY~L^ef3ON?2oKN$t%&HA$~n|9M+T zv|NY8kVsm6F%lbCYdS2~`PH}6ZcUDvUnAtKaUw@vLPpH1uPgGjGqyP&khRtSwC31%Qp37dp5$moOwf&tBb|0kP;wg-c$ht zcASG>C(j+Gp0ln0YHEs`Udl8+^1#($Wt+E_+fRfD9&ZiF67X-=CzAiTEW%1?%+zCq z-f0LC3?%a;V`A%l;82yaN!I0_$W9BZO^JhkfOCAX$lw;;BSuI&sf|sKLG}hQ>aNKY_=Y$}Gi>OR#j>pEp##x3JxinXQtXo}NqiggIBVeP#N`o$1Sj z-mS<>BfDqXhc75F+Bj32fOFm0p*I(2n)uz=T@Zow_6+nzCpO-i`{ z(SG>B{VmNGRsN#Se+Q1u9=+uV*Ps@VAR?mo-0!btjqk0^Ew}$Cyv>5=`LB**GgXCp zhC{F;j{Z>3U!C>|Fs_&yEiazQTIF}&fSgA3DSJ(?2djqmrN?r6L$#3DBUb9ICPr94 z^5_!jE$@bKWxaKGGWZwE)zxg8&-dK%TXHtUkF2C!hR{)mIXtX&a;nt#A7BBt2^ZC_ z3WyAPw!)z}D&erHx}K@5^(TAfthK-W_Lkb}@U6F#rZ132==5>DmLb%?bgQ~ztufPH zVJ><#@d~&@D{fJJSgl+j4p&p6o?8R95I2krqgNDI$me3+&Q==y^ks3fh${|FTeG=r zT|~B;Z5IQF4E#rr;SHEvueCA`;Xf+q^foN5KK{KmcLZaVFU9(g zBzgr7_Rhv2XxXigc|U3UV{pe07r*CC<|hVKQLTTo?YYq=OK;F12*@;v+oqQ2R~+4GJo-8yQeO2)vb z3M+FH*OzT*OsTv!g+!n))|x)LX1Sufm*?~^67CV9nTLPSX*ats8Y4Oe5kF=S{(r)F>|V8CyNy_ zcUFt}$lg>os|#nh7-dbQJU?}(zR|4s!v6OU>zq0NxJJc@OiaJpNN=FW4i&~*EhZC^Os zn6d&;SyVb=&SIF%R_^e(GC}mZ=hDep^Vg9>xo_)U4>R&f*pErt9QJaFo?BO>^ocf` zHSybJ%%=cVBCyLb{rlDT&Z_F{j?1zCp$wsO?XOcfsTkgqI`JEHE7+^iaLuG727AAo zhk$Q6_1oz7B9R_TrRrBc@z%}HOE>$m$e^tDm%k(0_P#G?XjV(je`!j#C{zpgeqSK= zo-dU|<`>ANMwka4V9+GkrZri~R^yp)vD7TMwhAVHV1bx{VbPrvm^x2kdGCpPctGj` z|2L%-x=QH3`G}-lA)V9|S>L2a(oBESRFT^74=~c;u)T)+(<;7jP7*Ci@JQ z)z%8QK3*#Sn`;Oig1~f04>glFF2Sjq+gdyCc!MkEyNS}{W!CWKqPSTXLh;5mTxqmZ zP%BfNnR5m*lGvl~B(%(6e;-ZT$ni2Ox|;uwQtsj@7m`hE($wQe8rQnSRx2iw2X{UG za7H0RCKYFS?>KItjnGaqcU%$?&p+5V+O5je$ZJm(ASD-XZspEZTgqNF`$hfdsxgZu zsNKSo!!Dc4A7#i`Y4Xu9>;l=e^QJ*9HElrvNid2&IMN-EQAS+hw!+Jg)g0`vX?+7z zWz6kIbV*D;ymT+G&fW($Sa;`q(iZ;WTQl_tyFGO=5o-9j4E@K=VR_oVJjPGnc)D0d zg;$VG(;CemzFVIt=suVeXek2yl09fK%s*|_RnswV-~HqB`=LqJWuA*;=VUHV`R|1X zxEPVi6TPnq(+BX21tL_mh7P(Dy7U&zS7x>1c+9 zXU?l;>k&y)Tf(1JX8L>?h2^-Ju1QQnaxY4=W!bAcxw%J~KQOL6os&y(w9#e;Q}cr#c(j{g2?wXx^xE%98!VUQ2$1xfyqGhb z*s4Y|f-{}aC@-CZ{$red#*z=mKOz!0{~UAQe1iXC0BUOk-CR$Q-sN9omxUWv7)Z^Q zL2TO2d`Wt~B^1Xll}rcoxF*r1YJ2eUi7iId6d1DN!R2 zxUQ;X>ewt9zTOY+=6-vg!>=orN}h0v!RYfPLE#@%=R2qs_D)JJ4nZZPwKP<}bh#vD z4i-@cJMcgSjG{)5YHAe=1X(=%mpY8$D)etCx@Mc1sg!~d?J;<7DI}0aeHVkmRV-I4 ziB~p?Ec9d+et&39$VLONHHnMwq)9E86K)sFqsW-L6rbWg!TiBX1s>ZHl{&v~;f`%B zsSgGjE@WXD8eL?2q9F<7G6rRjUGqEg`R?Rr_(a!g%Kqg32pn^7nqF}wxOh6facuka zP>clec&nck{C0=yv96FsNmuYye))9uf*qd)<+Y`Z3Q9NF)~+v=Yw)n0kINQEe?yA6 z7`-Dhv0crY9p5nM3D^omtKR74j78H=W*>J=^MYi*a~QDX8>U|QnP2j_oxx{E7_Pc#{ z;SeW`>))$bMc&9#N=i{7lyrdK1DlyUVb^Gj`yT&`{0=hoi7H_}2_Z5FK%2`CQ8-;3%l+j?g;m{$*DD&`Z z!960ElK>@`aINvZlSIh&Jq`1pBtB9+_Bwk8$M*TN+4bMQazxt-+omReJfBeEGAoXI z;~sh}R_Wzlscvw#cz`DQ$w|M@2=Qih6ya9>9PjI*d83TDVBs!;(O0}O@G0>|&eQ}VwisDAr8lB8<;9WuVF*2KO zuW{fbWNQ_TA~z1QQ3H#) zfbX87Di8!~3x?rhhY+%ho*|c{Vi+lPEY~eP<=`i*udN1Zg+0DWKyF<@Ywu?$yT7J> zNPhd0SJzH$9N3tv2{V_1`f%&1Ttt!fs~vjaoN{pkoKuGX@IXQad83EWp+lm3z}o!H zIUu?x4|HCl4hbCAUPxE@S^l!ZoS&1-6zYv;kwH9K{nmb80=VP`F$Gldcli zN6zbi5lww?2i6;Qci-IJXiR#Nt;8_#{0Lg-^>tO50mXsM|_@dbmIG zr_B~}BR;&LJDua#{^931vxh-o{KRfrz|(0OX;O%um8?bEGzk=X7Ll^^%|DqrCGX~1 ztUSgsYI4IYj{h?h2~*CfA7L8u(Y)RanE{q!QiC`ct5ljf8d7scwtAi!4; zAecPA6N+}uY;|}zp_fDA^Nc7*MD2(8ax1gD|E6zPF%$FC%ChoX``S?X7}eW9js%9%c%#oV?#x-As3CfsJ z;L9-Rw7onyg7>%g?__|jCTSf;3gmH#qsO-5tS36i=k(WyW($aE^|5ejt;gEk8^dP{ z@BU3u9k6FS0kz+LT<$=n&!zQPq5I5Hq1%uI20LW71a6iU&cUt^6W5ES8cZ9XVMeHT zOnvvr#IeeVKdD}h2(oD!;>!Eytrt}Cuon9{vHE}d;-eD1R)ND$dD5Z1Ic#RWOQcrf85v$63Z6%SuDT8{WP!*#G~+ZU2(R#2GF36+ zPd(j?E1>R>1)$3qbk5`~B|4=Y7JW>EpIY!OG42=XN)?ZvY2vzc^BWENvp(=L&7PsH zo9{rb&Ek$zp;;3^z+o+8uH~qJ)ItEhBi15)pHs9AX2dTX`%}_Tz~r(k-E%rjyigl9 z8{cW~yycWM``~1D=TRJ}KLQ&=r(GL{z@4v)jotMXK)wQ_zp}8^v0j=#SYe4`ovb7A zR3Mu}@AKo+nz>lW#v~+1m=esaNx)yG99=%(#@40?Yb&7PF5|`2zh(lijlc_#l zo#`_H0jLwPG0M5`g)J+egGs)u4RES?#7y|wuq}y0eukb|u1mxIlDHou)L7&HJn?V( z{@?!+#+1|M{=bYzK{0&;dnk<^HED?&pS!~$&nFIPJL@${zw(Syv3LDl#Ln=O+Ud=J z^mrIBNa3Om@cm6gMlagNz+?I>hw_JMFDMz`2J(DOM93cgUB7Xj>&^+_r=)?6 zpd5(*2-;i+XwaZg^3)Zt6B0?*;}=zyjk4w;j(V)HDgOFf`Hrb=cy*YYh~IGb%F8{E zRyfB-;KL2gwIY25Rb6w)5}D<;3iod5t4`75>V-gXV?PFOXi~>80Wa)eTbb#@+n@Cw zos@A++~^+gRe2b;Eti=e#rHGU3$QpCUy@?ovEB7Ww@t61t7? zVvLPzf`j=lC5!yOlx*YRfkgl8$VxSCQtELg2Q<93*I-@rOVi#sYi!m;5EsM@Q?X)E zX;i9ONoG}|gt>+GtWW+ zzNsur6QVzqjs@8D)Wk1q*JjYX*9-pxJK?dqwlvK=5tBUM^ByD2zVzC;REz{qs%aM! z90YOVDIxP+>zW%2FIOT*5jWYaW@$HCN-=5<*{-)lzt6^e+8c_^1Ye(NRPw2k4JU&m zo-H`?ga|1CHjF@lYaJE4&7L_pNQ`2+cC&r0o3sp-s{J(!ll-)vy12D|#^r9}?rrFf zx7&^2;1X>$*K{c5z2pPYu=kJ{O5F2|Nj8?Cr<_PCSbC|lt;Z?+KHsM0!ptBd9uN+h z16<>or>9|iN=qYp3^DG;Q9XvHhx+wf*bL+;iT)s(YXR0W29TDl)aa%XKPsmdHyQ5Z zMl+#zr{GYD;z;ENu*?=w{-ucApDtU%rMIPB?dxXU5$D5rfB*0m9FI{dRuWmMvBvdj zGWnY_7VvB%vOd#6*m6Vsi=C`XvzWtgx$vMjc9`0td~yx@ir1&@`>qW$Y9HQIQBgA; z-B_sRMH|4z=Y?A(Y%CdVn5^*EN7U=2!l~ZoN+Tigjl7hl+JKpWW5@W82FD9LUzYo* z=DL%1P$Bz#v{oOs)*lE-LOjg4#a?Xp4pPTZbi+Us7gL#A=Y@OZmN1}hftAnVjCA%; z8qH1MBS4j{t5f9J7yYqVDR9~(&-O1|_hHkESr2G-P)5u@SnlM#b(UzRG}#f9TB%BtGo1=19F4 z#R6&+f0m;^JGwn_XZb5hw#Uxb0JeoA-mj`vMwo6=?8+JEf`gSAQ6L=q4AC@pOFaRE zKuo}+RmteRr>X>lRQ4%yz9|XL1#%#$U4qBsV5-QR%d9e{ww--^JciEPJ(wpI z%rn)vCUK}%d^2_9*PzkVGD7S^<8i<$Yx$J4oDJS3~Mnm~!b zjcY`SL9*}Dz6N-7&00(P*JT-zU_ikggSBQ`Q&W~%tG1cHO6vtGkxn^-s=BOH;?%kf zOk%E~X$|(&CGi5v!CM z*Vl#T3jf0O5W^0xK&TQ@K*9y1ZH9QF`yckPd~Gf3hYcN6Ok+w^-l-`;-t|wXbR_du@uO!L!^%e)A6zm6QG?=}D5N*<~Aw7ng`Ea~VUt9#58r zlOP)25I`(Jqe=vU<%)gH>QdE}Y z{0z><5v6Q?-VqCjYT~`OIL42mW;3b9Db%Vnn}u#N0$Shl1PLijABef7+r5smXt`mv zyQ*8=W20AorE>>U-DFkcHM^F}oJi@%1;$}%Ji`@S=RnE~C25`@s-kO-!X=`ULl<76 z*0*!gc>d2=DyT`+PiKXjdzjK&nDvEdUv0~Ftu~m0N+^-)a6B;*h3!MuTp{zSl?Ob# z%6J6E`Fx(s%jCj$VrIqA1f{_Ow&do`<=>VQtxiM8*Hr1)FE=q2>>>D6kQqoDEZm`q z(&<8y@0D?C4_)sQGy9u$ix(2NmttrUi#Om}e8GKNAO1P0XJRgNUOy9TmC&2Hbq+Zc zCmbsWTYo6)Xpp7pG|I^al?XVBrOZ9tqJgQDcD=2lp0|h1B0J)$l~B`c@5f6F8SXAo zw5RGtFKYS@@tWT4Ua4O#(2u!d+JZ2q|AlOivQ_7LeXcV}o4!S0RC>BZrAQ4YhJNb> zvDBVt{{}8ZW%)8p5s^`q+rx{HPSHbYo-e7Cabx9#IDjWHnD{Y>!Qh%~xd(R)Xh_K( zbLcr4762N$3E!i^8ip9a*>d@z44&c@3>>FB{V%oGV$a^<8Ya;9c=yzn)(mp0v(-eB+%Z-Sk*T8zV>eZ#X@zl-BvFIe&|H8NFz3&F$ zb^C|_hLD^Ij*#hfzn9xC@EH`X!;`_w(<#5o{Oe^a{x7xj*)Dxd{nss6v{l5+Wo=2q z=ca1~J~`;yuhc1@)FLZo)vV??=dR82ZT<`SLdk{3RRWp7nC}_-c8hgE2XDeGu z+}{Q(`aOR`E-riFyG3()+?(N!Hh8S{kt8HApG=CbH5n0gKYF1;#8RES5lV?suXqxX z0EX|V1xMk`y}?t&feG!rqq)?&xw}05I9O_5=L5#M%%Z|A)2dyj!xAd8B)Bb@gV287 zverM@smHcErenwPgQG4-ul$F}TJ(7DWVOpWJ#mM>q+bRQ( zcHWacQ96V___ghe6R@@iFOk^O~I!z6k|P@jk?5YoVPw2MoG7KlsU0rq3y8LY}jGH zK8S-!=;_wE#qEqhVA!j@LWdnFczJ9_fZkTGEVKXUwvjpf!W>w*-91M3^CcOq_L>>{Wy~U!@XXdd2WD-b9boa%9at$mKcWB1pw1 zwlh)C|bviu!MCP+4$apxqOxv#{xD%^-dW_RkO`C>(#2On(MY9W=~WVtqC? z0sJ{f?aUViHqfxrc!1(g&Q||s45-}zsp=JzAky*|rP&gTZBcgn1>})93S2Z(mv8V_ z9%zc41goC|rrriquo@q(a6F7wudAgpmeVJ;$D$@^_L^f^^IB=O$S+H4x&qN7?1#p^ zT4*0%iSB#1W`+rJXj6IPCco=9;=^z25<&mIc0r5Q(=_z&LjHnGyrhVw0*i_-`B4kC z27C7p9olJ7LC@r`TK7mCC7X4`J7ID(BZw4)m0ZCH(g%#jCmvQCu*clZ5a9+6V8qQp~%LHaj^p{N2#BeZ^@uLsq# zC(xyv8hBXnCiiqJrA;A?R38MJgpbh#7ry^SOQ9e-tV?NqIOQPj4fDc0TfrFPTrxJ( zDD#=<`msxQlwqZeOvFZMEvzaX9Ny65!B2l1_-^Lf)s6moWsy}-+?r@-6v}0(yWw)! z?VEMFiGU^624lr7AZg~YecNc`phjBGArmPj1q={ZMo91^R*S;l*QvC32!1JYBJf=z z>|-_xAZF^ge27jZR^2Rk9mTK_F~>HMT-#Md_*lV;(6x}NzT}$jd+c%@dSt^w31gGf zTLOKJNly1pP{Z>4%*HPfzPeTFhe2Hi6-2Ov?jKu!b!cSMD&Nq-g$FCD5oJ-M}6V@>+3AW3dnXkIfMno z4=CT-q!w8^eCQ{Hk3)nw|HjW!fx50Wp6qBB`~jEkNh6yixAQuihCAR7)=nz$EuX~* zf#{C4md53D#Tsq2`iSLp!PNoO0Mn-twfal}M(?S20l^Orw&{th`4Vh-`i_q*x6?Iw zmKe@NH22N6g8PH|qyjVJVXY@UOOf<#XtmPIq-2S&APay=HEgEVJVDmzv~1=ijww;1H!?hq~8KxXjyL3NQ zMWx;iKbu)a?_)-=-ck5?p8 za9Cah;NiMR)+5!yJ_gR3p+y*3o|10BZg}syrm;BI5>(=ggzRn5ctGNifPR>i(|7&( zwH(djvvU#ZeN7Qb!PG5xd3R>v3euSm4bbv}PGW-zSQKk(H{M;+Ok$^G`**)F+O2Ug zrbi>x%IOE^pS*nKWYDHiu-fe!annDVX&4{{M08>1CssNUB>@9qhmTI3`E~Wq zCFNLaB3eVKP40jQ=db6>-h#N+rPpTUui`4R$Sg3eK`%eIZGPm**c89vao7UO0|~jH zo9>@&*9lB|KWnz;D$MOU5j+FvGz|3@JED>M9%`cZylsR@oFE0iWz*_KY*??yjlb$0 zXc>#}Iz6(ue!{QbgkC+*l~Bn{E@}_F|H^z}x3zzldLfExBAqP~Wlht8#BHm?Rb-V& zpZXM|2XoGSBO4$dW0oG~xs(kopW?@t*iTln?m8d3cLW}u@*v@S4<6w-+AE2ViaG%K zj3#YrRSQgnggJjR^3d_CF$b$Y+`jjMLF1hQq|=QN}5%GqJ`+tqxJOuMWt69uZkW*4a5jYRyI!OtS$mbvf1c*O|!5 zAIHnh9Nup}{NCUMnCmy#l|^S6AiEp#`U5|?lz{2XRd?&bmW!_x25hNrgq3Olzv|18 z3TIJPrkcqeh9yl$1*PN(`)BBx6BAGSHSshmbYo8lJqF*EG zd9RloDTzwR@V>8!Lj_p*K`09W7|KtM@?$xd)roMNVg7UBa(0B}I|K#Vd6j;~Z87v~ zd-@8~Lmu*owhKYTEFiPAYRu_SYQ{?n^>YF)Yl%QX5^~gZwNI7(OFG{$4`jh*gd#As zT$AVEMmq4T34^_8&C7DE`HEG>|E0QiY!c&pk%HL6hCAALFo0bAJy0QoSKfA6xvnZF z!xLDN-`%fn(9Ndi0fSOokv>aNxdZMT=^fHur_)l!yy-NjBRxy(77~A)Suml5x0+q& z%(HK7O0&n*UquKm!J_8j8DD&)*4h7LOFw@{%TW;REpPY-!u3s4kI5%`7P41>BeMK% z-odXpzKd1B`WDmxwWdR6&Zvz7Vs6*RYAY%PeMG(<5`(NH3!Ex+w_lDqs=L`+zr$xT zvYgKWeqm%ec<5hqi{Y}gu8Y*j&0Dq*oJDG_t7`IpU~QT(s;5L_NEUJV3XeDgC_fn`vAEV;BG&}JyhlYmQTcqt zyBs@Gj%brTY-bHsUUnX(u>as+U)=P8dld{-yFkmA4^<#oC{`A0XISosd9jHmB-G^& z(+-NsGdJw3a~Wnm?U&cs&&S_=eR>bg{#yGl8vo?tdh>-?*I}5m$olT&f-$~GAy6%1 za{hi3pokGTXR0Fc!)Ir{_l0y1R(cBlc=rw>ivM>AJQ9Ubog7 zFOAcQhxwO_>ikuo2R+mKwZE+AiC05SY5-?Bla?$9br@$pA$!c=yE)yD?ehh=;uIctdc&J|axJZ>8Uq`f z@(JYb_*xU#gH&*wvCKj~n`fc)>S^>a_~y}s7Oh)UhNZt=d zvnNAUl$A>Aw->%|Q)dr%!*M0DIZpl9>62-hu%5k^kr07A;@t{5s%$+6Z;-xD;WsVK z@K{N8+9_Fr>pOd{_m><*kt6UEfC8E>4A#KI&7#Qs&bq#0gio(Vd|?u5QzK9!SC`+g z%b9h|qn6SFS+lAOSDKUp+j|e9d}q#y%sc))I<)E1HB=&c$m$Kz>@+#{!V(6vUNPJB zZ50k>Qs)a5sHH4OT{Fm*Ap?b~BNW>GVQHk}?5|o~oJeF0J1b_O$!oq@a{z(ESyo|h z_Bwnr&gEAd8RAARHXHY^@T7z7ZtnWCoJ9N~+$_iHU)hCvPO%TiN$%ab zC#sU7c2rIY`xm7Q_LR78=Ts7jk@*Isb*}x)Fog=4f381*R3=rC=rfr(M)P%Y!8Th? zs9bjK!O3dXiG4obBK5qV9DwC&VF5{sbP)Tld;cLS_3H_DZg@^(#bodI10t?fa2dvQ zd1jxx%c;;iXjy-XQKv)q7yq>b5lsnmvin6T98a_7GW=+W{MXUdfIy;TBQ)C~MlLIL z4hX&pHV9dY0uB{+2*SF|S`AntN6VQ#h{-V9lcW@c$puxazm~3zXTY*KgxnE59M%04 zG9gNt?ZoNdT3~lTRQ8&qR4ow`C4b`-MrDT2xTxP>aGZd z;V#P60d?y*HfDt64sdg3<%>tLXFrE5$luCMOyej!0r4xaTZt|>1HbGB%C(mG_=Hj4 z2D{S=2~puVc`hNMcS+-lzD@SrAE&OnEyM6`XJN|zfJsUOU@E~%Z=NcaG#&;ja zX7`km|CWn%Ri48!BFpYzY`LXeW;HFt#U&86`7KEVAmB2e;OCi&9S%Q?O$!dw2uqnB z56JPc*^75NJmZozwb=N3b0E73TI4C&6=*y@yK8r_GIJ;L(CY9mxNTh+9mF4^&h}wC zYyZvmv7x(Mze~T$aQ_W77G)$M)#aujHXcImw{+KBNEy=|1Y2o?p zw?zQ5nOvwH^1E@*m|{Z3w1f*5UDIJ_k<8KY+Yf8umb}qROaU0HgzVTSKE0A`yV@}V zGR7Qak|0pC_dfjS*rihERaolh-+30A(&CEBF{pH_H3vSRQ{U*-irmcbhD4 z0ZqKp1cyAvD0WHps=9_z{RKFif7^@Lcmo_DT|3h6#tw>%cUwa1`8(UoN4h`JNqMeG zKLD`C$Keksagm0fvTE-bwQ=au`A0wDjlMGk))`_7_KwJRz)?YG>j$vpzy7WbBI|Z& z(}5z5S?uoa-aKSvq2Nn0<%{jqef0FWu++U*4x^NsNZ9ZJ+YMqcHy3x&w;os?$AGiA zm(b0M2uIKa^=+Uf(+Q{OLTr6>I;Zftt!k>t?n0#jwZ=~Mj$P|1?C#iT?BtMi)-R#c zYO&Zc9Vi_b!a@UIQ$dt;?khOYbIT#8p|9E^_+d8`E(xv7?XMBYoiC_M+vO1LJI8(q zn4QTF8y~_Xc)rOdHfS-Vak$txBf!4~YZYy#UAa8T=zms4438+QrkZ&Ih;_{hdSeNy zwAXLj@PRKstd$Bkzh*p^Yd7#B;#xy+GEAUR==kpRNXF3_#4(e~55F!lWH7QBp8cdz zMJA9V?KwixP9n|IfMvQl98XZUT|I%dyWS5f@;>U=ewG=CG#=P-jv8DSKLna=$k$t4 zW1Y1#vR#7*N73{>k74lr{TFWX^c_aWDyU^=sG6jobJEiuU`!6i?Hy95lgQjR7QEBK zh#mePXJ-`@N7r`k0KwhegS)#E2<{p*cyM2z1O|gwR+!I02{uCwnU?+RG++74U{7=wQu=tCzWCgo<8<32?q(zoHvPOwK}wV zlnA;sro>&ghJvU|2Nm41pp16j<2rtSfCU@B;nH8NdG4%9{lXg~XJcd`H@!F)tL70lttoRn01@NI8RW0}U4W#L>z)(JlrD^=8rjaDtk3_)>(X)~zsQ>G6|#A4pt*|FPV zCG|9I9RBIapv>SOA&p;47TzvOasYnu8*i|WLHV!Ork;YtqP~guBnDVGItaOFh0Px` zPekN$Mj$dgPW;zcf=Xrhox^58hu?*hzXB77`!gL4gMZ70pVr9^M!*1y&^H95X%gTV zVNFc(zme#8F&;gJh4M6oll=T~C)dplcDo5$&zmNo*^<8dj{;vpiVyC!1;C2vaQBtq zJ@mc%_WEbI!D+7d)b!=N1xO<$n7>WD(v)060+?4&^C1B9(lv5;7(XA?Xv+)4mGN}K|y8zCfmGZiZ9%@ zz~;MBgD)<_;$0}Kges-!3~=OjfVb03ix%p4!i|M0P77rw7agl}hPmK!^!J=xh5vEf zr|7U!MsvyzW#LAYnHjU{7#OpuEG+W;-Se>P;Vx+IwECh#rn;k8q?%tqLHPMm4gcx3 z;nB}oNzctUW20LQ8`k<{v8n(zo>8#-#ytD(@xbX8(S-AHB*!no0uX5{Gxuu;p!?UN z^(=O*YLjNNUR{|H;T3CGHW3LSZBGHlPo}W0lkQKwZ5j^j$K3FcH?v_ky|%lMvV9GAo~uE46?+~LBTUO;ONrM*t+z&5ol7>#+hN^EDTTm6Ddm3GgK*sT zgHZ11bSj&~MeDh_PsA*ed-6NJ=%V+2ONRX?Qu6}!Jsh*Y(Jh;F$`B$AnY#ZN!Uur9 z{YMo=0o#<-mqIq^SWT)H%k*k4HJmgpZkL4ylXu&bZvZjp0g#GMq8Z6L$`#EDR1Y-) zo+}q|YN`FRq5O%$k`ATfT2jEBfg*?Xe8Hq4S=1tv=+oRw?CE0bn7m?wDncgRPo!eT zsJ3Dq#F%Q;O%-X?`p|&E-Q;PSI&IZ6Dat+=;9nT~Y1<)(SiSb+_q9%bkloICVnc#3T2Mm zj{@!cH}pdgyQ(xlcLbMw($m9jM&ljFdaj}V0!ZAW%<+OKdr1NJX;B2WDfs8z*&)Nb z6N@Toj>pdI%(Dk`HEZ`PbA=f^fB^3+GoZd(vyK@o;A|aQKhODEl(lnk^G#zLs9oNQ zwgqS(pUD|h-70LVDcZ6cV}dR{!1@jflh5IkV3)Oj0&>!c2;z&Qn+(dU7iZ=Ht$Q7T zoKMD2wH=|7Tdcq5@jP+7ImX)WZGVEcAI&S=4KMc;Mi5uFGHTsa5#{tn1Qx$~;1Uvx zNM5=CRHV!Pw90G(Vi85`6U{&=G(XoiX^A`39W>_XxiN8vqkcf+r%<>pi8$^PuFrUf zf->y8cB%J-2_LTAhDHh(k&iaDE;-GJKQ^sOcD(y8?>Ita9_?!R?EdQ1oBpZPZj!ZU zHH5`f-~6~LxR_%DFr;Sgv;@E8y`6Wz*p&hR&Iysn+*{u+u3H;lN(`w%gEE_|JF@rq z8S8b|5u>|Z+J?-MQ6a>HWeL$^xUWuqFA4jVbox((L#Fh+3_Ph*H>-7x9L?OKvQLJw z?P%f%j=$>BHkb7M%nTnM4g@Yk3z1NF$b%!9ZCG;r-itE)ap`VCu`ae7j~&pZx_;gP zV&texdmapCJO1s1wPu~ezZ-9lyvj0o;=^({uaanLdbelOHsgm~kb0ku_hNIRSJu== zxWZZ+xGU!}E)9r^EW&K9&^R6!N0+;y`_gbOS(`3~>ST83K|ze2=pi(WRlD(f*43n? zxL=hY?0ShS{T>)kd~Q>L!S0pS!(z!~wy{Fjg@R8L4eyL*!#NCM84Rt0f?E!LEu$u@OK zEc-X@i?v^qu?e2vHOTFx^LCy0$2IJ{p3fhC+8Kq95NJ{f5)kgmU*COnl?}tB=|#ir zD1s=qIGsO};6@S=W7D~C&uM^*mpCD~ z9_@`t8I)2{*~Hvm;%sJTU~Co<^0cdUR2uhpq_$GoDSY0JA6}7uSWNs3oaxgFD(ONb zu56{HA(1Jt>#SdJY^LM?P7Bd}HY9sQCB^quv3Bf&|4pkEk3IsWUns`@-W^&@1AQuuJP6qs!6Nf00OOugq-D4nagML1Equ*V^U`RvauJnv zMiH}j)f}DLw9a(DL5q{(9EhBYDPSpXQ5lxicJ5+gQ-~h=hvFP0%rx!0zwT7 z=SKnsJ78;L?eGDSS$Amun4aYlmIp8JhH_2)&2MQ7PsD1y` z8NoP;!LunU6eT}3kMxWS_()9aq@b+6G&`x}-MUA?PzPr8TsKn&s~}an?n#{$RJ1U1 z@BFGLA!4s_iOtJ$3xA&wt4Q9q3GFkiThK^=Mt6Rhp8N&B#WBb>|1O?C2S9Zi=YoQ^ zuSJu~D>6ho8KNO(KLpxBJVh*a~!qFt-+Y zdu?C|>7`!L0N7LUQ@KHTy|Q`v6^KiktV4mMymhv3NC?1 ze`%FD@cUIpsY`ccgheTfwiNfqEF@h&03%}Paxwv}k>w02Wlai48A#L*8p4G%M`XzmZ4 zY<(nOTVBXo_(LaXU!Rp_gGwUwWsu=$z^`8#ijgv6eao`bbi&PXGZxLV%!K`XN#6lF zsJARuFpo71Kg=S5GLu*JQ@c+7RQ(XPQ)ZS>{3Y^8w{Mv#$%tE4wEX!hucn9jk|GoJ zRJ9yd^lOlHF`xbp$uQ%X*@pNjH8mhxQAtwD^c5sm)x}t);v{wI0ptyq zB@i3fNEoK*0@Ka=1TtZh4asrKF#uvPA}cc0dG*h?+9mDRA^`ia6GNmZfL%D|vy$OSC-BXLx>5YZI1JWZF^n zpUQo>v!4+Lrc6H21S^OO7ZVtaK1MzcuGcabFC6y^G{w?ow~`H311P&pkz_ZMxaSf@ z%g^WX^UIXMkCo&B%8Yf#C?lv(D!C*dXgEzd4SI|Uzzvu8I`y2|5Vko~kqT?G&cVMN z|4BFkaVO^zMtTS65L+eP1yOxwfU+w_8<|}ax2Gqu9B{K#lcFYv3_8ic$C8m$rw|~? zCYVyI657pT52slVfNnYKDGJL*uUI)tThXdA@vEw|bDW~nNqJ9{AAuuFux-z6N!6Sz ziq?AdFvn*=L}(+4q4V}p6WqpoBFhV=S6Ij>r>NJZJsUfz+ZOBbZJEXzS0MiclsM_& z5|ew>MO6YZBXfhdpMw5`{f6N!O61BCB&pYVpyR*j#oNjPENAz8o+im}Lpa$1JE}XM zjPjx+b#<(C#h2aq?OGviDMBd9cw{a{TdXq-#PC5bU(Qzt)jJy6>!?@8{KRDHSH9kD&A`;ZwsL1ps?Eoh9w=1J8FrAPjb zwNU&8&DwzgmVbmZ?_ws$I~E8_#mgTi3o-~)BZ>%_P6#4yWAMMgm<&NK5v|ZKx{%`% z1q~3CSPtz0Q|#tHMi$I3>PPPie<~a7F?^t~u#}T)e3RdugBcTfZ$8RwX73}Kex%H5 z`C2eBO36~T6UvTNH=~g-AAs#0LndKD7H3+I*Vgb^d%z*w*FNP%4zTcFR#y*B&PByR ztXNh#S1tAp#gWJFzt?!y4}7vYrm3}2-Qwy#^0O?TFKz3k=1h&BK#Bzwz5>fk0QEic z#`|XP-*g7sS?}Tr>7C#OIHGQFIby57IYeEU8r-}1w)v;gGsh&%ncGO0<*}wxzzc_b z7Hf;bC&!Gz^}ULB5CP*=A@BhE(@A{F+=I&Frnn}ddQQB++C-yxH`&X?qAjXf61UqB z_>6DkqYdB6uT;?kJwrb>@rXIa|EJvaZ69(^BXKD-CmVzw?HT%ZM4?DW=Tm4gBe)4O zCbkVq86OVseP)QN9rM|XDL?Nkg`$bM))>zqu2TOwPV*AlmrE}KG+)SbAQM-LR-|?( zWt~hG&r)_omaC`8*19Pn{CbmZ))hpL!}>&%9I9sTW4Nhueh_0oYsoo@IXfnyBiie~ zT~A+8V3ym6tuo7CQL>G{R=v*19tbkx!`XyjHHwV^3+S+tvxO$=SF1Zl>!eAU!bsNS z5$Lf-3rYQwrh6U%vImnnIdQnjtcdt`w~6(pV;ILPZwO`n4+z){D{Rx?_{DR{CtX4AbazpjI`ZLy;b*H_A=ULwGmwm9EexXi7@ob|E=J8@t zLzKsnu$5EqyUNT`?`u!h(He*OL=rxSMVSxZ>Dd8zDx+o$MZktM8Mt0y+M$h5R1(J- zF$4C0-4S)97m)M(lZ0>67#|VKAsD1-#A@Wo9HNx99l{)$EDnrQhrnlv1-1WZ`n_N} zf5Zl!cmB*vSpF++(p#$&N;1+q-l$NDy3U7vIytqwG9WTk+0O?qsY7r>$N*6_AV>fa z=%$6TEW?4I$nhkoVhbtm@0u2Y^xsQLfbd|O0x#T!z+uy&@v|5GtIGO1eYY@I#FGYfd?ck*b*~8G~J$`PXSmq9w9DN zJYOMD@6crO%@K4>_u(exx!$42cKK}j< zwNv!B*l{@t%HZB}vUg?LcD6MVXdB1~3%RnUGc_~QI~#hwE(N>Vk}xDpSX9vuaxtVK z3=i((y-%gwf8TZw`JS9!YXXR!f<`WesAr?D6!_6gi7Fyr-O-+-UK+RjX#Sw@uF=h+ z*BVD9J^wic1{`%LV4~wctLDCVVD@~dn20=`$vxe*Y>KPFKqX%o8EvhX4*ej4lG*7t zujW;5R=O^VmElbPV`LWN?UJ@nZU%&+S6GwUMULUKWQdRdiK0`5!3uC9r41`8f@^o$ zAFp3`TP;du=t)Qn<#%=Z@kv41znFWHw!XUTyix35PP6L#GiYY>KAA0`MR|JO-I4gi zJNWrw%>>nVD&1t37hasiAUkd}DKH`L8|KjAxPMYD0`-0E1j&BiHjKr!nFSWfp3#$c zGcTGYHvzQC?DZcn(dOzM9U+#Sg{*R%QD@u&}^8+6_1dWQSc&m!Oc*pa*++V7(DK}@j871RS6)DWE2!5 z>Jq5ksyqp*RqklG81isA*gSOY;%W$rdW(;u2kiBt1y3eZP|75`*-9gvm7B_lo0IVi zVxjKcZ+1^GRvX~@VAb^9hq>mDo|7Z&v4_}CA+bl$-WdB8^&k_gqY(2cqpfaB0{xO) zI-hk4Qv1GJ+WTo#@F>H?ry|hQ{t8AkLg|KP#kJ~26llcUQR^bu@duG@fjffaE*aWo zPE(jnIj|;@=;p}dY;Vxp@Sga0w7Ou7r4Bmw@=<16{{p(}Nl^f2Q9?B(wKaD$1(YT` zGJIb1@nLc`+3RKt5y|zR?Pmv5FZwW>{9i#K4Rygf=wZ?%+gFC>CI<^QPDBXC#hmNK z3K@H2B{|7*0)~w4o1Ph+AAQpGZ}+bIg^7x2?IqcMiiB_Q?W8n9k$gvXlmEZ1If zJNx!>roX9QByi>bwpY IbLkO2(;L8Ebbv1r!1<8IlfRx%foeUD~zn+RefnEqw!Q zmRe=1&z2Q-?^i0a^*pwroz|m+_9vWwmSGgu%kHIrIVcMD8m zYLlCSX0{HH+uZ2F`ewyTI(`?LVVB%YEGqHVNA5Oq@1KYmU*|*sX_)%kg;+>F#;ifMs{wd2l+blLjIGGe9Onq@1lX z?Ah@BI9RT>|7-6IvRP|r_}wc!*~++KM6V+AL_q2u>b{f*3-TSfxTgcKBI!L6h?g(-DHPG) zmoTb5?&%X5`o2{W&7K)`2}ejK4w)V|md)+$Fq4l=rHwkioT-sE6qSLurE`9d0(?;g zzu8XNm%U3x6O8W}cD7n%lQQw>1FD9(Y?eg*hAS^V3+Delz><=XhkX%gM<(PRLjN$X}GGv~7UL zp$T()TH(jNCxXNB8QTxD1Zp6%4qT;oB{B{V7)FS|droi0OsSeB(`)R-!(Jp?zZg<| z)&lf1+IQU&I&@)SYotVAGmNb?S-&k8S=a}WUT>8kuY2F)T)5ixMJ?^%0CV*q=m4<{ zLPS>tS#A15oz{jk=)@bo973l>XFoi6tCLvCtNSx}kEQvp8yg@CZRX4v5NMrGrcEd? z?fk#&hId0&XM**_<`+~9G7opV7t*=19rUUKXM6Lu^ctoje|ETKsXN^JA~0yq=%j?) z&P^wVh_ru?!~yBMqb61Z4Jf*5Istm0B6|W-+bM}o$4rLVlFz$^ac<|QCk8TAjZ=pF zO7C>KN_LI@H#Y33SeAxH)v`?i(p*y z>@<_mYcP+_uvuTZ>mG(jj|rWa$F3_WoofEYBW)9_6#*PJTm`(}0SO%y0U+*fCk573 zNcHsY;65h~4(#s38-d~n;C?J9db7ib?g8QJ{E&)Qiq_cdbb542B`DdzPFG$hVBAeJ zw?cUXNy%ysZg?3-=b0|1;>rWAujh4-hKDvcA^N5S@!7L9pWYmC@jA%tl(Tv?aq@y zVisF+yTorx%E%z7b^`t$G@T}()avf`X#5dJfVM-t+7u$sDjq=d)s`q(a&?i(jJp5G zd@`L<0Y&hC{twj#FW2Lk4q{Z73s*t#mBjv?qlpWP?cy0Or74H8;Q-wfonX(898JVkF*r5E!PK zPBJfvP5A&}-(!Z>;K_XaHwk?lYqaB{KJxXZ)ud22nS$?ft-X=woJ8wbiju9~ zx1is7v<#kOV1**}hVldU8$!?k#tY<7VO=0L5vcR47*OJw6gSrn_Pu0LiB|hK7os71lh>p7Tn_rx1 zcE9;_3V!;v~1Zg<>9nYC2(3AAV3Vrhx}73}LAq@o&JqZMdxtX-<8w(6b-y0qGi6k6$>EI^>dj zIh4^1~kN}=Me!O;EY7>F%HcQRZrf}3qLV;5dOSSpx#;~CJ7hkuMD40pZ90BLzr+YW;kyKnJ!&k`Af^D}aQhKT@_wLNRT^{=w3Kxx9efJj#M|G%oHjHPm!;I(LV<0PhxFtINR8v2lLG z*a9h$RdN=_IE8iK{tBxo7h__2MV6qVGksg9av&)nxcCc@*Q;#09s~7)s;srlA4ZlgJAAW#Um8EATRjwLC`}v`bA;l?bK3 zVc6&hE>u;B$Ey_GGu7N*$ui-}AviV;Eu?zRcI(aHvmHf0Esr)C8V{ztX4|HCQ}itx zqHO~>HyYsR?T)WoMT<%zSPb%8XA&djVLI_EXkcpuH86-qU7QgJMAqNcxp7qJQ0QkY%%aE`5} z!jSn|1uMoRK9+Fo)h#U(Tp%0ZQgZ8~^sxZn|BSQBqHZ!2h5Vp6QaKt#Vr?SjXcwd) zMpEMvWZVkzP1^;6s5p_I)*9EE?MBCoJtNQ$K6pdN)dM@ zWmSDS>sQsyQGXBUYaB|y$I&gz9$I4 z4NJehHZUCh8HAjCIEGkF8i&`vL@I|GETv!+=u|;)J<)Zud?r-sH;Zs!JE9k`a88#? zSNEvUtR4(V%kM#t9hYG`6y<}BxEjNF5oWnkK4ko^tXTRLUO(NzG;;lw0m?=_1o#Tp z#LsP=i0dsrDdUP|v-; z>&Epv-~Y?*w}uFw@f{EcCs&hM_z}i%_J~yIwJEo|1W|pF&ac*Oq8D_Gkzm-`y%ah< zuSS?Qpi7aB=j{J|*&$D5*x3kZ#EY{{y&jK@ktjMkV)TF;nm9RowLi>qOL==(Rr)>u z*{l_ujh+Qr1KSWkY^hX|zmDp61uCJ%hyR+t0de3jGH9fMQd`HKQ3Ag(8o!@VbeGppjwEr!SaX8r4{DI|lFFm7= z)tb2?Cl!sdKWW(-0P}cLuLc3+mKV1;zNC+I)WjI=sqq}6&iJZ$c2MyjT#Gr=molti zj$SMcbe?BY&%r#WPj`=u&_r#5;|P&&aFpjeo#(GX3GkD&y$Kzdt|?}+lkv3NKxnVP zeKT%a*h@Ow=Zt(AQOii@-||cjn0)^a0xO5%Dh&Stj-_E%t_coPTjkv)J*cw|6&{=cli?+Nkda=z@kyW=f`m5L*%wOrbDzL}wI?m|@&RYw( zk0=IJj0qU<5V~KL{LIWyvBiNUH^pHPufa~ zaGE*nNCn}Xw(*nc%tL5Z&Hc;XPiki;h!J#oN(C>IYyPn*Vl>LK;Jk!-_Xs7 z_b~U<44d5~i1wFD;%@qQNgpe{?|#?y7#-~I%}qzO>qf!INehGh8FiY8PJ5j9bA!^i zVgTc(d!U-`=*ZFQ4}QgqcHi1HpH<~2w|+@3NZAAZza2dWSAu630(<4}|N8kK1l#;X z0ZWprWN$THDlB0n*IQleThAGL|2EA>MVC^4HvJ0G^L-;z-$K!r^HWjeFKSy0MFd*i zHp)F@*Z&%#=NjdQoaiQzGANNx3k5%e%mEXNUJpHrHCH?Do0!U#A8ikQPsb&O4q^LF zU1DbTcwxTVZ6)(RPSmwmNM?FsD`A;=klMMJeL15GS)~POKb|h3&{@;!EFPF^CpI*n z1B!L=jPH8NV~GGK_+IFDR~;8g!dj@xpgkU>qp<0(+yVCiPOm*N$$2g7kOlSbJAjL6 z(!Iq#D6@^r8)KO_w>By~rx2I5)Eilt=wYVN9V>IKRuH8IbX_42X-C1PW z*Rz|ce*^TLh7HC+zJ&q=OVGFkL!h*c#j1>3;c0#MW%y7F>mPUNO^po>&KbAIj;5=< z%a2u?Q@__)-B71o&J&5kcT-~(w*gOJN%<7?Y8Ay?CXjI2<}=hZ9IHI8gPVgzkKFAM zm?nxb$J55kyB8D+ouhIUPD*?nvgvuldjMYe)&NNMIsE?iW2C-y4g)?Yrx?$TwQY7< zY@C(>lTfK~~lOy$Bfz;2YP(EP&FLrCF196SL3u=`| z=ED2F2@T6Esn-kB?CWN>%I}jfbW}bk+6y$LT9+UpKSyXM*-uxk(DQ`w-LX)zi1(+myD_88Iwq&y76%aUv_2o{R!7F=$<2s5xV{;R>bs` zFw|n#!O`_oUE++9vhk$5w-dN5l(KHY{gwPDN~7n4M0$7`Dv8d934gU6zmg=*z7q{y1{`^tSovKrLx10gu?yn%4q0o zRlQcg@;3uj1!>9}Jrez>`@S?D1?;+kzt>&-Snq~_cW4hRZ8!9#M=tbtZR0_jO?m#C zy|MUON zY9s=Wa0|ApN|-stg{?!fI1j8HyG8M+^nW`jnirIp^*2+$0dbU88)6_TKzsR?9M?>T z>7RYMc$=^eg(_c(S)~e`sLgQDVATnw{8X*hj=_R!XAO2OUJJHnBr++M;A?h6XC-_E zX`viQ5^}a48}NvRJ9|TOlt)?JH1qFqihr9^D4@l1>3U$v^G&5 zJoAoW_&anR{GcUj!fVX_Bi$F&@H+cAl8chMn%bU|C?wx>+jLF&^4iPXNepwYEC1pF zZRx@u_Hh$$YQVCAo20i+k6qWj5O~}!ZT_pFr`W4~t7jP+O+De|KW%=4C6(^%tAcTn z6i{;u9KhG#r7V9gBKp%kX{O9tKEXPl7NGyFD*C||PGX7G?d`fG{YSKufD^DjSsrX! zVd0Shj^d_qms7bQkY|4()Y|^9DeK)8jDI0aD2-5(>EzUpF*e`|0ab>zB=R;QEYHL9}rIWbi5@_z9h0sK% z)NFMnToXqRY&bpu3V$9dYLj&7bt}yBR))x$VO}8UrSqQDhw3HYPXYb!2h5hi*s5@5 zqXA!u#j5VFqktxf%=|kt1x>&JdtWa@2tgY9jo*Vf8RJrEb=J9Z4e_(qSwW`Ij0%K+ zkrEkY2-n%KFjnsC1=);-$h++@U$d$G9sItHu_> z%9I?h@jp&S(D#sQ>0lEq3N3&DK1q`qs{vnQR-pAG8((ERb- zeqFp0Kf-rRGJ$Nxtj9OoY5Nd}@5r7$duo!9Q!{|)F&Y`^N>*h-+aS^EaJS~)3$jbb zowAD3Z^S+hm^1D*K`CQu6;7_V=a?a&YogIQA{;sL)h^sK9{zov;JDos(YTMpCL8Ee zqC3;IirxKo>t~tRY~MmOm<%NV1alU%O$`)O|1SGWga)W+1lxCC-6oMJkRR_kXFGy_ zGO)?!+P?B?t#n?y`^hTiN}x=hzuRW+x#KFZnceQy1E1%D%WC!v?XTD6n3*4$|A+B; zte`b&cGr6kkMUpAOrrTS<-OCg^K`Rj>p9u@i-G_ z?X=vnyI2OfHBRcOdjJc7>ICEbchzOgFRtM9>NRmS>@k2dSYolJ$m`QQHPkb%H1PV26Ido8X`dE*}vAh|Acb{ z@Ih%IVx|%?Z+m%!N}Q{oL^VOE7lDvhROaTh;D1*5{kiE0D*$DBcN*kC`T7o?rrAKA zc+u(1g^%Y)K`gm?%4i2G%1gNkQ8P=D4B)l8eK?hPcF^%dv=iEhU_TVGi&Y$zzleC) z>SyNWM&29N%!X8A-5;q&wmEaIn1(_3%@;ISr48l2^~3S(6~}6)3G6xmwrC!k2x0L` zS3?M8+#QIQ8{iBLqC%2J2gATvXT7fha;OZAlpF9U#fr^&3AxsFEVzL7-A|g^)BV)< zOf9Hp#YO6_r8$ zTi~ITX+G#xQtA7`j|q-d4+RE~$SwUpdenrA6)6bDgp|P-W95u9EYt^M8dW z?na~Tw>n#z$V3ZqpR&1{!9Ac<;u0t29xm7f3{obLL^J#UTCcpP9}!>v>&PYk5){+} z6Z&W+>4ma#7ils=C-_+q&4AI1zj6KXTGDwvoZ-~BVVjq`c>K*DN{VVmtGjP#Yn+#3 z_1h?rLUR34(KcvyE+ao8n^Yu*IPz zVbL7oHgZQEpDtQUzmR+ajF%L$9T&nC%~F(_XRq78z4J z^m*6XLj%6G_vglB3-gsLJpyPPT-Kw>Qd+2jpViyEE8{^+1ztO8hkMV~&!@r!lx^xu zwEeZe4`~O=L2L#5&!Yo%#rlTKmwsso5(HtyALEhLj!RFq)>@cyXk+dBAmA7qK$@4; z+eIUln&oHA+>vi@k{e^u-}rkJ+{U2Tt`HQZbK?Vr1_2d8&b<-mzCh?_1!A@GnV?_! zl%&TE4OYVoq9b__+9c|EY}zQaxhQcsJ;|M2x;fG3@3u+e%|7f^jNrv}n*yh8PPpV3BM(;P9_YSXT6g4}f4tDnlM zWqBn-**mNDv}w)jb(p+oQk8&MEOuq!L-48A=?cw+{@!QiC{ZK9!QSC8ADfx$tHqc` z>(Q=YA*E5lnz3iF%g2xLzF-I{D!nz*%Z&NMOP`@;;Bl5m+%3UuDvk+4lkUx`z1L zSt{{UU)N@(@lTY;RS&bRf5{8uVHR$vYJzMG!|w)zC?Yt zEKId6IDcX?`xt(qo7k+g&?9{df>LE(Xfj?8ggGdfsR`gOQlA~nKgX2aABmB6g1^pbvE z`86Kk{?Gl=KqD?@^rbB5F#7`wIN{IqR)J zqZkrgYv-6YJux#4ht~RvYvB-|1w_o4bT=np#odsLS0AduuUQP`y{Z6t>QFj1`Fi(* zM(g${hTBq83E8e=ld?E=kyNaZ-_KLx0i~6Z7@+r}yuZCTC$Agi{?s8VBa{pmCn-qH!fi}Yr zs`_6cDd3sUoa+$8z!JT2jCsxV!&u>@EkfSdJSg!<}Pp&JalWG|1tE zMPH><1FHM#5NV-(S1r&uaHTx1APg8NpSYQ5DA)@I^$Zt#LDx$Z{OKxmwP7Lk1H<#b*&rX17^XHf_VBx5-N!bM+2^iT_i*@L zHNc_p9(BK)2ktx(DOxz;R7%;{vlZ|n{ELn$%Zp|u1HEwzbnFdZ|Hw?DvIV?8oGjti ziaeL$KUK_PZvS{3XYewTIoH$APUCSGCg65qdGCxuH>&}4jCp%!g#~QHnK_*|hHhf}OHt#9{=anwD_L>^L6HSUGMB*)uCOgLx(7;mz zWw?HB2-iK%c;NEiV-rShdpS@^8^Yo%0LtDtuP#uhM9Xjv<2RH;k^yv?-wzkGdr~$ zpSJ>k+Jr{Q&YF~unBSf2*0_`8W}w+^pKHKoI`_F&BM|j&2a4W5xirlCQaa=7#F%#S z)BP&^lD)Kd-$a8y2}lK#>?uQ>5EMzXJ_#bA-jV_KrmPPXhzX1Tz69qmlPUIBZ38|>(Q;f> zC`n`ASZP7*49wq=R-!jptgS4)`|mGv+}aSo2+cZq?=>eSHA9%3@&Ttzbc1XF5H#x8{Xp_ zl%&uk>Qk$GT`dMc)LMSG^WHvs_bPImi$BvNZBg(4(iZCMm1{7m#hS&<9my_%Rlk!6 zogUg~q#<`Sbpq}^cej4p#`F0O3l&+(RU;KUn{4)su$k`mXVVy&kHqVQMRr*3>UL*J z$xN#MD?YR_p}a@vPm&OnsOAPNB~qehv+5z|)nNk_d>{ysY#RRHe)$5UEh8zW4z%=v zAP4AXo*)csiFG+2n@_fLaYqWbooV5})KvWY%yvYJBwQUW+G<~-YJN-!oYA66{Qabn z@S!j;Mn}YA_afFr)($qqyoisrQpWO%^EV0^R-dgJ{@%SB9mlSyTMO)>%h_c#qx_mc zto@juM3Y<#TTkHf;(4Nkc~BE*_Kkqt02W!0j~AYQSPhN7#y5Tei0%9NNcnbZ1?#XB z<3EGJw9>Q^f%`+YEO?d5bb3xYs1KsbSXXghW|d2Ct0L)RBeJ0$@ATY|c_(2*>S27z zD>urp%1suV{3wB3lxDhLl8B(cFT=o2KmV$o4&tv{A=|cDY9vo3wyiCbwga=z#F2Uig`PNP4h_H7l)CDwn zXKjbg)m;CWVAmH5d2l7N;T631=;v>A&=%hTr;WE(l?pUYB2VnXNfL(891rlz)4sj8 zQ^AzYTw;s8&!mBQ@&(_I7=2UxYPuee=L*}WIn7o$bpR24@sk$6`k(A~mV|jmrSdT9 z8VFJEQp2dY}ig{4DD;@GQ3bx6??K zwOtoRIdF^Brsj_Bi)7pJ#dpq5E|s^Wg)%0G|62NxW%?@?@0?A1Js%>Rs40C0Xq?^-p+5c@!*E&O6Dgk*OcwD# zyvc{uiq>mV6!ZL_Us zf0(D5WS7?96SI7KH_iY;2S5-)AOpz*AjklnuTJk7RAQ@AHiYz`^XY=W%>cvAfWU_9 z<`L};kIQVO7yK9B;LNKXP`Ogkk9?uddRZT3=Uu29wrQ4_7Bxb1KzLrssO1o_D;gwNd z6z-m)|K)AB1IHgx`Z_%JbzLpKr0(zin^!Y5S;D6zs@<+OD4TcdPpz{2{j22dXR`9ASy@^0TEHWdiCPP zfQ@?<#NI$eLFJE%G`)ZULcH#D!nnGF$pX52R)$CW2~~d`(Zxmf6{fGBGhz%Iu_n;2m1i^$UhoG~yW@ zCM0DxHCSRof(z`V@aPK4NP{Jhb;$uF;9Y$!BvZi=G+o{baq)uI*O|(vBBL)$9=Hk+ zj|0fQP`OJkZhj z&<}SJChEMlPjqBJMs11)IbG={ z#nUtu!PI=pq?j288lEN*ZN>JbaJ9clwbe#O!aFghbW7-VrSikn_D&dra-wPfp)W+$ zb{m;7*RT-Tp`lBjaq^`d1s-$Oi!wH| z3+qX%79<39mX-9@FSrC|jf7#6hk~jodB`WYD}FMhLQNpfKPBNHq;{k!O_4)0&V;^J zhUt|D6c-Y0upDR!CaNh*bh^+R6PNm>CG@}pFlk7CC#;nxpsBdR2UUpD)e}QBg2W*M z?vS7>;uV5Q37g!wS?x_}!7Nv_sX61&m3*QTjf;iQDw|{mv?p)q3fY;4&ioCpsMC!g z>tsjL^@6_Icx__^!?SJk_OgEC`m%M)mNGM~iE3IC-b|JsEAkH+2vn%`@5#Gk&U)M?lsK0=>X@LyK z-ZT^#`3%4_U+HqPG6}H!OCIIjYcj7m3t(Ec8V1<(FNNyS-^*vRMui0R=nq;z4cH6; zx7sRh0zJrasOc&fb89k8!Bxm4(uF@JDx0Z)iI8A5Y7d5svd4xXY3kp$iQki*c&Krj z0eNg>;3S~Jp-D77C@~_+$qwrxAg{^cTjglp^sxw%qw>cXV2#!r(nVj}f5@n9$JEfy z=P$H`=D0?QT6lO992g3iY2wCEp~)*a?np6Mc~4dlR60jB4LD^3N_O|TR_Ck-6ln2( zK$j4h@+nSy#*K~aRY~(6|DCBlS%7}4E7ziqzX?4+7sY0XdG5`wxumm7@0x(4aMMJ9@ z;{Yn3ye0xq2)8N!32z5Z8gSFjg-4EMDu$l@YVuoVrT6N& z(wL{(g#~E?aU+WatE(ED$9Sb^Br;^{j;&?qj$IB^|mey*3ouINI) zKuA=}Q9t7!y4?S&Z0K7aQG>UNXkS4Vyrkz)JqIrNkr|!~yHXmGQ!6migp4blofw?9 zB+-T|LmsC#@S0AzEv#YQt35W=c)Z6;5_yhy9^O`ZbIg~tp~I#)(ix2syrwx!m8;*r?XBus=98Kt9tP2(+8Dy#!g_QF4BaAW!!~jx{{II!_Zev6wCEb?7BjJH7$Is zOf`+R;kE)vrP&>Gs<>!Nskg>fue2vUWoFdg+$IHcv&^0x9Pzf3yFg@9$V5T0F}R0TIw$R~(~N(EQjaf=h0wr4uwF;$_-*EDeR z8XI)Yq$ikvODEZ|r`D9^zLP-kRhB1ET&;P<_I0#oiLt|J5|Ux>*|>=^y(`YNyizk> zCHKUpjOwmcDGAC%0}I@YMndlSA#rkweW~zfP$Qq+>9S5T->F2@DJTyNbs&i5!6`bN z(`DCACOqjkt)(d^E+^9~NC})03k=C;BgIQTLxl~0Yttr4-lX3PaU-%xQ2h8y@!1FE zlS#F|Y!7x>((GoOo-{jc2p22%fG3mC(bdBQ=*b8M(J~Rbu_}tT2ciw>OBy9}7SOJbdoD+0P zC?y+zVKPNtWd<&@z>G?&*l|=O?=q9ee0&OOXXO( zVzv8Z^nZj<7L`8x3d*SmOwpffK^f;wCl27!=ZVoG#BZA`eIpC$76X-n7!APInht2A zzoJdurfr2`wa7|KzcL{|WNC*N*|0`w!5sj9y970#@QlsI1r~sujw*JKQS7w99XiOM zWpY~tbQT`+LL!z}VbuZ75`7JcK_B{s%@v3^&C3Lc48e1-nc$ev z_*xu#0eDMCex?O*52?JJ0RpXrSk2-sNfh3a>%e1q@MI7&OUXn@=uVh$hh@;!bS>n6 z?35tbVi_%+&7+KT3gr1JCe>SXPy^hDay12wkC((VsV8F4g~mAkP?=PaiOI#`OXTpx z%%lpH#-}4ulltU=kb~aPU%gU^f-)^^wmm|lDR2*Cb=%D+E?NL(>!vr!(_&DnxH2^@Vig1 z$`4DDCZ!}asggUi1vb){*56s{rbgCP{tzD{fYM}r97KXVF+s&}Ls32l4RHPwE=y|x z&}N?rZePu>6e6P$1v23&g}jNXdpuE{x0O%*$GgfAYfmT#tT{9oD?g)DwMKA_ zSdtYd`x9mL>eb3e{byMsw)(wzaX}?79n~MBN-)UuN9$~C2DO($6>n*@GxD_(GV0OP z)N?Y<758wC$WUxf2Z$+ut__HoWKR3|%I?~qdb%*^!hRx$jLB4VAolDU8-kP(<6&Qr0(RipNJTAwcy=kO10r1+P>qahH9}G1f-bS?@~F@pn5?jonet-bvSLqAvvitH zl?7Mo?@A6%U$eHv7xcC06^^civxK3DX=|pzn~oK#@CmnN%mE}aAUEm2r95Dr;CVtT zWAj!4+3-_d462!vUM4w?YT**GOK$&@7v7|z5eaFRYet8ENjj^1*IR$5eCDF}mP3y` zt~~1}U#XqlRGHSX<4c_#+87lVyJM`KMD?oxIX;ZxDsnH})nNV-C#4>M|UYb~_W8xv`%qU|@YrR+^r zdA-0Pxs*qLI|QwEMQvbOv#S$~GzZ`eS`HQFwpl?#qof~-6c9NsJcKHv@?m-wpvKr| zoai`ZQJRqcqK-11n27iM8nRPPj1TAXCZ;HZObvP=iP+VNqqa-8(SG6K z(&|s@G{?u#6sIz?<+GpoKzYaCytX{ysn09VeC{uQl*NnnijZujD?a#~^U9N-@#6B4 ze|vj*|2y7b*wdcutYZ zKJnpq8FuR}*O%i@Iny4mWM$p;-z>|PuPg^1dSp53n3Kx4zVVf^ZO4|feCdjiqpMwm zPD84H>#HrmIuETVXJeogg*kNbU__>Jfd=VPWE`MGnbfT5Y~B0O_aLGZ34@u~O7O$C!$~{q@Vr0a{VL_AfqC?pl9Kx%#rtmldm4m(w5g=e1iLlgNF zi$)WicB9`tb969nM*2~0&d#Tv>WVU@_No_WgcC&RNM)Z+`)RENwer2`C;Xs)F?M8Z zMY^Pgkx#o5+Z%I#JMNqJ!h3VMbLoYD_U6J{($+K*;bF96e5t6g$detkl!Edw1}nY( zj@!yR-tzkLxbvS?p8b=rFneq+QlEURcKYO}zeudJ<$ap;9(?xW$}uOL>Y{s0QRAR} zblZ)&t81_zENb<~uoSj|#B{K{9`&}rdQJJlCqHNrT+A!o*(L!j*24NGO?qp8bb_>e z^_sFw6Wv|6tt-opK3Rf7u8Wð&{YjF|9%ISA6jAz-&A8e9lZ7Ms@rxfsf4QzTXG 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

    X2X0TFftJ0I3oG}3r;hsPMUM=0!n@@p*ax?fm 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 diff --git a/docs/user-guides/desktop/index.md b/docs/user-guides/desktop/index.md index abc3ae7ccd050..72d627c7a3e71 100644 --- a/docs/user-guides/desktop/index.md +++ b/docs/user-guides/desktop/index.md @@ -75,7 +75,17 @@ Before you can use Coder Desktop, you will need to sign in. 1. Open the Desktop menu and select **Sign in**: - Coder Desktop menu before the user signs in +

    + + ## macOS + + Coder Desktop menu before the user signs in + + ## Windows + + Coder Desktop menu before the user signs in + +
    1. In the **Sign In** window, enter your Coder deployment's URL and select **Next**: @@ -101,17 +111,19 @@ Before you can use Coder Desktop, you will need to sign in. Copy session token -1. Select the Coder icon in the menu bar (macOS) or system tray (Windows), and click the CoderVPN toggle to start the VPN. +1. Select the Coder icon in the menu bar (macOS) or system tray (Windows), and click the **Coder Connect** toggle to enable the connection. + + ![Coder Desktop on Windows - enable Coder Connect](../../images/user-guides/desktop/coder-desktop-win-enable-coder-connect.png) This may take a few moments, as Coder Desktop will download the necessary components from the Coder server if they have been updated. -1. macOS: You may be prompted to enter your password to allow CoderVPN to start. +1. macOS: You may be prompted to enter your password to allow Coder Connect to start. -1. CoderVPN is now running! +1. Coder Connect is now running! -## CoderVPN +## Coder Connect -While active, CoderVPN will list your owned workspaces and configure your system to be able to connect to them over private IPv6 addresses and custom hostnames ending in `.coder`. +While active, Coder Connect will list the workspaces you own and will configure your system to connect to them over private IPv6 addresses and custom hostnames ending in `.coder`. ![Coder Desktop list of workspaces](../../images/user-guides/desktop/coder-desktop-workspaces.png) @@ -138,14 +150,14 @@ You can also connect to the SSH server in your workspace using any SSH client, s ``` > [!NOTE] -> Currently, the Coder IDE extensions for VSCode and JetBrains create their own tunnel and do not utilize the CoderVPN tunnel to connect to workspaces. +> 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. ## Accessing web apps in a secure browser context Some web applications require a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) to function correctly. A browser typically considers an origin secure if the connection is to `localhost`, or over `HTTPS`. -As CoderVPN uses its own hostnames and does not provide TLS to the browser, Google Chrome and Firefox will not allow any web APIs that require a secure context. +As Coder Connect uses its own hostnames and does not provide TLS to the browser, Google Chrome and Firefox will not allow any web APIs that require a secure context. > [!NOTE] > Despite the browser showing an insecure connection without `HTTPS`, the underlying tunnel is encrypted with WireGuard in the same fashion as other Coder workspace connections (e.g. `coder port-forward`). @@ -184,7 +196,7 @@ We are planning some changes to Coder Desktop that will make accessing secure co 1. Select **String** on the entry with the same name at the bottom of the list, then select the plus icon on the right. -1. In the text field, enter the full workspace hostname, without the `http` scheme and port (e.g. `your-workspace.coder`), and then select the tick icon. +1. In the text field, enter the full workspace hostname, without the `http` scheme and port: `your-workspace.coder`. Then select the tick icon. If you need to enter multiple URLs, use a comma to separate them. From abe3ad68f52dfc62eced3ccda2a3777144043609 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 8 Apr 2025 10:29:00 +0200 Subject: [PATCH 024/384] fix: add continue-on-error to SBOM generation and force flag to cosign clean (#17288) This PR makes the SBOM generation and attestation process more resilient by: 1. Adding `continue-on-error: true` to the SBOM generation steps in both CI and release workflows 2. Adding `--force=true` flag to all `cosign clean` commands to ensure they don't fail if in a non-interactive shell (which is the case for CI) Change-Id: Ide303c059b1a3d0e3fd77863310e99668325bc69 Signed-off-by: Thomas Kosiewski Signed-off-by: Thomas Kosiewski --- .github/workflows/ci.yaml | 3 ++- .github/workflows/release.yaml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d25cb84173326..a98fbe9b8f28b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1182,6 +1182,7 @@ jobs: - name: SBOM Generation and Attestation if: github.ref == 'refs/heads/main' + continue-on-error: true env: COSIGN_EXPERIMENTAL: 1 run: | @@ -1200,7 +1201,7 @@ jobs: syft "${IMAGE}" -o spdx-json > "${SBOM_FILE}" echo "Attesting SBOM to image: ${IMAGE}" - cosign clean "${IMAGE}" + cosign clean --force=true "${IMAGE}" cosign attest --type spdxjson \ --predicate "${SBOM_FILE}" \ --yes \ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index eb3983dac807f..653912ae2dad2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -509,7 +509,7 @@ jobs: # Attest SBOM to multi-arch image echo "Attesting SBOM to multi-arch image: ${{ steps.build_docker.outputs.multiarch_image }}" - cosign clean "${{ steps.build_docker.outputs.multiarch_image }}" + cosign clean --force=true "${{ steps.build_docker.outputs.multiarch_image }}" cosign attest --type spdxjson \ --predicate coder_${{ steps.version.outputs.version }}_sbom.spdx.json \ --yes \ @@ -522,7 +522,7 @@ jobs: syft "${latest_tag}" -o spdx-json > coder_latest_sbom.spdx.json echo "Attesting SBOM to latest image: ${latest_tag}" - cosign clean "${latest_tag}" + cosign clean --force=true "${latest_tag}" cosign attest --type spdxjson \ --predicate coder_latest_sbom.spdx.json \ --yes \ From ce22de8d15b9ee26a92b6ce34548cc772682c240 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:30:05 +0200 Subject: [PATCH 025/384] feat: log long-lived connections acceptance (#17219) Closes #16904 --- Makefile | 8 +- coderd/httpmw/logger.go | 97 +++++++++---- coderd/httpmw/logger_internal_test.go | 174 ++++++++++++++++++++++++ coderd/httpmw/loggermock/loggermock.go | 70 ++++++++++ coderd/inboxnotifications.go | 3 + coderd/provisionerjobs.go | 3 + coderd/provisionerjobs_internal_test.go | 7 + coderd/workspaceagents.go | 9 ++ enterprise/coderd/provisionerdaemons.go | 4 + 9 files changed, 351 insertions(+), 24 deletions(-) create mode 100644 coderd/httpmw/logger_internal_test.go create mode 100644 coderd/httpmw/loggermock/loggermock.go diff --git a/Makefile b/Makefile index e8cdcd3a3a1ba..6486f5cbed5fa 100644 --- a/Makefile +++ b/Makefile @@ -581,7 +581,8 @@ GEN_FILES := \ $(TAILNETTEST_MOCKS) \ coderd/database/pubsub/psmock/psmock.go \ agent/agentcontainers/acmock/acmock.go \ - agent/agentcontainers/dcspec/dcspec_gen.go + agent/agentcontainers/dcspec/dcspec_gen.go \ + coderd/httpmw/loggermock/loggermock.go # all gen targets should be added here and to gen/mark-fresh gen: gen/db gen/golden-files $(GEN_FILES) @@ -630,6 +631,7 @@ gen/mark-fresh: coderd/database/pubsub/psmock/psmock.go \ agent/agentcontainers/acmock/acmock.go \ agent/agentcontainers/dcspec/dcspec_gen.go \ + coderd/httpmw/loggermock/loggermock.go \ " for file in $$files; do @@ -669,6 +671,10 @@ agent/agentcontainers/acmock/acmock.go: agent/agentcontainers/containers.go go generate ./agent/agentcontainers/acmock/ touch "$@" +coderd/httpmw/loggermock/loggermock.go: coderd/httpmw/logger.go + go generate ./coderd/httpmw/loggermock/ + touch "$@" + agent/agentcontainers/dcspec/dcspec_gen.go: \ node_modules/.installed \ agent/agentcontainers/dcspec/devContainer.base.schema.json \ diff --git a/coderd/httpmw/logger.go b/coderd/httpmw/logger.go index 79e95cf859d8e..0da964407b3e4 100644 --- a/coderd/httpmw/logger.go +++ b/coderd/httpmw/logger.go @@ -35,42 +35,93 @@ func Logger(log slog.Logger) func(next http.Handler) http.Handler { slog.F("start", start), ) - next.ServeHTTP(sw, r) + logContext := NewRequestLogger(httplog, r.Method, start) - end := time.Now() + ctx := WithRequestLogger(r.Context(), logContext) + + next.ServeHTTP(sw, r.WithContext(ctx)) // Don't log successful health check requests. if r.URL.Path == "/api/v2" && sw.Status == http.StatusOK { return } - httplog = httplog.With( - slog.F("took", end.Sub(start)), - slog.F("status_code", sw.Status), - slog.F("latency_ms", float64(end.Sub(start)/time.Millisecond)), - ) - - // For status codes 400 and higher we + // For status codes 500 and higher we // want to log the response body. if sw.Status >= http.StatusInternalServerError { - httplog = httplog.With( + logContext.WithFields( slog.F("response_body", string(sw.ResponseBody())), ) } - // We should not log at level ERROR for 5xx status codes because 5xx - // includes proxy errors etc. It also causes slogtest to fail - // instantly without an error message by default. - logLevelFn := httplog.Debug - if sw.Status >= http.StatusInternalServerError { - logLevelFn = httplog.Warn - } - - // We already capture most of this information in the span (minus - // the response body which we don't want to capture anyways). - tracing.RunWithoutSpan(r.Context(), func(ctx context.Context) { - logLevelFn(ctx, r.Method) - }) + logContext.WriteLog(r.Context(), sw.Status) }) } } + +type RequestLogger interface { + WithFields(fields ...slog.Field) + WriteLog(ctx context.Context, status int) +} + +type SlogRequestLogger struct { + log slog.Logger + written bool + message string + start time.Time +} + +var _ RequestLogger = &SlogRequestLogger{} + +func NewRequestLogger(log slog.Logger, message string, start time.Time) RequestLogger { + return &SlogRequestLogger{ + log: log, + written: false, + message: message, + start: start, + } +} + +func (c *SlogRequestLogger) WithFields(fields ...slog.Field) { + c.log = c.log.With(fields...) +} + +func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { + if c.written { + return + } + c.written = true + end := time.Now() + + logger := c.log.With( + slog.F("took", end.Sub(c.start)), + slog.F("status_code", status), + slog.F("latency_ms", float64(end.Sub(c.start)/time.Millisecond)), + ) + // We already capture most of this information in the span (minus + // the response body which we don't want to capture anyways). + tracing.RunWithoutSpan(ctx, func(ctx context.Context) { + // We should not log at level ERROR for 5xx status codes because 5xx + // includes proxy errors etc. It also causes slogtest to fail + // instantly without an error message by default. + if status >= http.StatusInternalServerError { + logger.Warn(ctx, c.message) + } else { + logger.Debug(ctx, c.message) + } + }) +} + +type logContextKey struct{} + +func WithRequestLogger(ctx context.Context, rl RequestLogger) context.Context { + return context.WithValue(ctx, logContextKey{}, rl) +} + +func RequestLoggerFromContext(ctx context.Context) RequestLogger { + val := ctx.Value(logContextKey{}) + if logCtx, ok := val.(RequestLogger); ok { + return logCtx + } + return nil +} diff --git a/coderd/httpmw/logger_internal_test.go b/coderd/httpmw/logger_internal_test.go new file mode 100644 index 0000000000000..d3035e50d98c9 --- /dev/null +++ b/coderd/httpmw/logger_internal_test.go @@ -0,0 +1,174 @@ +package httpmw + +import ( + "context" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/tracing" + "github.com/coder/coder/v2/testutil" + "github.com/coder/websocket" +) + +func TestRequestLogger_WriteLog(t *testing.T) { + t.Parallel() + ctx := context.Background() + + sink := &fakeSink{} + logger := slog.Make(sink) + logger = logger.Leveled(slog.LevelDebug) + logCtx := NewRequestLogger(logger, "GET", time.Now()) + + // Add custom fields + logCtx.WithFields( + slog.F("custom_field", "custom_value"), + ) + + // Write log for 200 status + logCtx.WriteLog(ctx, http.StatusOK) + + require.Len(t, sink.entries, 1, "log was written twice") + + require.Equal(t, sink.entries[0].Message, "GET") + + require.Equal(t, sink.entries[0].Fields[0].Value, "custom_value") + + // Attempt to write again (should be skipped). + logCtx.WriteLog(ctx, http.StatusInternalServerError) + + require.Len(t, sink.entries, 1, "log was written twice") +} + +func TestLoggerMiddleware_SingleRequest(t *testing.T) { + t.Parallel() + + sink := &fakeSink{} + logger := slog.Make(sink) + logger = logger.Leveled(slog.LevelDebug) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + // Create a test handler to simulate an HTTP request + testHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte("OK")) + }) + + // Wrap the test handler with the Logger middleware + loggerMiddleware := Logger(logger) + wrappedHandler := loggerMiddleware(testHandler) + + // Create a test HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/test-path", nil) + require.NoError(t, err, "failed to create request") + + sw := &tracing.StatusWriter{ResponseWriter: httptest.NewRecorder()} + + // Serve the request + wrappedHandler.ServeHTTP(sw, req) + + require.Len(t, sink.entries, 1, "log was written twice") + + require.Equal(t, sink.entries[0].Message, "GET") + + fieldsMap := make(map[string]interface{}) + for _, field := range sink.entries[0].Fields { + fieldsMap[field.Name] = field.Value + } + + // Check that the log contains the expected fields + requiredFields := []string{"host", "path", "proto", "remote_addr", "start", "took", "status_code", "latency_ms"} + for _, field := range requiredFields { + _, exists := fieldsMap[field] + require.True(t, exists, "field %q is missing in log fields", field) + } + + require.Len(t, sink.entries[0].Fields, len(requiredFields), "log should contain only the required fields") + + // Check value of the status code + require.Equal(t, fieldsMap["status_code"], http.StatusOK) +} + +func TestLoggerMiddleware_WebSocket(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + sink := &fakeSink{ + newEntries: make(chan slog.SinkEntry, 2), + } + logger := slog.Make(sink) + logger = logger.Leveled(slog.LevelDebug) + done := make(chan struct{}) + wg := sync.WaitGroup{} + // Create a test handler to simulate a WebSocket connection + testHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + conn, err := websocket.Accept(rw, r, nil) + if !assert.NoError(t, err, "failed to accept websocket") { + return + } + defer conn.Close(websocket.StatusGoingAway, "") + + requestLgr := RequestLoggerFromContext(r.Context()) + requestLgr.WriteLog(r.Context(), http.StatusSwitchingProtocols) + // Block so we can be sure the end of the middleware isn't being called. + wg.Wait() + }) + + // Wrap the test handler with the Logger middleware + loggerMiddleware := Logger(logger) + wrappedHandler := loggerMiddleware(testHandler) + + // RequestLogger expects the ResponseWriter to be *tracing.StatusWriter + customHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + defer close(done) + sw := &tracing.StatusWriter{ResponseWriter: rw} + wrappedHandler.ServeHTTP(sw, r) + }) + + srv := httptest.NewServer(customHandler) + defer srv.Close() + wg.Add(1) + // nolint: bodyclose + conn, _, err := websocket.Dial(ctx, srv.URL, nil) + require.NoError(t, err, "failed to dial WebSocket") + defer conn.Close(websocket.StatusNormalClosure, "") + + // Wait for the log from within the handler + newEntry := testutil.RequireRecvCtx(ctx, t, sink.newEntries) + require.Equal(t, newEntry.Message, "GET") + + // Signal the websocket handler to return (and read to handle the close frame) + wg.Done() + _, _, err = conn.Read(ctx) + require.ErrorAs(t, err, &websocket.CloseError{}, "websocket read should fail with close error") + + // Wait for the request to finish completely and verify we only logged once + _ = testutil.RequireRecvCtx(ctx, t, done) + require.Len(t, sink.entries, 1, "log was written twice") +} + +type fakeSink struct { + entries []slog.SinkEntry + newEntries chan slog.SinkEntry +} + +func (s *fakeSink) LogEntry(_ context.Context, e slog.SinkEntry) { + s.entries = append(s.entries, e) + if s.newEntries != nil { + select { + case s.newEntries <- e: + default: + } + } +} + +func (*fakeSink) Sync() {} diff --git a/coderd/httpmw/loggermock/loggermock.go b/coderd/httpmw/loggermock/loggermock.go new file mode 100644 index 0000000000000..47818ca11d9e6 --- /dev/null +++ b/coderd/httpmw/loggermock/loggermock.go @@ -0,0 +1,70 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/coder/coder/v2/coderd/httpmw (interfaces: RequestLogger) +// +// Generated by this command: +// +// mockgen -destination=loggermock/loggermock.go -package=loggermock . RequestLogger +// + +// Package loggermock is a generated GoMock package. +package loggermock + +import ( + context "context" + reflect "reflect" + + slog "cdr.dev/slog" + gomock "go.uber.org/mock/gomock" +) + +// MockRequestLogger is a mock of RequestLogger interface. +type MockRequestLogger struct { + ctrl *gomock.Controller + recorder *MockRequestLoggerMockRecorder + isgomock struct{} +} + +// MockRequestLoggerMockRecorder is the mock recorder for MockRequestLogger. +type MockRequestLoggerMockRecorder struct { + mock *MockRequestLogger +} + +// NewMockRequestLogger creates a new mock instance. +func NewMockRequestLogger(ctrl *gomock.Controller) *MockRequestLogger { + mock := &MockRequestLogger{ctrl: ctrl} + mock.recorder = &MockRequestLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRequestLogger) EXPECT() *MockRequestLoggerMockRecorder { + return m.recorder +} + +// WithFields mocks base method. +func (m *MockRequestLogger) WithFields(fields ...slog.Field) { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range fields { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "WithFields", varargs...) +} + +// WithFields indicates an expected call of WithFields. +func (mr *MockRequestLoggerMockRecorder) WithFields(fields ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithFields", reflect.TypeOf((*MockRequestLogger)(nil).WithFields), fields...) +} + +// WriteLog mocks base method. +func (m *MockRequestLogger) WriteLog(ctx context.Context, status int) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "WriteLog", ctx, status) +} + +// WriteLog indicates an expected call of WriteLog. +func (mr *MockRequestLoggerMockRecorder) WriteLog(ctx, status any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteLog", reflect.TypeOf((*MockRequestLogger)(nil).WriteLog), ctx, status) +} diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index 6da047241d790..ea20c60de3cce 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -219,6 +219,9 @@ func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) encoder := wsjson.NewEncoder[codersdk.GetInboxNotificationResponse](conn, websocket.MessageText) defer encoder.Close(websocket.StatusNormalClosure) + // Log the request immediately instead of after it completes. + httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + for { select { case <-ctx.Done(): diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 47963798f4d32..335643390796f 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -554,6 +554,9 @@ func (f *logFollower) follow() { return } + // Log the request immediately instead of after it completes. + httpmw.RequestLoggerFromContext(f.ctx).WriteLog(f.ctx, http.StatusAccepted) + // no need to wait if the job is done if f.complete { return diff --git a/coderd/provisionerjobs_internal_test.go b/coderd/provisionerjobs_internal_test.go index af5a7d66a6f4c..c2c0a60c75ba0 100644 --- a/coderd/provisionerjobs_internal_test.go +++ b/coderd/provisionerjobs_internal_test.go @@ -19,6 +19,8 @@ import ( "github.com/coder/coder/v2/coderd/database/dbmock" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/pubsub" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermock" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/testutil" @@ -305,11 +307,16 @@ func Test_logFollower_EndOfLogs(t *testing.T) { JobStatus: database.ProvisionerJobStatusRunning, } + mockLogger := loggermock.NewMockRequestLogger(ctrl) + mockLogger.EXPECT().WriteLog(gomock.Any(), http.StatusAccepted).Times(1) + ctx = httpmw.WithRequestLogger(ctx, mockLogger) + // we need an HTTP server to get a websocket srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { uut := newLogFollower(ctx, logger, mDB, ps, rw, r, job, 0) uut.follow() })) + defer srv.Close() // job was incomplete when we create the logFollower, and still incomplete when it queries diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 1573ef70eb443..1744c0c6749ca 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -555,6 +555,9 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { t := time.NewTicker(recheckInterval) defer t.Stop() + // Log the request immediately instead of after it completes. + httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + go func() { defer func() { logger.Debug(ctx, "end log streaming loop") @@ -928,6 +931,9 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) { encoder := wsjson.NewEncoder[*tailcfg.DERPMap](ws, websocket.MessageBinary) defer encoder.Close(websocket.StatusGoingAway) + // Log the request immediately instead of after it completes. + httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + go func(ctx context.Context) { // TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout? t := time.NewTicker(api.AgentConnectionUpdateFrequency) @@ -1315,6 +1321,9 @@ func (api *API) watchWorkspaceAgentMetadata( sendTicker := time.NewTicker(sendInterval) defer sendTicker.Stop() + // Log the request immediately instead of after it completes. + httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + // Send initial metadata. sendMetadata() diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 5b0f0ca197743..15e3c3901ade3 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -376,6 +376,10 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) logger.Debug(ctx, "drpc server error", slog.Error(err)) }, }) + + // Log the request immediately instead of after it completes. + httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + err = server.Serve(ctx, session) srvCancel() logger.Info(ctx, "provisioner daemon disconnected", slog.Error(err)) From f935e2a1d2b2f643137e21a38a97b9b5178ef1af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:17:47 +0000 Subject: [PATCH 026/384] chore: bump github.com/go-playground/validator/v10 from 10.25.0 to 10.26.0 (#17173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.25.0 to 10.26.0.
    Release notes

    Sourced from github.com/go-playground/validator/v10's releases.

    v10.26.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/go-playground/validator/compare/v10.25.0...v10.26.0

    Commits

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/go-playground/validator/v10&package-manager=go_modules&previous-version=10.25.0&new-version=10.26.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 42dde8033dc67..28202d05d42fd 100644 --- a/go.mod +++ b/go.mod @@ -119,7 +119,7 @@ require ( github.com/go-chi/render v1.0.1 github.com/go-jose/go-jose/v4 v4.0.5 github.com/go-logr/logr v1.4.2 - github.com/go-playground/validator/v10 v10.25.0 + github.com/go-playground/validator/v10 v10.26.0 github.com/gofrs/flock v0.12.0 github.com/gohugoio/hugo v0.143.0 github.com/golang-jwt/jwt/v4 v4.5.2 diff --git a/go.sum b/go.sum index 4d09c0ece78b8..61312a6c94daa 100644 --- a/go.sum +++ b/go.sum @@ -406,8 +406,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= -github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= From 3487f37f9af69061750c45587e9db92aa2a54d5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 09:48:35 +0000 Subject: [PATCH 027/384] chore: bump github.com/go-chi/httprate from 0.14.1 to 0.15.0 (#17171) Bumps [github.com/go-chi/httprate](https://github.com/go-chi/httprate) from 0.14.1 to 0.15.0.
    Release notes

    Sourced from github.com/go-chi/httprate's releases.

    v0.15.0

    • upgrade to xxhash v3
    Commits

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/go-chi/httprate&package-manager=go_modules&previous-version=0.14.1&new-version=0.15.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 | 4 +++- go.sum | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 28202d05d42fd..7421d224d7c5d 100644 --- a/go.mod +++ b/go.mod @@ -115,7 +115,7 @@ require ( github.com/gliderlabs/ssh v0.3.4 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 - github.com/go-chi/httprate v0.14.1 + github.com/go-chi/httprate v0.15.0 github.com/go-chi/render v1.0.1 github.com/go-jose/go-jose/v4 v4.0.5 github.com/go-logr/logr v1.4.2 @@ -483,6 +483,8 @@ require ( require github.com/mark3labs/mcp-go v0.17.0 require ( + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect ) diff --git a/go.sum b/go.sum index 61312a6c94daa..197ae825a2c5f 100644 --- a/go.sum +++ b/go.sum @@ -365,8 +365,8 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/hostrouter v0.2.0 h1:GwC7TZz8+SlJN/tV/aeJgx4F+mI5+sp+5H1PelQUjHM= github.com/go-chi/hostrouter v0.2.0/go.mod h1:pJ49vWVmtsKRKZivQx0YMYv4h0aX+Gcn6V23Np9Wf1s= -github.com/go-chi/httprate v0.14.1 h1:EKZHYEZ58Cg6hWcYzoZILsv7ppb46Wt4uQ738IRtpZs= -github.com/go-chi/httprate v0.14.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= +github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= +github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= @@ -616,6 +616,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -999,6 +1001,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.nhat.io/otelsql v0.15.0 h1:e2lpIaFPe62Pa1fXZoOWXTvMzcN4SwHwHdCz1wDUG6c= From 88b7c9ef5d0823b2f3d853a8fea887a3f612cdcc Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 8 Apr 2025 14:36:15 +0200 Subject: [PATCH 028/384] feat: install more terminal fonts (#17289) Related: https://github.com/coder/coder/issues/15024 --- coderd/apidoc/docs.go | 8 +++- coderd/apidoc/swagger.json | 12 ++++- codersdk/users.go | 13 ++++-- docs/reference/api/schemas.md | 12 ++--- site/package.json | 2 + site/pnpm-lock.yaml | 16 +++++++ site/src/api/typesGenerated.ts | 9 +++- .../AppearancePage/AppearanceForm.tsx | 8 +++- .../AppearancePage/AppearancePage.test.tsx | 44 ++++++++++++++++++- site/src/theme/constants.ts | 12 ++++- site/src/theme/globalFonts.ts | 6 ++- 11 files changed, 122 insertions(+), 20 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d4dfb80cd13b5..a4ce06d7cb2c3 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -15605,12 +15605,16 @@ const docTemplate = `{ "enum": [ "", "ibm-plex-mono", - "fira-code" + "fira-code", + "source-code-pro", + "jetbrains-mono" ], "x-enum-varnames": [ "TerminalFontUnknown", "TerminalFontIBMPlexMono", - "TerminalFontFiraCode" + "TerminalFontFiraCode", + "TerminalFontSourceCodePro", + "TerminalFontJetBrainsMono" ] }, "codersdk.TimingStage": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7e28bf764d9e7..37dbcb4b3ec02 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -14187,11 +14187,19 @@ }, "codersdk.TerminalFontName": { "type": "string", - "enum": ["", "ibm-plex-mono", "fira-code"], + "enum": [ + "", + "ibm-plex-mono", + "fira-code", + "source-code-pro", + "jetbrains-mono" + ], "x-enum-varnames": [ "TerminalFontUnknown", "TerminalFontIBMPlexMono", - "TerminalFontFiraCode" + "TerminalFontFiraCode", + "TerminalFontSourceCodePro", + "TerminalFontJetBrainsMono" ] }, "codersdk.TimingStage": { diff --git a/codersdk/users.go b/codersdk/users.go index bdc9b521367f0..ab51775e5494d 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -192,12 +192,17 @@ type ValidateUserPasswordResponse struct { // TerminalFontName is the name of supported terminal font type TerminalFontName string -var TerminalFontNames = []TerminalFontName{TerminalFontUnknown, TerminalFontIBMPlexMono, TerminalFontFiraCode} +var TerminalFontNames = []TerminalFontName{ + TerminalFontUnknown, TerminalFontIBMPlexMono, TerminalFontFiraCode, + TerminalFontSourceCodePro, TerminalFontJetBrainsMono, +} const ( - TerminalFontUnknown TerminalFontName = "" - TerminalFontIBMPlexMono TerminalFontName = "ibm-plex-mono" - TerminalFontFiraCode TerminalFontName = "fira-code" + TerminalFontUnknown TerminalFontName = "" + TerminalFontIBMPlexMono TerminalFontName = "ibm-plex-mono" + TerminalFontFiraCode TerminalFontName = "fira-code" + TerminalFontSourceCodePro TerminalFontName = "source-code-pro" + TerminalFontJetBrainsMono TerminalFontName = "jetbrains-mono" ) type UserAppearanceSettings struct { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 35f9f61f7c640..be809670a6d84 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -6723,11 +6723,13 @@ Restarts will only happen on weekdays in this list on weeks which line up with W #### Enumerated Values -| Value | -|-----------------| -| `` | -| `ibm-plex-mono` | -| `fira-code` | +| Value | +|-------------------| +| `` | +| `ibm-plex-mono` | +| `fira-code` | +| `source-code-pro` | +| `jetbrains-mono` | ## codersdk.TimingStage diff --git a/site/package.json b/site/package.json index 2b5104ddcb283..6f164005ab49e 100644 --- a/site/package.json +++ b/site/package.json @@ -44,6 +44,8 @@ "@fontsource-variable/inter": "5.1.1", "@fontsource/fira-code": "5.2.5", "@fontsource/ibm-plex-mono": "5.1.1", + "@fontsource/jetbrains-mono": "5.2.5", + "@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", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 7a6dac0d026b6..92382a11b2ad7 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -46,6 +46,12 @@ importers: '@fontsource/ibm-plex-mono': specifier: 5.1.1 version: 5.1.1 + '@fontsource/jetbrains-mono': + specifier: 5.2.5 + version: 5.2.5 + '@fontsource/source-code-pro': + specifier: 5.2.5 + version: 5.2.5 '@monaco-editor/react': specifier: 4.6.0 version: 4.6.0(monaco-editor@0.52.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1049,6 +1055,12 @@ packages: '@fontsource/ibm-plex-mono@5.1.1': resolution: {integrity: sha512-1aayqPe/ZkD3MlvqpmOHecfA3f2B8g+fAEkgvcCd3lkPP0pS1T0xG5Zmn2EsJQqr1JURtugPUH+5NqvKyfFZMQ==, tarball: https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.1.1.tgz} + '@fontsource/jetbrains-mono@5.2.5': + resolution: {integrity: sha512-TPZ9b/uq38RMdrlZZkl0RwN8Ju9JxuqMETrw76pUQFbGtE1QbwQaNsLlnUrACNNBNbd0NZRXiJJSkC8ajPgbew==, tarball: https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.5.tgz} + + '@fontsource/source-code-pro@5.2.5': + resolution: {integrity: sha512-1k7b9IdhVSdK/rJ8CkqqGFZ01C3NaXNynPZqKaTetODog/GPJiMYd6E8z+LTwSUTIX8dm2QZORDC+Uh91cjXSg==, tarball: https://registry.npmjs.org/@fontsource/source-code-pro/-/source-code-pro-5.2.5.tgz} + '@humanwhocodes/config-array@0.11.14': resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==, tarball: https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz} engines: {node: '>=10.10.0'} @@ -7022,6 +7034,10 @@ snapshots: '@fontsource/ibm-plex-mono@5.1.1': {} + '@fontsource/jetbrains-mono@5.2.5': {} + + '@fontsource/source-code-pro@5.2.5': {} + '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 1197d6b6e109e..0fd31361e69a3 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2658,11 +2658,18 @@ export interface TemplateVersionsByTemplateRequest extends Pagination { } // From codersdk/users.go -export type TerminalFontName = "fira-code" | "ibm-plex-mono" | ""; +export type TerminalFontName = + | "fira-code" + | "ibm-plex-mono" + | "jetbrains-mono" + | "source-code-pro" + | ""; export const TerminalFontNames: TerminalFontName[] = [ "fira-code", "ibm-plex-mono", + "jetbrains-mono", + "source-code-pro", "", ]; diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx index 9ecee2dfac83a..10b549d23c792 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx @@ -16,7 +16,11 @@ import { Stack } from "components/Stack/Stack"; import { ThemeOverride } from "contexts/ThemeProvider"; import type { FC } from "react"; import themes, { DEFAULT_THEME, type Theme } from "theme"; -import { DEFAULT_TERMINAL_FONT, terminalFontLabels } from "theme/constants"; +import { + DEFAULT_TERMINAL_FONT, + terminalFontLabels, + terminalFonts, +} from "theme/constants"; import { Section } from "../Section"; export interface AppearanceFormProps { @@ -115,7 +119,7 @@ export const AppearanceForm: FC = ({ value={name} control={} label={ -
    +
    {terminalFontLabels[name]}
    } diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx index 59dc62980b9f0..6f78fec6b58a0 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx @@ -51,8 +51,8 @@ describe("appearance page", () => { theme_preference: "dark", }); - const ibmPlex = await screen.findByText("Fira Code"); - await userEvent.click(ibmPlex); + const firaCode = await screen.findByText("Fira Code"); + await userEvent.click(firaCode); // Check if the API was called correctly expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(1); @@ -61,4 +61,44 @@ describe("appearance page", () => { theme_preference: "dark", }); }); + + it("changes font to fira code, then back to web terminal font", async () => { + renderWithAuth(); + + // given + jest + .spyOn(API, "updateAppearanceSettings") + .mockResolvedValueOnce({ + ...MockUser, + terminal_font: "fira-code", + theme_preference: "dark", + }) + .mockResolvedValueOnce({ + ...MockUser, + terminal_font: "ibm-plex-mono", + theme_preference: "dark", + }); + + // when + const firaCode = await screen.findByText("Fira Code"); + await userEvent.click(firaCode); + + // then + expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(1); + expect(API.updateAppearanceSettings).toHaveBeenCalledWith({ + terminal_font: "fira-code", + theme_preference: "dark", + }); + + // when + const ibmPlex = await screen.findByText("Web Terminal Font"); + await userEvent.click(ibmPlex); + + // then + expect(API.updateAppearanceSettings).toHaveBeenCalledTimes(2); + expect(API.updateAppearanceSettings).toHaveBeenNthCalledWith(2, { + terminal_font: "ibm-plex-mono", + theme_preference: "dark", + }); + }); }); diff --git a/site/src/theme/constants.ts b/site/src/theme/constants.ts index 162e67310749c..8a3c6375dce3a 100644 --- a/site/src/theme/constants.ts +++ b/site/src/theme/constants.ts @@ -7,13 +7,23 @@ export const BODY_FONT_FAMILY = `"Inter Variable", system-ui, sans-serif`; export const terminalFonts: Record = { "fira-code": MONOSPACE_FONT_FAMILY.replace("IBM Plex Mono", "Fira Code"), + "jetbrains-mono": MONOSPACE_FONT_FAMILY.replace( + "IBM Plex Mono", + "JetBrains Mono", + ), + "source-code-pro": MONOSPACE_FONT_FAMILY.replace( + "IBM Plex Mono", + "Source Code Pro", + ), "ibm-plex-mono": MONOSPACE_FONT_FAMILY, "": MONOSPACE_FONT_FAMILY, }; export const terminalFontLabels: Record = { "fira-code": "Fira Code", - "ibm-plex-mono": "IBM Plex Mono", + "jetbrains-mono": "JetBrains Mono", + "source-code-pro": "Source Code Pro", + "ibm-plex-mono": "Web Terminal Font", "": "", // needed for enum completeness, otherwise fails the build }; export const DEFAULT_TERMINAL_FONT = "ibm-plex-mono"; diff --git a/site/src/theme/globalFonts.ts b/site/src/theme/globalFonts.ts index db8089f9db266..c30bccca63d53 100644 --- a/site/src/theme/globalFonts.ts +++ b/site/src/theme/globalFonts.ts @@ -3,6 +3,10 @@ import "@fontsource/ibm-plex-mono/400.css"; import "@fontsource/ibm-plex-mono/600.css"; // Main body copy font import "@fontsource-variable/inter"; -// Alternative font for Terminal +// Alternative fonts for Terminal import "@fontsource/fira-code/400.css"; import "@fontsource/fira-code/600.css"; +import "@fontsource/source-code-pro/400.css"; +import "@fontsource/source-code-pro/600.css"; +import "@fontsource/jetbrains-mono/400.css"; +import "@fontsource/jetbrains-mono/600.css"; From b1f59aafc1fde80cbdca26d5178057d0b87c36ff Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Tue, 8 Apr 2025 17:01:21 +0400 Subject: [PATCH 029/384] fix: stop checking gauges unrelated to TestAgent_Stats_Magic (#17290) Fixes https://github.com/coder/internal/issues/564 The test is asserting too much, including stats guages that are not directly related to the thing we are trying to test: ConnectionCount, RxBytes, and TxBytes. I think the author assumed that these are counts that only go up, but they are guages and eventually zero back out, so there are race condtions where not all of them are non-zero at the same time. --- agent/agent_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/agent/agent_test.go b/agent/agent_test.go index 8ccf9b4cd7ebb..bbf0221ab5259 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -190,7 +190,7 @@ func TestAgent_Stats_Magic(t *testing.T) { s, ok := <-stats t.Logf("got stats: ok=%t, ConnectionCount=%d, RxBytes=%d, TxBytes=%d, SessionCountVSCode=%d, ConnectionMedianLatencyMS=%f", ok, s.ConnectionCount, s.RxBytes, s.TxBytes, s.SessionCountVscode, s.ConnectionMedianLatencyMs) - return ok && s.ConnectionCount > 0 && s.RxBytes > 0 && s.TxBytes > 0 && + return ok && // Ensure that the connection didn't count as a "normal" SSH session. // This was a special one, so it should be labeled specially in the stats! s.SessionCountVscode == 1 && @@ -258,8 +258,7 @@ func TestAgent_Stats_Magic(t *testing.T) { s, ok := <-stats t.Logf("got stats with conn open: ok=%t, ConnectionCount=%d, SessionCountJetBrains=%d", ok, s.ConnectionCount, s.SessionCountJetbrains) - return ok && s.ConnectionCount > 0 && - s.SessionCountJetbrains == 1 + return ok && s.SessionCountJetbrains == 1 }, testutil.WaitLong, testutil.IntervalFast, "never saw stats with conn open", ) From 44ddc9f654f8af000a359fa922b24bd18dc77c30 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 8 Apr 2025 15:35:13 +0100 Subject: [PATCH 030/384] chore(agent/agentscripts): increase timeout in TestTimeout (#17293) Fixes https://github.com/coder/internal/issues/329 This was due to a race between the process starting and the timeout of the agent startup script executor. I'm taking the 'lazy' route here and increasing the timeout to 100ms. This does technically mean that this makes the test 100 times longer to execute. However, if it takes more than 100ms to run a `sleep infinity` command on our test runner, I think we have other issues. --- agent/agentscripts/agentscripts_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/agentscripts/agentscripts_test.go b/agent/agentscripts/agentscripts_test.go index cf914daa3d09e..72554e7ef0a75 100644 --- a/agent/agentscripts/agentscripts_test.go +++ b/agent/agentscripts/agentscripts_test.go @@ -108,7 +108,7 @@ func TestTimeout(t *testing.T) { err := runner.Init([]codersdk.WorkspaceAgentScript{{ LogSourceID: uuid.New(), Script: "sleep infinity", - Timeout: time.Millisecond, + Timeout: 100 * time.Millisecond, }}, aAPI.ScriptCompleted) require.NoError(t, err) require.ErrorIs(t, runner.Execute(context.Background(), agentscripts.ExecuteAllScripts), agentscripts.ErrTimeout) From 389e88ec82aa9dfe32720879550a3fd51238e118 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 8 Apr 2025 16:53:22 +0100 Subject: [PATCH 031/384] chore(cli): refactor TestServer/Prometheus to use testutil.Eventually (#17295) Updates https://github.com/coder/internal/issues/282 Refactors existing tests to use `testutil.Eventually` which plays nicer with `testutil.Context`. --- cli/server_test.go | 88 +++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/cli/server_test.go b/cli/server_test.go index 715cbe5c7584c..c6f8231a1a1f9 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -1208,7 +1208,7 @@ func TestServer(t *testing.T) { } } return htmlFirstServedFound - }, testutil.WaitMedium, testutil.IntervalFast, "no html_first_served telemetry item") + }, testutil.WaitLong, testutil.IntervalSlow, "no html_first_served telemetry item") }) t.Run("Prometheus", func(t *testing.T) { t.Parallel() @@ -1216,9 +1216,7 @@ func TestServer(t *testing.T) { t.Run("DBMetricsDisabled", func(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() - + ctx := testutil.Context(t, testutil.WaitLong) randPort := testutil.RandomPort(t) inv, cfg := clitest.New(t, "server", @@ -1235,46 +1233,45 @@ func TestServer(t *testing.T) { clitest.Start(t, inv) _ = waitAccessURL(t, cfg) - var res *http.Response - require.Eventually(t, func() bool { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://127.0.0.1:%d", randPort), nil) - assert.NoError(t, err) + testutil.Eventually(ctx, t, func(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://127.0.0.1:%d/metrics", randPort), nil) + if err != nil { + t.Logf("error creating request: %s", err.Error()) + return false + } // nolint:bodyclose - res, err = http.DefaultClient.Do(req) + res, err := http.DefaultClient.Do(req) if err != nil { + t.Logf("error hitting prometheus endpoint: %s", err.Error()) return false } defer res.Body.Close() - scanner := bufio.NewScanner(res.Body) - hasActiveUsers := false + var activeUsersFound bool + var scannedOnce bool for scanner.Scan() { + line := scanner.Text() + if !scannedOnce { + t.Logf("scanned: %s", line) // avoid spamming logs + scannedOnce = true + } + if strings.HasPrefix(line, "coderd_db_query_latencies_seconds") { + t.Errorf("db metrics should not be tracked when --prometheus-collect-db-metrics is not enabled") + } // This metric is manually registered to be tracked in the server. That's // why we test it's tracked here. - if strings.HasPrefix(scanner.Text(), "coderd_api_active_users_duration_hour") { - hasActiveUsers = true - continue - } - if strings.HasPrefix(scanner.Text(), "coderd_db_query_latencies_seconds") { - t.Fatal("db metrics should not be tracked when --prometheus-collect-db-metrics is not enabled") + if strings.HasPrefix(line, "coderd_api_active_users_duration_hour") { + activeUsersFound = true } - t.Logf("scanned %s", scanner.Text()) - } - if scanner.Err() != nil { - t.Logf("scanner err: %s", scanner.Err().Error()) - return false } - - return hasActiveUsers - }, testutil.WaitShort, testutil.IntervalFast, "didn't find coderd_api_active_users_duration_hour in time") + return activeUsersFound + }, testutil.IntervalSlow, "didn't find coderd_api_active_users_duration_hour in time") }) t.Run("DBMetricsEnabled", func(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() - + ctx := testutil.Context(t, testutil.WaitLong) randPort := testutil.RandomPort(t) inv, cfg := clitest.New(t, "server", @@ -1291,31 +1288,34 @@ func TestServer(t *testing.T) { clitest.Start(t, inv) _ = waitAccessURL(t, cfg) - var res *http.Response - require.Eventually(t, func() bool { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://127.0.0.1:%d", randPort), nil) - assert.NoError(t, err) + testutil.Eventually(ctx, t, func(ctx context.Context) bool { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://127.0.0.1:%d/metrics", randPort), nil) + if err != nil { + t.Logf("error creating request: %s", err.Error()) + return false + } // nolint:bodyclose - res, err = http.DefaultClient.Do(req) + res, err := http.DefaultClient.Do(req) if err != nil { + t.Logf("error hitting prometheus endpoint: %s", err.Error()) return false } defer res.Body.Close() - scanner := bufio.NewScanner(res.Body) - hasDBMetrics := false + var dbMetricsFound bool + var scannedOnce bool for scanner.Scan() { - if strings.HasPrefix(scanner.Text(), "coderd_db_query_latencies_seconds") { - hasDBMetrics = true + line := scanner.Text() + if !scannedOnce { + t.Logf("scanned: %s", line) // avoid spamming logs + scannedOnce = true + } + if strings.HasPrefix(line, "coderd_db_query_latencies_seconds") { + dbMetricsFound = true } - t.Logf("scanned %s", scanner.Text()) - } - if scanner.Err() != nil { - t.Logf("scanner err: %s", scanner.Err().Error()) - return false } - return hasDBMetrics - }, testutil.WaitShort, testutil.IntervalFast, "didn't find coderd_db_query_latencies_seconds in time") + return dbMetricsFound + }, testutil.IntervalSlow, "didn't find coderd_db_query_latencies_seconds in time") }) }) t.Run("GitHubOAuth", func(t *testing.T) { From 52d555880c448ce47f737a4a649bf6a697207c9b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 8 Apr 2025 14:15:14 -0500 Subject: [PATCH 032/384] chore: add custom samesite options to auth cookies (#16885) Allows controlling `samesite` cookie settings from the deployment config --- cli/server.go | 1 - cli/testdata/coder_server_--help.golden | 3 ++ cli/testdata/server-config.yaml.golden | 3 ++ coderd/apidoc/docs.go | 17 ++++++-- coderd/apidoc/swagger.json | 17 ++++++-- coderd/apikey.go | 6 +-- coderd/coderd.go | 11 +++-- coderd/coderdtest/oidctest/idp.go | 2 +- coderd/coderdtest/testjar/cookiejar.go | 33 +++++++++++++++ coderd/httpmw/csrf.go | 4 +- coderd/httpmw/csrf_test.go | 4 +- coderd/httpmw/oauth2.go | 12 +++--- coderd/httpmw/oauth2_test.go | 23 ++++++----- coderd/userauth.go | 7 ++-- coderd/userauth_test.go | 39 ++++++++++++++++-- coderd/workspaceapps/provider.go | 5 ++- coderd/workspaceapps/proxy.go | 13 +++--- codersdk/deployment.go | 40 ++++++++++++++++++- docs/reference/api/general.md | 5 ++- docs/reference/api/schemas.md | 28 +++++++++++-- docs/reference/cli/server.md | 11 +++++ enterprise/cli/proxyserver.go | 2 +- .../cli/testdata/coder_server_--help.golden | 3 ++ enterprise/coderd/coderdenttest/proxytest.go | 2 +- enterprise/wsproxy/wsproxy.go | 8 ++-- site/src/api/typesGenerated.ts | 8 +++- 26 files changed, 240 insertions(+), 67 deletions(-) create mode 100644 coderd/coderdtest/testjar/cookiejar.go diff --git a/cli/server.go b/cli/server.go index ea6f4d665f4de..5ea0f4ebbd687 100644 --- a/cli/server.go +++ b/cli/server.go @@ -641,7 +641,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. GoogleTokenValidator: googleTokenValidator, ExternalAuthConfigs: externalAuthConfigs, RealIPConfig: realIPConfig, - SecureAuthCookie: vals.SecureAuthCookie.Value(), SSHKeygenAlgorithm: sshKeygenAlgorithm, TracerProvider: tracerProvider, Telemetry: telemetry.NewNoop(), diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 7fe70860e2e2a..1cefe8767f3b0 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -251,6 +251,9 @@ NETWORKING OPTIONS: Specifies whether to redirect requests that do not match the access URL host. + --samesite-auth-cookie lax|none, $CODER_SAMESITE_AUTH_COOKIE (default: lax) + Controls the 'SameSite' property is set on browser session cookies. + --secure-auth-cookie bool, $CODER_SECURE_AUTH_COOKIE Controls if the 'Secure' property is set on browser session cookies. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 271593f753395..911270a579457 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -174,6 +174,9 @@ networking: # Controls if the 'Secure' property is set on browser session cookies. # (default: , type: bool) secureAuthCookie: false + # Controls the 'SameSite' property is set on browser session cookies. + # (default: lax, type: enum[lax\|none]) + sameSiteAuthCookie: lax # Whether Coder only allows connections to workspaces via the browser. # (default: , type: bool) browserOnly: false diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a4ce06d7cb2c3..6bb177d699501 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11902,6 +11902,9 @@ const docTemplate = `{ "description": "HTTPAddress is a string because it may be set to zero to disable.", "type": "string" }, + "http_cookies": { + "$ref": "#/definitions/codersdk.HTTPCookieConfig" + }, "in_memory_database": { "type": "boolean" }, @@ -11962,9 +11965,6 @@ const docTemplate = `{ "scim_api_key": { "type": "string" }, - "secure_auth_cookie": { - "type": "boolean" - }, "session_lifetime": { "$ref": "#/definitions/codersdk.SessionLifetime" }, @@ -12484,6 +12484,17 @@ const docTemplate = `{ } } }, + "codersdk.HTTPCookieConfig": { + "type": "object", + "properties": { + "same_site": { + "type": "string" + }, + "secure_auth_cookie": { + "type": "boolean" + } + } + }, "codersdk.Healthcheck": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 37dbcb4b3ec02..de1d4e41c0673 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10642,6 +10642,9 @@ "description": "HTTPAddress is a string because it may be set to zero to disable.", "type": "string" }, + "http_cookies": { + "$ref": "#/definitions/codersdk.HTTPCookieConfig" + }, "in_memory_database": { "type": "boolean" }, @@ -10702,9 +10705,6 @@ "scim_api_key": { "type": "string" }, - "secure_auth_cookie": { - "type": "boolean" - }, "session_lifetime": { "$ref": "#/definitions/codersdk.SessionLifetime" }, @@ -11214,6 +11214,17 @@ } } }, + "codersdk.HTTPCookieConfig": { + "type": "object", + "properties": { + "same_site": { + "type": "string" + }, + "secure_auth_cookie": { + "type": "boolean" + } + } + }, "codersdk.Healthcheck": { "type": "object", "properties": { diff --git a/coderd/apikey.go b/coderd/apikey.go index becb9737ed62e..ddcf7767719e5 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -382,12 +382,10 @@ func (api *API) createAPIKey(ctx context.Context, params apikey.CreateParams) (* APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(newkey)}, }) - return &http.Cookie{ + return api.DeploymentValues.HTTPCookies.Apply(&http.Cookie{ Name: codersdk.SessionTokenCookie, Value: sessionToken, Path: "/", HttpOnly: true, - SameSite: http.SameSiteLaxMode, - Secure: api.SecureAuthCookie, - }, &newkey, nil + }), &newkey, nil } diff --git a/coderd/coderd.go b/coderd/coderd.go index 1eefd15a8d655..c03c77b518c05 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -155,7 +155,6 @@ type Options struct { GithubOAuth2Config *GithubOAuth2Config OIDCConfig *OIDCConfig PrometheusRegistry *prometheus.Registry - SecureAuthCookie bool StrictTransportSecurityCfg httpmw.HSTSConfig SSHKeygenAlgorithm gitsshkey.Algorithm Telemetry telemetry.Reporter @@ -740,7 +739,7 @@ func New(options *Options) *API { StatsCollector: workspaceapps.NewStatsCollector(options.WorkspaceAppsStatsCollectorOptions), DisablePathApps: options.DeploymentValues.DisablePathApps.Value(), - SecureAuthCookie: options.DeploymentValues.SecureAuthCookie.Value(), + Cookies: options.DeploymentValues.HTTPCookies, APIKeyEncryptionKeycache: options.AppEncryptionKeyCache, } @@ -828,7 +827,7 @@ func New(options *Options) *API { next.ServeHTTP(w, r) }) }, - httpmw.CSRF(options.SecureAuthCookie), + httpmw.CSRF(options.DeploymentValues.HTTPCookies), ) // This incurs a performance hit from the middleware, but is required to make sure @@ -868,7 +867,7 @@ func New(options *Options) *API { r.Route(fmt.Sprintf("/%s/callback", externalAuthConfig.ID), func(r chi.Router) { r.Use( apiKeyMiddlewareRedirect, - httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, nil), + httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil), ) r.Get("/", api.externalAuthCallback(externalAuthConfig)) }) @@ -1123,14 +1122,14 @@ func New(options *Options) *API { r.Get("/github/device", api.userOAuth2GithubDevice) r.Route("/github", func(r chi.Router) { r.Use( - httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, nil), + httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil), ) r.Get("/callback", api.userOAuth2Github) }) }) r.Route("/oidc/callback", func(r chi.Router) { r.Use( - httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, oidcAuthURLParams), + httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, oidcAuthURLParams), ) r.Get("/", api.userOIDC) }) diff --git a/coderd/coderdtest/oidctest/idp.go b/coderd/coderdtest/oidctest/idp.go index d4f24140b6726..b82f8a00dedb4 100644 --- a/coderd/coderdtest/oidctest/idp.go +++ b/coderd/coderdtest/oidctest/idp.go @@ -1320,7 +1320,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { // requests will fail. func (f *FakeIDP) HTTPClient(rest *http.Client) *http.Client { if f.serve { - if rest == nil || rest.Transport == nil { + if rest == nil { return &http.Client{} } return rest diff --git a/coderd/coderdtest/testjar/cookiejar.go b/coderd/coderdtest/testjar/cookiejar.go new file mode 100644 index 0000000000000..caec922c40ae4 --- /dev/null +++ b/coderd/coderdtest/testjar/cookiejar.go @@ -0,0 +1,33 @@ +package testjar + +import ( + "net/http" + "net/url" + "sync" +) + +func New() *Jar { + return &Jar{} +} + +// Jar exists because 'cookiejar.New()' strips many of the http.Cookie fields +// that are needed to assert. Such as 'Secure' and 'SameSite'. +type Jar struct { + m sync.Mutex + perURL map[string][]*http.Cookie +} + +func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { + j.m.Lock() + defer j.m.Unlock() + if j.perURL == nil { + j.perURL = make(map[string][]*http.Cookie) + } + j.perURL[u.Host] = append(j.perURL[u.Host], cookies...) +} + +func (j *Jar) Cookies(u *url.URL) []*http.Cookie { + j.m.Lock() + defer j.m.Unlock() + return j.perURL[u.Host] +} diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go index 8cd043146c082..41e9f87855055 100644 --- a/coderd/httpmw/csrf.go +++ b/coderd/httpmw/csrf.go @@ -16,10 +16,10 @@ import ( // for non-GET requests. // If enforce is false, then CSRF enforcement is disabled. We still want // to include the CSRF middleware because it will set the CSRF cookie. -func CSRF(secureCookie bool) func(next http.Handler) http.Handler { +func CSRF(cookieCfg codersdk.HTTPCookieConfig) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { mw := nosurf.New(next) - mw.SetBaseCookie(http.Cookie{Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: secureCookie}) + mw.SetBaseCookie(*cookieCfg.Apply(&http.Cookie{Path: "/", HttpOnly: true})) mw.SetFailureHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sessCookie, err := r.Cookie(codersdk.SessionTokenCookie) if err == nil && diff --git a/coderd/httpmw/csrf_test.go b/coderd/httpmw/csrf_test.go index 03f2babb2961a..9e8094ad50d6d 100644 --- a/coderd/httpmw/csrf_test.go +++ b/coderd/httpmw/csrf_test.go @@ -53,7 +53,7 @@ func TestCSRFExemptList(t *testing.T) { }, } - mw := httpmw.CSRF(false) + mw := httpmw.CSRF(codersdk.HTTPCookieConfig{}) csrfmw := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})).(*nosurf.CSRFHandler) for _, c := range cases { @@ -87,7 +87,7 @@ func TestCSRFError(t *testing.T) { var handler http.Handler = http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusOK) }) - handler = httpmw.CSRF(false)(handler) + handler = httpmw.CSRF(codersdk.HTTPCookieConfig{})(handler) // Not testing the error case, just providing the example of things working // to base the failure tests off of. diff --git a/coderd/httpmw/oauth2.go b/coderd/httpmw/oauth2.go index 49e98da685e0f..25bf80e934d98 100644 --- a/coderd/httpmw/oauth2.go +++ b/coderd/httpmw/oauth2.go @@ -40,7 +40,7 @@ func OAuth2(r *http.Request) OAuth2State { // a "code" URL parameter will be redirected. // AuthURLOpts are passed to the AuthCodeURL function. If this is nil, // the default option oauth2.AccessTypeOffline will be used. -func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOpts map[string]string) func(http.Handler) http.Handler { +func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, cookieCfg codersdk.HTTPCookieConfig, authURLOpts map[string]string) func(http.Handler) http.Handler { opts := make([]oauth2.AuthCodeOption, 0, len(authURLOpts)+1) opts = append(opts, oauth2.AccessTypeOffline) for k, v := range authURLOpts { @@ -118,22 +118,20 @@ func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOp } } - http.SetCookie(rw, &http.Cookie{ + http.SetCookie(rw, cookieCfg.Apply(&http.Cookie{ Name: codersdk.OAuth2StateCookie, Value: state, Path: "/", HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }) + })) // Redirect must always be specified, otherwise // an old redirect could apply! - http.SetCookie(rw, &http.Cookie{ + http.SetCookie(rw, cookieCfg.Apply(&http.Cookie{ Name: codersdk.OAuth2RedirectCookie, Value: redirect, Path: "/", HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }) + })) http.Redirect(rw, r, config.AuthCodeURL(state, opts...), http.StatusTemporaryRedirect) return diff --git a/coderd/httpmw/oauth2_test.go b/coderd/httpmw/oauth2_test.go index ca5dcf5f8a52d..9739735f3eaf7 100644 --- a/coderd/httpmw/oauth2_test.go +++ b/coderd/httpmw/oauth2_test.go @@ -50,7 +50,7 @@ func TestOAuth2(t *testing.T) { t.Parallel() req := httptest.NewRequest("GET", "/", nil) res := httptest.NewRecorder() - httpmw.ExtractOAuth2(nil, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(nil, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) require.Equal(t, http.StatusBadRequest, res.Result().StatusCode) }) t.Run("RedirectWithoutCode", func(t *testing.T) { @@ -58,7 +58,7 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?redirect="+url.QueryEscape("/dashboard"), nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) location := res.Header().Get("Location") if !assert.NotEmpty(t, location) { return @@ -82,7 +82,7 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?redirect="+url.QueryEscape(uri.String()), nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) location := res.Header().Get("Location") if !assert.NotEmpty(t, location) { return @@ -97,7 +97,7 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?code=something", nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) require.Equal(t, http.StatusBadRequest, res.Result().StatusCode) }) t.Run("NoStateCookie", func(t *testing.T) { @@ -105,7 +105,7 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?code=something&state=test", nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode) }) t.Run("MismatchedState", func(t *testing.T) { @@ -117,7 +117,7 @@ func TestOAuth2(t *testing.T) { }) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode) }) t.Run("ExchangeCodeAndState", func(t *testing.T) { @@ -133,7 +133,7 @@ func TestOAuth2(t *testing.T) { }) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { state := httpmw.OAuth2(r) require.Equal(t, "/dashboard", state.Redirect) })).ServeHTTP(res, req) @@ -144,7 +144,7 @@ func TestOAuth2(t *testing.T) { res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("foo", "bar")) authOpts := map[string]string{"foo": "bar"} - httpmw.ExtractOAuth2(tp, nil, authOpts)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, authOpts)(nil).ServeHTTP(res, req) location := res.Header().Get("Location") // Ideally we would also assert that the location contains the query params // we set in the auth URL but this would essentially be testing the oauth2 package. @@ -157,12 +157,17 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?oidc_merge_state="+customState+"&redirect="+url.QueryEscape("/dashboard"), nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{ + Secure: true, + SameSite: "none", + }, nil)(nil).ServeHTTP(res, req) found := false for _, cookie := range res.Result().Cookies() { if cookie.Name == codersdk.OAuth2StateCookie { require.Equal(t, cookie.Value, customState, "expected state") + require.Equal(t, true, cookie.Secure, "cookie set to secure") + require.Equal(t, http.SameSiteNoneMode, cookie.SameSite, "same-site = none") found = true } } diff --git a/coderd/userauth.go b/coderd/userauth.go index abbe2b4a9f2eb..91472996737aa 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -204,7 +204,7 @@ func (api *API) postConvertLoginType(rw http.ResponseWriter, r *http.Request) { Path: "/", Value: token, Expires: claims.Expiry.Time(), - Secure: api.SecureAuthCookie, + Secure: api.DeploymentValues.HTTPCookies.Secure.Value(), HttpOnly: true, // Must be SameSite to work on the redirected auth flow from the // oauth provider. @@ -1913,13 +1913,12 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C slog.F("user_id", user.ID), ) } - cookies = append(cookies, &http.Cookie{ + cookies = append(cookies, api.DeploymentValues.HTTPCookies.Apply(&http.Cookie{ Name: codersdk.SessionTokenCookie, Path: "/", MaxAge: -1, - Secure: api.SecureAuthCookie, HttpOnly: true, - }) + })) // This is intentional setting the key to the deleted old key, // as the user needs to be forced to log back in. key = *oldKey diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index ddf3dceba236f..7f6dcf771ab5d 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto" "crypto/rand" + "crypto/tls" "encoding/json" "fmt" "io" @@ -33,6 +34,7 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest/oidctest" + "github.com/coder/coder/v2/coderd/coderdtest/testjar" "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" @@ -66,8 +68,16 @@ func TestOIDCOauthLoginWithExisting(t *testing.T) { cfg.SecondaryClaims = coderd.MergedClaimsSourceNone }) + certificates := []tls.Certificate{testutil.GenerateTLSCertificate(t, "localhost")} client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ - OIDCConfig: cfg, + OIDCConfig: cfg, + TLSCertificates: certificates, + DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) { + values.HTTPCookies = codersdk.HTTPCookieConfig{ + Secure: true, + SameSite: "none", + } + }), }) const username = "alice" @@ -78,15 +88,36 @@ func TestOIDCOauthLoginWithExisting(t *testing.T) { "sub": uuid.NewString(), } - helper := oidctest.NewLoginHelper(client, fake) // Signup alice - userClient, _ := helper.Login(t, claims) + freshClient := func() *codersdk.Client { + cli := codersdk.New(client.URL) + cli.HTTPClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + //nolint:gosec + InsecureSkipVerify: true, + }, + } + cli.HTTPClient.Jar = testjar.New() + return cli + } + + unauthenticated := freshClient() + userClient, _ := fake.Login(t, unauthenticated, claims) + + cookies := unauthenticated.HTTPClient.Jar.Cookies(client.URL) + require.True(t, len(cookies) > 0) + for _, c := range cookies { + require.Truef(t, c.Secure, "cookie %q", c.Name) + require.Equalf(t, http.SameSiteNoneMode, c.SameSite, "cookie %q", c.Name) + } // Expire the link. This will force the client to refresh the token. + helper := oidctest.NewLoginHelper(userClient, fake) helper.ExpireOauthToken(t, api.Database, userClient) // Instead of refreshing, just log in again. - helper.Login(t, claims) + unauthenticated = freshClient() + fake.Login(t, unauthenticated, claims) } func TestUserLogin(t *testing.T) { diff --git a/coderd/workspaceapps/provider.go b/coderd/workspaceapps/provider.go index 1887036e35cbf..1cd652976f6f4 100644 --- a/coderd/workspaceapps/provider.go +++ b/coderd/workspaceapps/provider.go @@ -22,6 +22,7 @@ const ( type ResolveRequestOptions struct { Logger slog.Logger SignedTokenProvider SignedTokenProvider + CookieCfg codersdk.HTTPCookieConfig DashboardURL *url.URL PathAppBaseURL *url.URL @@ -75,12 +76,12 @@ func ResolveRequest(rw http.ResponseWriter, r *http.Request, opts ResolveRequest // // For subdomain apps, this applies to the entire subdomain, e.g. // app--agent--workspace--user.apps.example.com - http.SetCookie(rw, &http.Cookie{ + http.SetCookie(rw, opts.CookieCfg.Apply(&http.Cookie{ Name: codersdk.SignedAppTokenCookie, Value: tokenStr, Path: appReq.BasePath, Expires: token.Expiry.Time(), - }) + })) return token, true } diff --git a/coderd/workspaceapps/proxy.go b/coderd/workspaceapps/proxy.go index de97f6197a28c..bc8d32ed2ead9 100644 --- a/coderd/workspaceapps/proxy.go +++ b/coderd/workspaceapps/proxy.go @@ -110,8 +110,8 @@ type Server struct { // // Subdomain apps are safer with their cookies scoped to the subdomain, and XSS // calls to the dashboard are not possible due to CORs. - DisablePathApps bool - SecureAuthCookie bool + DisablePathApps bool + Cookies codersdk.HTTPCookieConfig AgentProvider AgentProvider StatsCollector *StatsCollector @@ -230,16 +230,14 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request, // We use different cookie names for path apps and for subdomain apps to // avoid both being set and sent to the server at the same time and the // server using the wrong value. - http.SetCookie(rw, &http.Cookie{ + http.SetCookie(rw, s.Cookies.Apply(&http.Cookie{ Name: AppConnectSessionTokenCookieName(accessMethod), Value: payload.APIKey, Domain: domain, Path: "/", MaxAge: 0, HttpOnly: true, - SameSite: http.SameSiteLaxMode, - Secure: s.SecureAuthCookie, - }) + })) // Strip the query parameter. path := r.URL.Path @@ -300,6 +298,7 @@ func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) // permissions to connect to a workspace. token, ok := ResolveRequest(rw, r, ResolveRequestOptions{ Logger: s.Logger, + CookieCfg: s.Cookies, SignedTokenProvider: s.SignedTokenProvider, DashboardURL: s.DashboardURL, PathAppBaseURL: s.AccessURL, @@ -405,6 +404,7 @@ func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler) token, ok := ResolveRequest(rw, r, ResolveRequestOptions{ Logger: s.Logger, + CookieCfg: s.Cookies, SignedTokenProvider: s.SignedTokenProvider, DashboardURL: s.DashboardURL, PathAppBaseURL: s.AccessURL, @@ -630,6 +630,7 @@ func (s *Server) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { appToken, ok := ResolveRequest(rw, r, ResolveRequestOptions{ Logger: s.Logger, + CookieCfg: s.Cookies, SignedTokenProvider: s.SignedTokenProvider, DashboardURL: s.DashboardURL, PathAppBaseURL: s.AccessURL, diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 089bd11567ab7..9db5a030ebc18 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -358,7 +358,7 @@ type DeploymentValues struct { Telemetry TelemetryConfig `json:"telemetry,omitempty" typescript:",notnull"` TLS TLSConfig `json:"tls,omitempty" typescript:",notnull"` Trace TraceConfig `json:"trace,omitempty" typescript:",notnull"` - SecureAuthCookie serpent.Bool `json:"secure_auth_cookie,omitempty" typescript:",notnull"` + HTTPCookies HTTPCookieConfig `json:"http_cookies,omitempty" typescript:",notnull"` StrictTransportSecurity serpent.Int64 `json:"strict_transport_security,omitempty" typescript:",notnull"` StrictTransportSecurityOptions serpent.StringArray `json:"strict_transport_security_options,omitempty" typescript:",notnull"` SSHKeygenAlgorithm serpent.String `json:"ssh_keygen_algorithm,omitempty" typescript:",notnull"` @@ -586,6 +586,30 @@ type TraceConfig struct { DataDog serpent.Bool `json:"data_dog" typescript:",notnull"` } +type HTTPCookieConfig struct { + Secure serpent.Bool `json:"secure_auth_cookie,omitempty" typescript:",notnull"` + SameSite string `json:"same_site,omitempty" typescript:",notnull"` +} + +func (cfg *HTTPCookieConfig) Apply(c *http.Cookie) *http.Cookie { + c.Secure = cfg.Secure.Value() + c.SameSite = cfg.HTTPSameSite() + return c +} + +func (cfg HTTPCookieConfig) HTTPSameSite() http.SameSite { + switch strings.ToLower(cfg.SameSite) { + case "lax": + return http.SameSiteLaxMode + case "strict": + return http.SameSiteStrictMode + case "none": + return http.SameSiteNoneMode + default: + return http.SameSiteDefaultMode + } +} + type ExternalAuthConfig struct { // Type is the type of external auth config. Type string `json:"type" yaml:"type"` @@ -2376,11 +2400,23 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Description: "Controls if the 'Secure' property is set on browser session cookies.", Flag: "secure-auth-cookie", Env: "CODER_SECURE_AUTH_COOKIE", - Value: &c.SecureAuthCookie, + Value: &c.HTTPCookies.Secure, Group: &deploymentGroupNetworking, YAML: "secureAuthCookie", Annotations: serpent.Annotations{}.Mark(annotationExternalProxies, "true"), }, + { + Name: "SameSite Auth Cookie", + Description: "Controls the 'SameSite' property is set on browser session cookies.", + Flag: "samesite-auth-cookie", + Env: "CODER_SAMESITE_AUTH_COOKIE", + // Do not allow "strict" same-site cookies. That would potentially break workspace apps. + Value: serpent.EnumOf(&c.HTTPCookies.SameSite, "lax", "none"), + Default: "lax", + Group: &deploymentGroupNetworking, + YAML: "sameSiteAuthCookie", + Annotations: serpent.Annotations{}.Mark(annotationExternalProxies, "true"), + }, { Name: "Terms of Service URL", Description: "A URL to an external Terms of Service that must be accepted by users when logging in.", diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 20372423f12ad..0db339a5baec9 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -260,6 +260,10 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "threshold_database": 0 }, "http_address": "string", + "http_cookies": { + "same_site": "string", + "secure_auth_cookie": true + }, "in_memory_database": true, "job_hang_detector_interval": 0, "logging": { @@ -433,7 +437,6 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ }, "redirect_to_access_url": true, "scim_api_key": "string", - "secure_auth_cookie": true, "session_lifetime": { "default_duration": 0, "default_token_lifetime": 0, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index be809670a6d84..8d38d0c4e346b 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1945,6 +1945,10 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "threshold_database": 0 }, "http_address": "string", + "http_cookies": { + "same_site": "string", + "secure_auth_cookie": true + }, "in_memory_database": true, "job_hang_detector_interval": 0, "logging": { @@ -2118,7 +2122,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "redirect_to_access_url": true, "scim_api_key": "string", - "secure_auth_cookie": true, "session_lifetime": { "default_duration": 0, "default_token_lifetime": 0, @@ -2422,6 +2425,10 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "threshold_database": 0 }, "http_address": "string", + "http_cookies": { + "same_site": "string", + "secure_auth_cookie": true + }, "in_memory_database": true, "job_hang_detector_interval": 0, "logging": { @@ -2595,7 +2602,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "redirect_to_access_url": true, "scim_api_key": "string", - "secure_auth_cookie": true, "session_lifetime": { "default_duration": 0, "default_token_lifetime": 0, @@ -2711,6 +2717,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `external_token_encryption_keys` | array of string | false | | | | `healthcheck` | [codersdk.HealthcheckConfig](#codersdkhealthcheckconfig) | false | | | | `http_address` | string | false | | Http address is a string because it may be set to zero to disable. | +| `http_cookies` | [codersdk.HTTPCookieConfig](#codersdkhttpcookieconfig) | false | | | | `in_memory_database` | boolean | false | | | | `job_hang_detector_interval` | integer | false | | | | `logging` | [codersdk.LoggingConfig](#codersdkloggingconfig) | false | | | @@ -2729,7 +2736,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `rate_limit` | [codersdk.RateLimitConfig](#codersdkratelimitconfig) | false | | | | `redirect_to_access_url` | boolean | false | | | | `scim_api_key` | string | false | | | -| `secure_auth_cookie` | boolean | false | | | | `session_lifetime` | [codersdk.SessionLifetime](#codersdksessionlifetime) | false | | | | `ssh_keygen_algorithm` | string | false | | | | `strict_transport_security` | integer | false | | | @@ -3298,6 +3304,22 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | » `[any property]` | array of string | false | | | | `regex_filter` | [regexp.Regexp](#regexpregexp) | false | | Regex filter is a regular expression that filters the groups returned by the OIDC provider. Any group not matched by this regex will be ignored. If the group filter is nil, then no group filtering will occur. | +## codersdk.HTTPCookieConfig + +```json +{ + "same_site": "string", + "secure_auth_cookie": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|----------------------|---------|----------|--------------|-------------| +| `same_site` | string | false | | | +| `secure_auth_cookie` | boolean | false | | | + ## codersdk.Healthcheck ```json diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index f55165bb397da..1b4052e335e66 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -992,6 +992,17 @@ Type of auth to use when connecting to postgres. For AWS RDS, using IAM authenti Controls if the 'Secure' property is set on browser session cookies. +### --samesite-auth-cookie + +| | | +|-------------|--------------------------------------------| +| Type | lax\|none | +| Environment | $CODER_SAMESITE_AUTH_COOKIE | +| YAML | networking.sameSiteAuthCookie | +| Default | lax | + +Controls the 'SameSite' property is set on browser session cookies. + ### --terms-of-service-url | | | diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go index ec77936accd12..35f0986614840 100644 --- a/enterprise/cli/proxyserver.go +++ b/enterprise/cli/proxyserver.go @@ -264,7 +264,7 @@ func (r *RootCmd) proxyServer() *serpent.Command { Tracing: tracer, PrometheusRegistry: prometheusRegistry, APIRateLimit: int(cfg.RateLimit.API.Value()), - SecureAuthCookie: cfg.SecureAuthCookie.Value(), + CookieConfig: cfg.HTTPCookies, DisablePathApps: cfg.DisablePathApps.Value(), ProxySessionToken: proxySessionToken.Value(), AllowAllCors: cfg.Dangerous.AllowAllCors.Value(), diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 8f383e145aa94..d11304742d974 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -252,6 +252,9 @@ NETWORKING OPTIONS: Specifies whether to redirect requests that do not match the access URL host. + --samesite-auth-cookie lax|none, $CODER_SAMESITE_AUTH_COOKIE (default: lax) + Controls the 'SameSite' property is set on browser session cookies. + --secure-auth-cookie bool, $CODER_SECURE_AUTH_COOKIE Controls if the 'Secure' property is set on browser session cookies. diff --git a/enterprise/coderd/coderdenttest/proxytest.go b/enterprise/coderd/coderdenttest/proxytest.go index 089bb7c2be99b..5aaaf4a88a725 100644 --- a/enterprise/coderd/coderdenttest/proxytest.go +++ b/enterprise/coderd/coderdenttest/proxytest.go @@ -156,7 +156,7 @@ func NewWorkspaceProxyReplica(t *testing.T, coderdAPI *coderd.API, owner *coders RealIPConfig: coderdAPI.RealIPConfig, Tracing: coderdAPI.TracerProvider, APIRateLimit: coderdAPI.APIRateLimit, - SecureAuthCookie: coderdAPI.SecureAuthCookie, + CookieConfig: coderdAPI.DeploymentValues.HTTPCookies, ProxySessionToken: token, DisablePathApps: options.DisablePathApps, // We need a new registry to not conflict with the coderd internal diff --git a/enterprise/wsproxy/wsproxy.go b/enterprise/wsproxy/wsproxy.go index 9108283513e4f..5dbf8ab6ea24d 100644 --- a/enterprise/wsproxy/wsproxy.go +++ b/enterprise/wsproxy/wsproxy.go @@ -70,7 +70,7 @@ type Options struct { TLSCertificates []tls.Certificate APIRateLimit int - SecureAuthCookie bool + CookieConfig codersdk.HTTPCookieConfig DisablePathApps bool DERPEnabled bool DERPServerRelayAddress string @@ -310,8 +310,8 @@ func New(ctx context.Context, opts *Options) (*Server, error) { Logger: s.Logger.Named("proxy_token_provider"), }, - DisablePathApps: opts.DisablePathApps, - SecureAuthCookie: opts.SecureAuthCookie, + DisablePathApps: opts.DisablePathApps, + Cookies: opts.CookieConfig, AgentProvider: agentProvider, StatsCollector: workspaceapps.NewStatsCollector(opts.StatsCollectorOptions), @@ -362,7 +362,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) { }, // CSRF is required here because we need to set the CSRF cookies on // responses. - httpmw.CSRF(s.Options.SecureAuthCookie), + httpmw.CSRF(s.Options.CookieConfig), ) // Attach workspace apps routes. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0fd31361e69a3..09da288ceeb76 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -649,7 +649,7 @@ export interface DeploymentValues { readonly telemetry?: TelemetryConfig; readonly tls?: TLSConfig; readonly trace?: TraceConfig; - readonly secure_auth_cookie?: boolean; + readonly http_cookies?: HTTPCookieConfig; readonly strict_transport_security?: number; readonly strict_transport_security_options?: string; readonly ssh_keygen_algorithm?: string; @@ -976,6 +976,12 @@ export interface GroupSyncSettings { readonly legacy_group_name_mapping?: Record; } +// From codersdk/deployment.go +export interface HTTPCookieConfig { + readonly secure_auth_cookie?: boolean; + readonly same_site?: string; +} + // From health/model.go export type HealthCode = | "EACS03" From f2d24bc3f4ea27bc4a9ed53a8b5949a984da3e81 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 8 Apr 2025 16:39:21 -0500 Subject: [PATCH 033/384] chore: add custom samesite options to auth cookies (#16885) Allows controlling `samesite` cookie settings from deployment values From 0e658219b2a88d879efb7d95bca9b4d7d755c625 Mon Sep 17 00:00:00 2001 From: Utsavkumar Lal <36764273+utsavll0@users.noreply.github.com> Date: Tue, 8 Apr 2025 23:59:41 -0400 Subject: [PATCH 034/384] feat: support filtering users table by login type (#17238) #15896 Mentions ability to add support for filtering by login type The issue mentions that backend API support exists but the backend did not seem to have the support for this filter. So I have added the ability to filter it. I also added a corresponding update to readme file to make sure the docs will correctly showcase this feature --- coderd/database/dbmem/dbmem.go | 12 +++ coderd/database/modelqueries.go | 1 + coderd/database/queries.sql.go | 12 ++- coderd/database/queries/users.sql | 6 ++ coderd/searchquery/search.go | 1 + coderd/searchquery/search_test.go | 88 ++++++++++++++++------ coderd/users.go | 1 + coderd/users_test.go | 120 ++++++++++++++++++++++++++++++ codersdk/users.go | 6 +- docs/admin/users/index.md | 3 + 10 files changed, 226 insertions(+), 24 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index d21da315ffa85..deafdc42e0216 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6824,6 +6824,18 @@ func (q *FakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams users = usersFilteredByRole } + if len(params.LoginType) > 0 { + usersFilteredByLoginType := make([]database.User, 0, len(users)) + for i, user := range users { + if slice.ContainsCompare(params.LoginType, user.LoginType, func(a, b database.LoginType) bool { + return strings.EqualFold(string(a), string(b)) + }) { + usersFilteredByLoginType = append(usersFilteredByLoginType, users[i]) + } + } + users = usersFilteredByLoginType + } + if !params.CreatedBefore.IsZero() { usersFilteredByCreatedAt := make([]database.User, 0, len(users)) for i, user := range users { diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 3c437cde293d3..1bf37ce0c09e6 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -395,6 +395,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, arg.CreatedAfter, arg.IncludeSystem, arg.GithubComUserID, + pq.Array(arg.LoginType), arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 55a3bd27e5e3f..b93ad49f8f9d4 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12410,16 +12410,22 @@ WHERE github_com_user_id = $10 ELSE true END + -- Filter by login_type + AND CASE + WHEN cardinality($11 :: login_type[]) > 0 THEN + login_type = ANY($11 :: login_type[]) + ELSE true + END -- End of filters -- Authorize Filter clause will be injected below in GetAuthorizedUsers -- @authorize_filter ORDER BY -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. - LOWER(username) ASC OFFSET $11 + LOWER(username) ASC OFFSET $12 LIMIT -- A null limit means "no limit", so 0 means return all - NULLIF($12 :: int, 0) + NULLIF($13 :: int, 0) ` type GetUsersParams struct { @@ -12433,6 +12439,7 @@ type GetUsersParams struct { CreatedAfter time.Time `db:"created_after" json:"created_after"` IncludeSystem bool `db:"include_system" json:"include_system"` GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"` + LoginType []LoginType `db:"login_type" json:"login_type"` OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` LimitOpt int32 `db:"limit_opt" json:"limit_opt"` } @@ -12472,6 +12479,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse arg.CreatedAfter, arg.IncludeSystem, arg.GithubComUserID, + pq.Array(arg.LoginType), arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 0bac76c8df14a..8757b377728a3 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -260,6 +260,12 @@ WHERE github_com_user_id = @github_com_user_id ELSE true END + -- Filter by login_type + AND CASE + WHEN cardinality(@login_type :: login_type[]) > 0 THEN + login_type = ANY(@login_type :: login_type[]) + ELSE true + END -- End of filters -- Authorize Filter clause will be injected below in GetAuthorizedUsers diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 938f725330cd0..6f4a1c337c535 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -88,6 +88,7 @@ func Users(query string) (database.GetUsersParams, []codersdk.ValidationError) { CreatedAfter: parser.Time3339Nano(values, time.Time{}, "created_after"), CreatedBefore: parser.Time3339Nano(values, time.Time{}, "created_before"), GithubComUserID: parser.Int64(values, 0, "github_com_user_id"), + LoginType: httpapi.ParseCustomList(parser, values, []database.LoginType{}, "login_type", httpapi.ParseEnum[database.LoginType]), } parser.ErrorExcessParams(values) return filter, parser.Errors diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go index 0a8e08e3d45fe..065937f389e4a 100644 --- a/coderd/searchquery/search_test.go +++ b/coderd/searchquery/search_test.go @@ -386,62 +386,69 @@ func TestSearchUsers(t *testing.T) { Name: "Empty", Query: "", Expected: database.GetUsersParams{ - Status: []database.UserStatus{}, - RbacRole: []string{}, + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, }, }, { Name: "Username", Query: "user-name", Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{}, - RbacRole: []string{}, + Search: "user-name", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, }, }, { Name: "UsernameWithSpaces", Query: " user-name ", Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{}, - RbacRole: []string{}, + Search: "user-name", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, }, }, { Name: "Username+Param", Query: "usEr-name stAtus:actiVe", Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{database.UserStatusActive}, - RbacRole: []string{}, + Search: "user-name", + Status: []database.UserStatus{database.UserStatusActive}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, }, }, { Name: "OnlyParams", Query: "status:acTIve sEArch:User-Name role:Owner", Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{database.UserStatusActive}, - RbacRole: []string{codersdk.RoleOwner}, + Search: "user-name", + Status: []database.UserStatus{database.UserStatusActive}, + RbacRole: []string{codersdk.RoleOwner}, + LoginType: []database.LoginType{}, }, }, { Name: "QuotedParam", Query: `status:SuSpenDeD sEArch:"User Name" role:meMber`, Expected: database.GetUsersParams{ - Search: "user name", - Status: []database.UserStatus{database.UserStatusSuspended}, - RbacRole: []string{codersdk.RoleMember}, + Search: "user name", + Status: []database.UserStatus{database.UserStatusSuspended}, + RbacRole: []string{codersdk.RoleMember}, + LoginType: []database.LoginType{}, }, }, { Name: "QuotedKey", Query: `"status":acTIve "sEArch":User-Name "role":Owner`, Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{database.UserStatusActive}, - RbacRole: []string{codersdk.RoleOwner}, + Search: "user-name", + Status: []database.UserStatus{database.UserStatusActive}, + RbacRole: []string{codersdk.RoleOwner}, + LoginType: []database.LoginType{}, }, }, { @@ -449,9 +456,48 @@ func TestSearchUsers(t *testing.T) { Name: "QuotedSpecial", Query: `search:"user:name"`, Expected: database.GetUsersParams{ - Search: "user:name", + Search: "user:name", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, + }, + }, + { + Name: "LoginType", + Query: "login_type:github", + Expected: database.GetUsersParams{ + Search: "", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{database.LoginTypeGithub}, + }, + }, + { + Name: "MultipleLoginTypesWithSpaces", + Query: "login_type:github login_type:password", + Expected: database.GetUsersParams{ + Search: "", Status: []database.UserStatus{}, RbacRole: []string{}, + LoginType: []database.LoginType{ + database.LoginTypeGithub, + database.LoginTypePassword, + }, + }, + }, + { + Name: "MultipleLoginTypesWithCommas", + Query: "login_type:github,password,none,oidc", + Expected: database.GetUsersParams{ + Search: "", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{ + database.LoginTypeGithub, + database.LoginTypePassword, + database.LoginTypeNone, + database.LoginTypeOIDC, + }, }, }, diff --git a/coderd/users.go b/coderd/users.go index 03f900c01ddeb..9b6407156cfa1 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -307,6 +307,7 @@ func (api *API) GetUsers(rw http.ResponseWriter, r *http.Request) ([]database.Us CreatedAfter: params.CreatedAfter, CreatedBefore: params.CreatedBefore, GithubComUserID: params.GithubComUserID, + LoginType: params.LoginType, // #nosec G115 - Pagination offsets are small and fit in int32 OffsetOpt: int32(paginationParams.Offset), // #nosec G115 - Pagination limits are small and fit in int32 diff --git a/coderd/users_test.go b/coderd/users_test.go index fdaad21a826a9..e32b6d0c5b927 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1902,6 +1902,126 @@ func TestGetUsers(t *testing.T) { require.Len(t, res.Users, 1) require.Equal(t, res.Users[0].ID, first.UserID) }) + + t.Run("LoginTypeNoneFilter", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "bob@email.com", + Username: "bob", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) + + res, err := client.Users(ctx, codersdk.UsersRequest{ + LoginType: []codersdk.LoginType{codersdk.LoginTypeNone}, + }) + require.NoError(t, err) + require.Len(t, res.Users, 1) + require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeNone) + }) + + t.Run("LoginTypeMultipleFilter", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + filtered := make([]codersdk.User, 0) + + bob, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "bob@email.com", + Username: "bob", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) + filtered = append(filtered, bob) + + charlie, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "charlie@email.com", + Username: "charlie", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeGithub, + }) + require.NoError(t, err) + filtered = append(filtered, charlie) + + res, err := client.Users(ctx, codersdk.UsersRequest{ + LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub}, + }) + require.NoError(t, err) + require.Len(t, res.Users, 2) + require.ElementsMatch(t, filtered, res.Users) + }) + + t.Run("DormantUserWithLoginTypeNone", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "bob@email.com", + Username: "bob", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) + + _, err = client.UpdateUserStatus(ctx, "bob", codersdk.UserStatusSuspended) + require.NoError(t, err) + + res, err := client.Users(ctx, codersdk.UsersRequest{ + Status: codersdk.UserStatusSuspended, + LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub}, + }) + require.NoError(t, err) + require.Len(t, res.Users, 1) + require.Equal(t, res.Users[0].Username, "bob") + require.Equal(t, res.Users[0].Status, codersdk.UserStatusSuspended) + require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeNone) + }) + + t.Run("LoginTypeOidcFromMultipleUser", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{ + OIDCConfig: &coderd.OIDCConfig{ + AllowSignups: true, + }, + }) + first := coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "bob@email.com", + Username: "bob", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeOIDC, + }) + require.NoError(t, err) + + for i := range 5 { + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: fmt.Sprintf("%d@coder.com", i), + Username: fmt.Sprintf("user%d", i), + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) + } + + res, err := client.Users(ctx, codersdk.UsersRequest{ + LoginType: []codersdk.LoginType{codersdk.LoginTypeOIDC}, + }) + require.NoError(t, err) + require.Len(t, res.Users, 1) + require.Equal(t, res.Users[0].Username, "bob") + require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeOIDC) + }) } func TestGetUsersPagination(t *testing.T) { diff --git a/codersdk/users.go b/codersdk/users.go index ab51775e5494d..3d9d95e683066 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -28,7 +28,8 @@ type UsersRequest struct { // Filter users by status. Status UserStatus `json:"status,omitempty" typescript:"-"` // Filter users that have the given role. - Role string `json:"role,omitempty" typescript:"-"` + Role string `json:"role,omitempty" typescript:"-"` + LoginType []LoginType `json:"login_type,omitempty" typescript:"-"` SearchQuery string `json:"q,omitempty"` Pagination @@ -755,6 +756,9 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, if req.SearchQuery != "" { params = append(params, req.SearchQuery) } + for _, lt := range req.LoginType { + params = append(params, "login_type:"+string(lt)) + } q.Set("q", strings.Join(params, " ")) r.URL.RawQuery = q.Encode() }, diff --git a/docs/admin/users/index.md b/docs/admin/users/index.md index ed7fbdebd4c5f..af26f4bb62a2b 100644 --- a/docs/admin/users/index.md +++ b/docs/admin/users/index.md @@ -190,6 +190,8 @@ to use the Coder's filter query: `status:active last_seen_before:"2023-07-01T00:00:00Z"` - To find users who were created between January 1 and January 18, 2023: `created_before:"2023-01-18T00:00:00Z" created_after:"2023-01-01T23:59:59Z"` +- To find users who login using Github: + `login_type:github` The following filters are supported: @@ -203,3 +205,4 @@ The following filters are supported: the RFC3339Nano format. - `created_before` and `created_after` - The time a user was created. Uses the RFC3339Nano format. +- `login_type` - Represents the login type of the user. Refer to the [LoginType documentation](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#LoginType) for a list of supported values From 8d122aa4abd21df927bb94677549bb0128a4f1b1 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 9 Apr 2025 09:20:47 +0100 Subject: [PATCH 035/384] chore(cli): avoid use of testutil.RandomPort() in prometheus test (#17297) Should hopefully fix https://github.com/coder/internal/issues/282 Instead of picking a random port for the prometheus server, listen on `:0` and read the port from the CLI stdout. --- cli/agent.go | 15 ++++++++++----- cli/server_test.go | 35 +++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/cli/agent.go b/cli/agent.go index bf189a4fc57c2..18c4542a6c3a0 100644 --- a/cli/agent.go +++ b/cli/agent.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "net" "net/http" "net/http/pprof" "net/url" @@ -491,8 +492,6 @@ func (r *RootCmd) workspaceAgent() *serpent.Command { } func ServeHandler(ctx context.Context, logger slog.Logger, handler http.Handler, addr, name string) (closeFunc func()) { - logger.Debug(ctx, "http server listening", slog.F("addr", addr), slog.F("name", name)) - // ReadHeaderTimeout is purposefully not enabled. It caused some issues with // websockets over the dev tunnel. // See: https://github.com/coder/coder/pull/3730 @@ -502,9 +501,15 @@ func ServeHandler(ctx context.Context, logger slog.Logger, handler http.Handler, Handler: handler, } go func() { - err := srv.ListenAndServe() - if err != nil && !xerrors.Is(err, http.ErrServerClosed) { - logger.Error(ctx, "http server listen", slog.F("name", name), slog.Error(err)) + ln, err := net.Listen("tcp", addr) + if err != nil { + logger.Error(ctx, "http server listen", slog.F("name", name), slog.F("addr", addr), slog.Error(err)) + return + } + defer ln.Close() + logger.Info(ctx, "http server listening", slog.F("addr", ln.Addr()), slog.F("name", name)) + if err := srv.Serve(ln); err != nil && !xerrors.Is(err, http.ErrServerClosed) { + logger.Error(ctx, "http server serve", slog.F("addr", ln.Addr()), slog.F("name", name), slog.Error(err)) } }() diff --git a/cli/server_test.go b/cli/server_test.go index c6f8231a1a1f9..e4d71e0c3f794 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -22,6 +22,7 @@ import ( "os" "path/filepath" "reflect" + "regexp" "runtime" "strconv" "strings" @@ -1217,24 +1218,31 @@ func TestServer(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) - randPort := testutil.RandomPort(t) - inv, cfg := clitest.New(t, + inv, _ := clitest.New(t, "server", "--in-memory", "--http-address", ":0", "--access-url", "http://example.com", "--provisioner-daemons", "1", "--prometheus-enable", - "--prometheus-address", ":"+strconv.Itoa(randPort), + "--prometheus-address", ":0", // "--prometheus-collect-db-metrics", // disabled by default "--cache-dir", t.TempDir(), ) + pty := ptytest.New(t) + inv.Stdout = pty.Output() + inv.Stderr = pty.Output() + clitest.Start(t, inv) - _ = waitAccessURL(t, cfg) + + // Wait until we see the prometheus address in the logs. + addrMatchExpr := `http server listening\s+addr=(\S+)\s+name=prometheus` + lineMatch := pty.ExpectRegexMatchContext(ctx, addrMatchExpr) + promAddr := regexp.MustCompile(addrMatchExpr).FindStringSubmatch(lineMatch)[1] testutil.Eventually(ctx, t, func(ctx context.Context) bool { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://127.0.0.1:%d/metrics", randPort), nil) + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://%s/metrics", promAddr), nil) if err != nil { t.Logf("error creating request: %s", err.Error()) return false @@ -1272,24 +1280,31 @@ func TestServer(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) - randPort := testutil.RandomPort(t) - inv, cfg := clitest.New(t, + inv, _ := clitest.New(t, "server", "--in-memory", "--http-address", ":0", "--access-url", "http://example.com", "--provisioner-daemons", "1", "--prometheus-enable", - "--prometheus-address", ":"+strconv.Itoa(randPort), + "--prometheus-address", ":0", "--prometheus-collect-db-metrics", "--cache-dir", t.TempDir(), ) + pty := ptytest.New(t) + inv.Stdout = pty.Output() + inv.Stderr = pty.Output() + clitest.Start(t, inv) - _ = waitAccessURL(t, cfg) + + // Wait until we see the prometheus address in the logs. + addrMatchExpr := `http server listening\s+addr=(\S+)\s+name=prometheus` + lineMatch := pty.ExpectRegexMatchContext(ctx, addrMatchExpr) + promAddr := regexp.MustCompile(addrMatchExpr).FindStringSubmatch(lineMatch)[1] testutil.Eventually(ctx, t, func(ctx context.Context) bool { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://127.0.0.1:%d/metrics", randPort), nil) + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("http://%s/metrics", promAddr), nil) if err != nil { t.Logf("error creating request: %s", err.Error()) return false From a8fbe71a22fdc9dfd607d3ebc93c0d469cede735 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 9 Apr 2025 09:21:17 +0100 Subject: [PATCH 036/384] chore(cli): increase healthcheck timeout in TestSupportbundle (#17291) Fixes https://github.com/coder/internal/issues/272 * Increases healthcheck timeout in tests. This seems to be the most usual cause of test failures. * Adds a non-nilness check before caching a healthcheck report. * Modifies the HTTP response code to 503 (was 404) when no healthcheck report is available. 503 seems to be a better indicator of the server state in this case, whereas 404 could be misinterpreted as a typo in the healthcheck URL. --- cli/support_test.go | 9 ++++++--- coderd/debug.go | 6 ++++-- coderd/debug_test.go | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cli/support_test.go b/cli/support_test.go index 1fb336142d4be..e1ad7fca7b0a4 100644 --- a/cli/support_test.go +++ b/cli/support_test.go @@ -50,7 +50,8 @@ func TestSupportBundle(t *testing.T) { secretValue := uuid.NewString() seedSecretDeploymentOptions(t, &dc, secretValue) client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ - DeploymentValues: dc.Values, + DeploymentValues: dc.Values, + HealthcheckTimeout: testutil.WaitSuperLong, }) owner := coderdtest.CreateFirstUser(t, client) r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ @@ -113,7 +114,8 @@ func TestSupportBundle(t *testing.T) { secretValue := uuid.NewString() seedSecretDeploymentOptions(t, &dc, secretValue) client := coderdtest.New(t, &coderdtest.Options{ - DeploymentValues: dc.Values, + DeploymentValues: dc.Values, + HealthcheckTimeout: testutil.WaitSuperLong, }) _ = coderdtest.CreateFirstUser(t, client) @@ -133,7 +135,8 @@ func TestSupportBundle(t *testing.T) { secretValue := uuid.NewString() seedSecretDeploymentOptions(t, &dc, secretValue) client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ - DeploymentValues: dc.Values, + DeploymentValues: dc.Values, + HealthcheckTimeout: testutil.WaitSuperLong, }) admin := coderdtest.CreateFirstUser(t, client) r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ diff --git a/coderd/debug.go b/coderd/debug.go index 0ae62282a22d8..64c7c9e632d0a 100644 --- a/coderd/debug.go +++ b/coderd/debug.go @@ -84,13 +84,15 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) { defer cancel() report := api.HealthcheckFunc(ctx, apiKey) - api.healthCheckCache.Store(report) + if report != nil { // Only store non-nil reports. + api.healthCheckCache.Store(report) + } return report, nil }) select { case <-ctx.Done(): - httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ + httpapi.Write(ctx, rw, http.StatusServiceUnavailable, codersdk.Response{ Message: "Healthcheck is in progress and did not complete in time. Try again in a few seconds.", }) return diff --git a/coderd/debug_test.go b/coderd/debug_test.go index 0d5dfd1885f12..f7a0a180ec61d 100644 --- a/coderd/debug_test.go +++ b/coderd/debug_test.go @@ -117,7 +117,7 @@ func TestDebugHealth(t *testing.T) { require.NoError(t, err) defer res.Body.Close() _, _ = io.ReadAll(res.Body) - require.Equal(t, http.StatusNotFound, res.StatusCode) + require.Equal(t, http.StatusServiceUnavailable, res.StatusCode) }) t.Run("Refresh", func(t *testing.T) { From 43b1a034b10799a369e2b3010c26386d39ec7cf4 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 9 Apr 2025 09:23:43 +0100 Subject: [PATCH 037/384] chore(agent/agentscripts): disable TestTimeout on macOS (#17300) --- agent/agentscripts/agentscripts_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/agent/agentscripts/agentscripts_test.go b/agent/agentscripts/agentscripts_test.go index 72554e7ef0a75..0100f399c5eff 100644 --- a/agent/agentscripts/agentscripts_test.go +++ b/agent/agentscripts/agentscripts_test.go @@ -102,6 +102,9 @@ func TestEnv(t *testing.T) { func TestTimeout(t *testing.T) { t.Parallel() + if runtime.GOOS == "darwin" { + t.Skip("this test is flaky on macOS, see https://github.com/coder/internal/issues/329") + } runner := setup(t, nil) defer runner.Close() aAPI := agenttest.NewFakeAgentAPI(t, testutil.Logger(t), nil, nil) From 3f3e2017bdda832ca29f30cbe0e6839ceb278730 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Wed, 9 Apr 2025 15:19:48 +0400 Subject: [PATCH 038/384] fix: fix http cache dir creation order in coderdtest (#17303) fixes coder/internal#565 Fixes the ordering of creating the HTTP cache temp dir with respect to starting the Coderd HTTP server, so that they are cleaned up in the correct (reverse) order. --- coderd/coderdtest/coderdtest.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index b9097863a5f67..0f0a99807a37d 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -405,6 +405,12 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can workspacestats.TrackerWithTickFlush(options.WorkspaceUsageTrackerTick, options.WorkspaceUsageTrackerFlush), ) + // create the TempDir for the HTTP file cache BEFORE we start the server and set a t.Cleanup to close it. TempDir() + // registers a Cleanup function that deletes the directory, and Cleanup functions are called in reverse order. If + // we don't do this, then we could try to delete the directory before the HTTP server is done with all files in it, + // which on Windows will fail (can't delete files until all programs have closed handles to them). + cacheDir := t.TempDir() + var mutex sync.RWMutex var handler http.Handler srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -515,7 +521,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can AppHostname: options.AppHostname, AppHostnameRegex: appHostnameRegex, Logger: *options.Logger, - CacheDir: t.TempDir(), + CacheDir: cacheDir, RuntimeConfig: runtimeManager, Database: options.Database, Pubsub: options.Pubsub, From 109e73bf977fb66b39fe841f0502ce0e7694df26 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 9 Apr 2025 11:16:00 -0400 Subject: [PATCH 039/384] docs: add details on external authentication priority (#17164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Issue Closes #16875 Clarify how Coder authentication works with Git providers, particularly the order of authentication methods used. ## Changes Made I've updated the External Authentication documentation to: 1. Clarify that Coder first attempts to use external auth provider tokens when available, and only defaults to SSH authentication if no tokens are available 2. Add more detailed explanations about both authentication methods 3. Improve the description of how the `coder gitssh` command works with existing and Coder-generated SSH keys ## Verification Claude verified that this accurately describes the behavior of the codebase by reviewing the `gitssh.go` implementation, which shows how Coder handles SSH authentication as a fallback when external auth is not available. [preview](https://coder.com/docs/@16875-git-workspace-auth/admin/external-auth) 🤖 Generated with https://claude.ai/code --------- Signed-off-by: dependabot[bot] Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Co-authored-by: Ben Potter Co-authored-by: M Atif Ali Co-authored-by: Bruno Quaresma Co-authored-by: Kyle Carberry Co-authored-by: Cian Johnston Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jon Ayers Co-authored-by: Hugo Dutka Co-authored-by: Ethan <39577870+ethanndickson@users.noreply.github.com> Co-authored-by: Michael Smith Co-authored-by: Claude Co-authored-by: Sas Swart --- docs/admin/external-auth.md | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/admin/external-auth.md b/docs/admin/external-auth.md index d894f77bac764..6c91a5891f2db 100644 --- a/docs/admin/external-auth.md +++ b/docs/admin/external-auth.md @@ -71,6 +71,55 @@ Use [`external-auth`](../reference/cli/external-auth.md) in the Coder CLI to acc coder external-auth access-token ``` +## Git Authentication in Workspaces + +Coder provides automatic Git authentication for workspaces through SSH authentication and Git-provider specific env variables. + +When performing Git operations, Coder first attempts to use external auth provider tokens if available. +If no tokens are available, it defaults to SSH authentication. + +### OAuth (external auth) + +For Git providers configured with [external authentication](#configuration), Coder can use OAuth tokens for Git operations. + +When Git operations require authentication, and no SSH key is configured, Coder will automatically use the appropriate external auth provider based on the repository URL. + +For example, if you've configured a GitHub external auth provider and attempt to clone a GitHub repository, Coder will use the OAuth token from that provider for authentication. + +To manually access these tokens within a workspace: + +```shell +coder external-auth access-token +``` + +### SSH Authentication + +Coder automatically generates an SSH key pair for each user that can be used for Git operations. +When you use SSH URLs for Git repositories, for example, `git@github.com:organization/repo.git`, Coder checks for and uses an existing SSH key. +If one is not available, it uses the Coder-generated one. + +The `coder gitssh` command wraps the standard `ssh` command and injects the SSH key during Git operations. +This works automatically when you: + +1. Clone a repository using SSH URLs +1. Pull/push changes to remote repositories +1. Use any Git command that requires SSH authentication + +You must add the SSH key to your Git provider. + +#### Add your Coder SSH key to your Git provider + +1. View your Coder Git SSH key: + + ```shell + coder publickey + ``` + +1. Add the key to your Git provider accounts: + + - [GitHub](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account#adding-a-new-ssh-key-to-your-account) + - [GitLab](https://docs.gitlab.com/user/ssh/#add-an-ssh-key-to-your-gitlab-account) + ## Git-provider specific env variables ### Azure DevOps From 98c05b356881c11d2d411bc78143b402388696a7 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 9 Apr 2025 20:28:32 +0300 Subject: [PATCH 040/384] test(agent/agentssh): fix macos signal flake during close (#17313) Fixes coder/internal#558 --- agent/agentssh/agentssh_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/agent/agentssh/agentssh_test.go b/agent/agentssh/agentssh_test.go index 69f92e0fd31a0..ae1aaa92f2ffd 100644 --- a/agent/agentssh/agentssh_test.go +++ b/agent/agentssh/agentssh_test.go @@ -13,6 +13,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/prometheus/client_golang/prometheus" "github.com/spf13/afero" @@ -200,7 +201,11 @@ func TestNewServer_CloseActiveConnections(t *testing.T) { } assert.NoError(t, err) + // Allow the session to settle (i.e. reach echo). pty.ExpectMatchContext(ctx, "started") + // Sleep a bit to ensure the sleep has started. + time.Sleep(testutil.IntervalMedium) + close(ch) err = sess.Wait() From d17bcc727ba1f9a60689d16c4453352e2a5d9598 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 9 Apr 2025 14:53:20 -0400 Subject: [PATCH 041/384] docs: update note markdown in parameters (#17318) Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/admin/templates/extending-templates/parameters.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/admin/templates/extending-templates/parameters.md b/docs/admin/templates/extending-templates/parameters.md index 4cb9e786d642e..5db1473cec3ec 100644 --- a/docs/admin/templates/extending-templates/parameters.md +++ b/docs/admin/templates/extending-templates/parameters.md @@ -293,10 +293,11 @@ data "coder_parameter" "instances" { } ``` -**NOTE:** as of -[`terraform-provider-coder` v0.19.0](https://registry.terraform.io/providers/coder/coder/0.19.0/docs), -`options` can be specified in `number` parameters; this also works with -validations such as `monotonic`. +> [!NOTE] +> As of +> [`terraform-provider-coder` v0.19.0](https://registry.terraform.io/providers/coder/coder/0.19.0/docs), +> `options` can be specified in `number` parameters; this also works with +> validations such as `monotonic`. ### String From a03a54dd148e31e509e1b37c19f6d4d163411fde Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 9 Apr 2025 15:17:02 -0400 Subject: [PATCH 042/384] fix(site): resolve all `Array.prototype.toSorted` and `Array.prototype.sort` bugs (#17307) Closes https://github.com/coder/coder/issues/16759 ## Changes made - Replaced all instances of `Array.prototype.toSorted` with `Array.prototype.sort` to provide better support for older browsers - Updated all `Array.prototype.sort` calls where necessary to remove risks of mutation render bugs - Refactored some code (moved things around, added comments) to make it more clear that certain `.sort` calls are harmless and don't have any risks --- site/src/api/queries/organizations.ts | 4 +- .../modules/dashboard/Navbar/proxyUtils.tsx | 2 +- .../workspaces/WorkspaceTiming/Chart/utils.ts | 6 +- .../StarterTemplates.tsx | 2 +- .../LicensesSettingsPageView.tsx | 2 +- site/src/pages/HealthPage/DERPPage.tsx | 2 +- .../CustomRolesPage/CustomRolesPageView.tsx | 4 +- .../TemplateInsightsPage.tsx | 146 +++++++++--------- site/src/pages/WorkspacePage/AppStatuses.tsx | 3 +- 9 files changed, 85 insertions(+), 86 deletions(-) diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index aa3b700a2cf43..632b5f0c730ad 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -270,7 +270,7 @@ export const organizationsPermissions = ( } return { - queryKey: ["organizations", organizationIds.sort(), "permissions"], + queryKey: ["organizations", [...organizationIds.sort()], "permissions"], queryFn: async () => { // Only request what we need for the sidebar, which is one edit permission // per sub-link (settings, groups, roles, and members pages) that tells us @@ -316,7 +316,7 @@ export const workspacePermissionsByOrganization = ( } return { - queryKey: ["workspaces", organizationIds.sort(), "permissions"], + queryKey: ["workspaces", [...organizationIds.sort()], "permissions"], queryFn: async () => { const prefixedChecks = organizationIds.flatMap((orgId) => Object.entries(workspacePermissionChecks(orgId, userId)).map( diff --git a/site/src/modules/dashboard/Navbar/proxyUtils.tsx b/site/src/modules/dashboard/Navbar/proxyUtils.tsx index 57afadb7fbdd9..674c62ef38f1e 100644 --- a/site/src/modules/dashboard/Navbar/proxyUtils.tsx +++ b/site/src/modules/dashboard/Navbar/proxyUtils.tsx @@ -4,7 +4,7 @@ export function sortProxiesByLatency( proxies: Proxies, latencies: ProxyLatencies, ) { - return proxies.toSorted((a, b) => { + return [...proxies].sort((a, b) => { const latencyA = latencies?.[a.id]?.latencyMS ?? Number.POSITIVE_INFINITY; const latencyB = latencies?.[b.id]?.latencyMS ?? Number.POSITIVE_INFINITY; return latencyA - latencyB; diff --git a/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts b/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts index 9721e9f0d1317..45c6f5bf681d1 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts +++ b/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts @@ -13,9 +13,9 @@ export const mergeTimeRanges = (ranges: TimeRange[]): TimeRange => { .sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime()); const start = sortedDurations[0].startedAt; - const sortedEndDurations = ranges - .slice() - .sort((a, b) => a.endedAt.getTime() - b.endedAt.getTime()); + const sortedEndDurations = [...ranges].sort( + (a, b) => a.endedAt.getTime() - b.endedAt.getTime(), + ); const end = sortedEndDurations[sortedEndDurations.length - 1].endedAt; return { startedAt: start, endedAt: end }; }; diff --git a/site/src/pages/CreateTemplateGalleryPage/StarterTemplates.tsx b/site/src/pages/CreateTemplateGalleryPage/StarterTemplates.tsx index f242f13d429fa..ade9bf5f9df52 100644 --- a/site/src/pages/CreateTemplateGalleryPage/StarterTemplates.tsx +++ b/site/src/pages/CreateTemplateGalleryPage/StarterTemplates.tsx @@ -26,7 +26,7 @@ const sortVisibleTemplates = (templates: TemplateExample[]) => { // The docker template should be the first template in the list, // as it's the easiest way to get started with Coder. const dockerTemplateId = "docker"; - return templates.sort((a, b) => { + return [...templates].sort((a, b) => { if (a.id === dockerTemplateId) { return -1; } diff --git a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx index 589a693d44dd8..c4152c7b8f565 100644 --- a/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/LicensesSettingsPage/LicensesSettingsPageView.tsx @@ -99,7 +99,7 @@ const LicensesSettingsPageView: FC = ({ {!isLoading && licenses && licenses?.length > 0 && ( - {licenses + {[...(licenses ?? [])] ?.sort( (a, b) => new Date(b.claims.license_expires).valueOf() - diff --git a/site/src/pages/HealthPage/DERPPage.tsx b/site/src/pages/HealthPage/DERPPage.tsx index 3daa403c99f36..b866bc9b01210 100644 --- a/site/src/pages/HealthPage/DERPPage.tsx +++ b/site/src/pages/HealthPage/DERPPage.tsx @@ -91,7 +91,7 @@ export const DERPPage: FC = () => {
    Regions
    - {Object.values(regions!) + {Object.values(regions ?? {}) .filter((region) => { // Values can technically be null return region !== null; diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx index dfbfa5029cbde..d6af718cd1a8b 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPageView.tsx @@ -170,8 +170,8 @@ const RoleTable: FC = ({ - {roles - ?.sort((a, b) => a.name.localeCompare(b.name)) + {[...(roles ?? [])] + .sort((a, b) => a.name.localeCompare(b.name)) .map((role) => ( = ({ ...panelProps }) => { const theme = useTheme(); - const validUsage = data?.filter((u) => u.seconds > 0); + const validUsage = data + ?.filter((u) => u.seconds > 0) + .sort((a, b) => b.seconds - a.seconds); const totalInSeconds = validUsage?.reduce((total, usage) => total + usage.seconds, 0) ?? 1; const usageColors = chroma @@ -438,86 +440,82 @@ const TemplateUsagePanel: FC = ({ gap: 24, }} > - {validUsage - .sort((a, b) => b.seconds - a.seconds) - .map((usage, i) => { - const percentage = (usage.seconds / totalInSeconds) * 100; - return ( -
    + {validUsage.map((usage, i) => { + const percentage = (usage.seconds / totalInSeconds) * 100; + return ( +
    +
    -
    - -
    -
    - {usage.display_name} -
    -
    - - - - +
    + {usage.display_name} +
    +
    + + - {formatTime(usage.seconds)} - {usage.times_used > 0 && ( - - Opened {usage.times_used.toLocaleString()}{" "} - {usage.times_used === 1 ? "time" : "times"} - - )} - -
    - ); - })} + /> + + + {formatTime(usage.seconds)} + {usage.times_used > 0 && ( + + Opened {usage.times_used.toLocaleString()}{" "} + {usage.times_used === 1 ? "time" : "times"} + + )} + +
    + ); + })}
    )} diff --git a/site/src/pages/WorkspacePage/AppStatuses.tsx b/site/src/pages/WorkspacePage/AppStatuses.tsx index cee2ed33069ae..95afb422de30b 100644 --- a/site/src/pages/WorkspacePage/AppStatuses.tsx +++ b/site/src/pages/WorkspacePage/AppStatuses.tsx @@ -165,7 +165,8 @@ export const AppStatuses: FC = ({ })), ); - // 2. Sort statuses chronologically (newest first) + // 2. Sort statuses chronologically (newest first) - mutating the value is + // fine since it's not an outside parameter allStatuses.sort( (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), From 0b58798a1aa99adcde519da34996ce7e3c2c8049 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 9 Apr 2025 14:35:43 -0500 Subject: [PATCH 043/384] feat: remove site wide perms from creating a workspace (#17296) Creating a workspace required `read` on site wide `user`. Only organization permissions should be required. --- coderd/coderd.go | 116 ++++++++------- coderd/coderdtest/authorize.go | 18 ++- coderd/httpapi/noop.go | 10 ++ coderd/httpmw/organizationparam.go | 2 +- coderd/httpmw/userparam.go | 29 +++- coderd/rbac/object.go | 23 +++ coderd/workspaces.go | 206 +++++++++++++++++---------- enterprise/coderd/workspaces_test.go | 125 ++++++++++++++++ 8 files changed, 393 insertions(+), 136 deletions(-) create mode 100644 coderd/httpapi/noop.go diff --git a/coderd/coderd.go b/coderd/coderd.go index c03c77b518c05..ff566ed369a15 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1146,64 +1146,74 @@ func New(options *Options) *API { r.Get("/", api.AssignableSiteRoles) }) r.Route("/{user}", func(r chi.Router) { - r.Use(httpmw.ExtractUserParam(options.Database)) - r.Post("/convert-login", api.postConvertLoginType) - r.Delete("/", api.deleteUser) - r.Get("/", api.userByName) - r.Get("/autofill-parameters", api.userAutofillParameters) - r.Get("/login-type", api.userLoginType) - r.Put("/profile", api.putUserProfile) - r.Route("/status", func(r chi.Router) { - r.Put("/suspend", api.putSuspendUserAccount()) - r.Put("/activate", api.putActivateUserAccount()) + r.Group(func(r chi.Router) { + r.Use(httpmw.ExtractUserParamOptional(options.Database)) + // Creating workspaces does not require permissions on the user, only the + // organization member. This endpoint should match the authz story of + // postWorkspacesByOrganization + r.Post("/workspaces", api.postUserWorkspaces) }) - r.Get("/appearance", api.userAppearanceSettings) - r.Put("/appearance", api.putUserAppearanceSettings) - r.Route("/password", func(r chi.Router) { - r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute)) - r.Put("/", api.putUserPassword) - }) - // These roles apply to the site wide permissions. - r.Put("/roles", api.putUserRoles) - r.Get("/roles", api.userRoles) - - r.Route("/keys", func(r chi.Router) { - r.Post("/", api.postAPIKey) - r.Route("/tokens", func(r chi.Router) { - r.Post("/", api.postToken) - r.Get("/", api.tokens) - r.Get("/tokenconfig", api.tokenConfig) - r.Route("/{keyname}", func(r chi.Router) { - r.Get("/", api.apiKeyByName) - }) + + r.Group(func(r chi.Router) { + r.Use(httpmw.ExtractUserParam(options.Database)) + + r.Post("/convert-login", api.postConvertLoginType) + r.Delete("/", api.deleteUser) + r.Get("/", api.userByName) + r.Get("/autofill-parameters", api.userAutofillParameters) + r.Get("/login-type", api.userLoginType) + r.Put("/profile", api.putUserProfile) + r.Route("/status", func(r chi.Router) { + r.Put("/suspend", api.putSuspendUserAccount()) + r.Put("/activate", api.putActivateUserAccount()) }) - r.Route("/{keyid}", func(r chi.Router) { - r.Get("/", api.apiKeyByID) - r.Delete("/", api.deleteAPIKey) + r.Get("/appearance", api.userAppearanceSettings) + r.Put("/appearance", api.putUserAppearanceSettings) + r.Route("/password", func(r chi.Router) { + r.Use(httpmw.RateLimit(options.LoginRateLimit, time.Minute)) + r.Put("/", api.putUserPassword) + }) + // These roles apply to the site wide permissions. + r.Put("/roles", api.putUserRoles) + r.Get("/roles", api.userRoles) + + r.Route("/keys", func(r chi.Router) { + r.Post("/", api.postAPIKey) + r.Route("/tokens", func(r chi.Router) { + r.Post("/", api.postToken) + r.Get("/", api.tokens) + r.Get("/tokenconfig", api.tokenConfig) + r.Route("/{keyname}", func(r chi.Router) { + r.Get("/", api.apiKeyByName) + }) + }) + r.Route("/{keyid}", func(r chi.Router) { + r.Get("/", api.apiKeyByID) + r.Delete("/", api.deleteAPIKey) + }) }) - }) - r.Route("/organizations", func(r chi.Router) { - r.Get("/", api.organizationsByUser) - r.Get("/{organizationname}", api.organizationByUserAndName) - }) - r.Post("/workspaces", api.postUserWorkspaces) - r.Route("/workspace/{workspacename}", func(r chi.Router) { - r.Get("/", api.workspaceByOwnerAndName) - r.Get("/builds/{buildnumber}", api.workspaceBuildByBuildNumber) - }) - r.Get("/gitsshkey", api.gitSSHKey) - r.Put("/gitsshkey", api.regenerateGitSSHKey) - r.Route("/notifications", func(r chi.Router) { - r.Route("/preferences", func(r chi.Router) { - r.Get("/", api.userNotificationPreferences) - r.Put("/", api.putUserNotificationPreferences) + r.Route("/organizations", func(r chi.Router) { + r.Get("/", api.organizationsByUser) + r.Get("/{organizationname}", api.organizationByUserAndName) + }) + r.Route("/workspace/{workspacename}", func(r chi.Router) { + r.Get("/", api.workspaceByOwnerAndName) + r.Get("/builds/{buildnumber}", api.workspaceBuildByBuildNumber) + }) + r.Get("/gitsshkey", api.gitSSHKey) + r.Put("/gitsshkey", api.regenerateGitSSHKey) + r.Route("/notifications", func(r chi.Router) { + r.Route("/preferences", func(r chi.Router) { + r.Get("/", api.userNotificationPreferences) + r.Put("/", api.putUserNotificationPreferences) + }) + }) + r.Route("/webpush", func(r chi.Router) { + r.Post("/subscription", api.postUserWebpushSubscription) + r.Delete("/subscription", api.deleteUserWebpushSubscription) + r.Post("/test", api.postUserPushNotificationTest) }) - }) - r.Route("/webpush", func(r chi.Router) { - r.Post("/subscription", api.postUserWebpushSubscription) - r.Delete("/subscription", api.deleteUserWebpushSubscription) - r.Post("/test", api.postUserPushNotificationTest) }) }) }) diff --git a/coderd/coderdtest/authorize.go b/coderd/coderdtest/authorize.go index af52f7fc70f53..279405c4e6a21 100644 --- a/coderd/coderdtest/authorize.go +++ b/coderd/coderdtest/authorize.go @@ -81,7 +81,7 @@ func AssertRBAC(t *testing.T, api *coderd.API, client *codersdk.Client) RBACAsse // Note that duplicate rbac calls are handled by the rbac.Cacher(), but // will be recorded twice. So AllCalls() returns calls regardless if they // were returned from the cached or not. -func (a RBACAsserter) AllCalls() []AuthCall { +func (a RBACAsserter) AllCalls() AuthCalls { return a.Recorder.AllCalls(&a.Subject) } @@ -140,8 +140,11 @@ func (a RBACAsserter) Reset() RBACAsserter { return a } +type AuthCalls []AuthCall + type AuthCall struct { rbac.AuthCall + Err error asserted bool // callers is a small stack trace for debugging. @@ -252,7 +255,7 @@ func (r *RecordingAuthorizer) AssertActor(t *testing.T, actor rbac.Subject, did } // recordAuthorize is the internal method that records the Authorize() call. -func (r *RecordingAuthorizer) recordAuthorize(subject rbac.Subject, action policy.Action, object rbac.Object) { +func (r *RecordingAuthorizer) recordAuthorize(subject rbac.Subject, action policy.Action, object rbac.Object, authzErr error) { r.Lock() defer r.Unlock() @@ -262,6 +265,7 @@ func (r *RecordingAuthorizer) recordAuthorize(subject rbac.Subject, action polic Action: action, Object: object, }, + Err: authzErr, callers: []string{ // This is a decent stack trace for debugging. // Some dbauthz calls are a bit nested, so we skip a few. @@ -288,11 +292,12 @@ func caller(skip int) string { } func (r *RecordingAuthorizer) Authorize(ctx context.Context, subject rbac.Subject, action policy.Action, object rbac.Object) error { - r.recordAuthorize(subject, action, object) if r.Wrapped == nil { panic("Developer error: RecordingAuthorizer.Wrapped is nil") } - return r.Wrapped.Authorize(ctx, subject, action, object) + authzErr := r.Wrapped.Authorize(ctx, subject, action, object) + r.recordAuthorize(subject, action, object, authzErr) + return authzErr } func (r *RecordingAuthorizer) Prepare(ctx context.Context, subject rbac.Subject, action policy.Action, objectType string) (rbac.PreparedAuthorized, error) { @@ -339,10 +344,11 @@ func (s *PreparedRecorder) Authorize(ctx context.Context, object rbac.Object) er s.rw.Lock() defer s.rw.Unlock() + authzErr := s.prepped.Authorize(ctx, object) if !s.usingSQL { - s.rec.recordAuthorize(s.subject, s.action, object) + s.rec.recordAuthorize(s.subject, s.action, object, authzErr) } - return s.prepped.Authorize(ctx, object) + return authzErr } func (s *PreparedRecorder) CompileToSQL(ctx context.Context, cfg regosql.ConvertConfig) (string, error) { diff --git a/coderd/httpapi/noop.go b/coderd/httpapi/noop.go new file mode 100644 index 0000000000000..52a0f5dd4d8a4 --- /dev/null +++ b/coderd/httpapi/noop.go @@ -0,0 +1,10 @@ +package httpapi + +import "net/http" + +// NoopResponseWriter is a response writer that does nothing. +type NoopResponseWriter struct{} + +func (NoopResponseWriter) Header() http.Header { return http.Header{} } +func (NoopResponseWriter) Write(p []byte) (int, error) { return len(p), nil } +func (NoopResponseWriter) WriteHeader(int) {} diff --git a/coderd/httpmw/organizationparam.go b/coderd/httpmw/organizationparam.go index 18938ec1e792d..782a0d37e1985 100644 --- a/coderd/httpmw/organizationparam.go +++ b/coderd/httpmw/organizationparam.go @@ -117,7 +117,7 @@ func ExtractOrganizationMemberParam(db database.Store) func(http.Handler) http.H // very important that we do not add the User object to the request context or otherwise // leak it to the API handler. // nolint:gocritic - user, ok := extractUserContext(dbauthz.AsSystemRestricted(ctx), db, rw, r) + user, ok := ExtractUserContext(dbauthz.AsSystemRestricted(ctx), db, rw, r) if !ok { return } diff --git a/coderd/httpmw/userparam.go b/coderd/httpmw/userparam.go index 03bff9bbb5596..2fbcc458489f9 100644 --- a/coderd/httpmw/userparam.go +++ b/coderd/httpmw/userparam.go @@ -31,13 +31,18 @@ func UserParam(r *http.Request) database.User { return user } +func UserParamOptional(r *http.Request) (database.User, bool) { + user, ok := r.Context().Value(userParamContextKey{}).(database.User) + return user, ok +} + // ExtractUserParam extracts a user from an ID/username in the {user} URL // parameter. func ExtractUserParam(db database.Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - user, ok := extractUserContext(ctx, db, rw, r) + user, ok := ExtractUserContext(ctx, db, rw, r) if !ok { // response already handled return @@ -48,15 +53,31 @@ func ExtractUserParam(db database.Store) func(http.Handler) http.Handler { } } -// extractUserContext queries the database for the parameterized `{user}` from the request URL. -func extractUserContext(ctx context.Context, db database.Store, rw http.ResponseWriter, r *http.Request) (user database.User, ok bool) { +// ExtractUserParamOptional does not fail if no user is present. +func ExtractUserParamOptional(db database.Store) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + user, ok := ExtractUserContext(ctx, db, &httpapi.NoopResponseWriter{}, r) + if ok { + ctx = context.WithValue(ctx, userParamContextKey{}, user) + } + + next.ServeHTTP(rw, r.WithContext(ctx)) + }) + } +} + +// ExtractUserContext queries the database for the parameterized `{user}` from the request URL. +func ExtractUserContext(ctx context.Context, db database.Store, rw http.ResponseWriter, r *http.Request) (user database.User, ok bool) { // userQuery is either a uuid, a username, or 'me' userQuery := chi.URLParam(r, "user") if userQuery == "" { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: "\"user\" must be provided.", }) - return database.User{}, true + return database.User{}, false } if userQuery == "me" { diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 4f42de94a4c52..9beef03dd8f9a 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -1,10 +1,14 @@ package rbac import ( + "fmt" + "strings" + "github.com/google/uuid" "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/rbac/policy" + cstrings "github.com/coder/coder/v2/coderd/util/strings" ) // ResourceUserObject is a helper function to create a user object for authz checks. @@ -37,6 +41,25 @@ type Object struct { ACLGroupList map[string][]policy.Action ` json:"acl_group_list"` } +// String is not perfect, but decent enough for human display +func (z Object) String() string { + var parts []string + if z.OrgID != "" { + parts = append(parts, fmt.Sprintf("org:%s", cstrings.Truncate(z.OrgID, 4))) + } + if z.Owner != "" { + parts = append(parts, fmt.Sprintf("owner:%s", cstrings.Truncate(z.Owner, 4))) + } + parts = append(parts, z.Type) + if z.ID != "" { + parts = append(parts, fmt.Sprintf("id:%s", cstrings.Truncate(z.ID, 4))) + } + if len(z.ACLGroupList) > 0 || len(z.ACLUserList) > 0 { + parts = append(parts, fmt.Sprintf("acl:%d", len(z.ACLUserList)+len(z.ACLGroupList))) + } + return strings.Join(parts, ".") +} + // ValidAction checks if the action is valid for the given object type. func (z Object) ValidAction(action policy.Action) error { perms, ok := policy.RBACPermissions[z.Type] diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 6b010b53020a3..d49de2388af59 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -406,31 +406,84 @@ func (api *API) postUserWorkspaces(rw http.ResponseWriter, r *http.Request) { ctx = r.Context() apiKey = httpmw.APIKey(r) auditor = api.Auditor.Load() - user = httpmw.UserParam(r) ) + var req codersdk.CreateWorkspaceRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + var owner workspaceOwner + // This user fetch is an optimization path for the most common case of creating a + // workspace for 'Me'. + // + // This is also required to allow `owners` to create workspaces for users + // that are not in an organization. + user, ok := httpmw.UserParamOptional(r) + if ok { + owner = workspaceOwner{ + ID: user.ID, + Username: user.Username, + AvatarURL: user.AvatarURL, + } + } else { + // A workspace can still be created if the caller can read the organization + // member. The organization is required, which can be sourced from the + // template. + // + // TODO: This code gets called twice for each workspace build request. + // This is inefficient and costs at most 2 extra RTTs to the DB. + // This can be optimized. It exists as it is now for code simplicity. + // The most common case is to create a workspace for 'Me'. Which does + // not enter this code branch. + template, ok := requestTemplate(ctx, rw, req, api.Database) + if !ok { + return + } + + // We need to fetch the original user as a system user to fetch the + // user_id. 'ExtractUserContext' handles all cases like usernames, + // 'Me', etc. + // nolint:gocritic // The user_id needs to be fetched. This handles all those cases. + user, ok := httpmw.ExtractUserContext(dbauthz.AsSystemRestricted(ctx), api.Database, rw, r) + if !ok { + return + } + + organizationMember, err := database.ExpectOne(api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{ + OrganizationID: template.OrganizationID, + UserID: user.ID, + IncludeSystem: false, + })) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching organization member.", + Detail: err.Error(), + }) + return + } + owner = workspaceOwner{ + ID: organizationMember.OrganizationMember.UserID, + Username: organizationMember.Username, + AvatarURL: organizationMember.AvatarURL, + } + } + aReq, commitAudit := audit.InitRequest[database.WorkspaceTable](rw, &audit.RequestParams{ Audit: *auditor, Log: api.Logger, Request: r, Action: database.AuditActionCreate, AdditionalFields: audit.AdditionalFields{ - WorkspaceOwner: user.Username, + WorkspaceOwner: owner.Username, }, }) defer commitAudit() - - var req codersdk.CreateWorkspaceRequest - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - owner := workspaceOwner{ - ID: user.ID, - Username: user.Username, - AvatarURL: user.AvatarURL, - } createWorkspace(ctx, aReq, apiKey.UserID, api, owner, req, rw, r) } @@ -450,65 +503,8 @@ func createWorkspace( rw http.ResponseWriter, r *http.Request, ) { - // If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it. - templateID := req.TemplateID - if templateID == uuid.Nil { - templateVersion, err := api.Database.GetTemplateVersionByID(ctx, req.TemplateVersionID) - if httpapi.Is404Error(err) { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Template version %q doesn't exist.", templateID.String()), - Validations: []codersdk.ValidationError{{ - Field: "template_version_id", - Detail: "template not found", - }}, - }) - return - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching template version.", - Detail: err.Error(), - }) - return - } - if templateVersion.Archived { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Archived template versions cannot be used to make a workspace.", - Validations: []codersdk.ValidationError{ - { - Field: "template_version_id", - Detail: "template version archived", - }, - }, - }) - return - } - - templateID = templateVersion.TemplateID.UUID - } - - template, err := api.Database.GetTemplateByID(ctx, templateID) - if httpapi.Is404Error(err) { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Template %q doesn't exist.", templateID.String()), - Validations: []codersdk.ValidationError{{ - Field: "template_id", - Detail: "template not found", - }}, - }) - return - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching template.", - Detail: err.Error(), - }) - return - } - if template.Deleted { - httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ - Message: fmt.Sprintf("Template %q has been deleted!", template.Name), - }) + template, ok := requestTemplate(ctx, rw, req, api.Database) + if !ok { return } @@ -776,6 +772,72 @@ func createWorkspace( httpapi.Write(ctx, rw, http.StatusCreated, w) } +func requestTemplate(ctx context.Context, rw http.ResponseWriter, req codersdk.CreateWorkspaceRequest, db database.Store) (database.Template, bool) { + // If we were given a `TemplateVersionID`, we need to determine the `TemplateID` from it. + templateID := req.TemplateID + + if templateID == uuid.Nil { + templateVersion, err := db.GetTemplateVersionByID(ctx, req.TemplateVersionID) + if httpapi.Is404Error(err) { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Template version %q doesn't exist.", req.TemplateVersionID), + Validations: []codersdk.ValidationError{{ + Field: "template_version_id", + Detail: "template not found", + }}, + }) + return database.Template{}, false + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching template version.", + Detail: err.Error(), + }) + return database.Template{}, false + } + if templateVersion.Archived { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Archived template versions cannot be used to make a workspace.", + Validations: []codersdk.ValidationError{ + { + Field: "template_version_id", + Detail: "template version archived", + }, + }, + }) + return database.Template{}, false + } + + templateID = templateVersion.TemplateID.UUID + } + + template, err := db.GetTemplateByID(ctx, templateID) + if httpapi.Is404Error(err) { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Template %q doesn't exist.", templateID), + Validations: []codersdk.ValidationError{{ + Field: "template_id", + Detail: "template not found", + }}, + }) + return database.Template{}, false + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching template.", + Detail: err.Error(), + }) + return database.Template{}, false + } + if template.Deleted { + httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ + Message: fmt.Sprintf("Template %q has been deleted!", template.Name), + }) + return database.Template{}, false + } + return template, true +} + func (api *API) notifyWorkspaceCreated( ctx context.Context, receiverID uuid.UUID, diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index eedd6f1bcfa1c..72859c5460fa7 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -31,6 +31,7 @@ import ( "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/rbac/policy" agplschedule "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/schedule/cron" "github.com/coder/coder/v2/coderd/util/ptr" @@ -245,7 +246,131 @@ func TestCreateWorkspace(t *testing.T) { func TestCreateUserWorkspace(t *testing.T) { t.Parallel() + // Create a custom role that can create workspaces for another user. + t.Run("ForAnotherUser", func(t *testing.T) { + t.Parallel() + + owner, first := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureTemplateRBAC: 1, + }, + }, + }) + ctx := testutil.Context(t, testutil.WaitShort) + //nolint:gocritic // using owner to setup roles + r, err := owner.CreateOrganizationRole(ctx, codersdk.Role{ + Name: "creator", + OrganizationID: first.OrganizationID.String(), + DisplayName: "Creator", + OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + codersdk.ResourceWorkspace: {codersdk.ActionCreate, codersdk.ActionWorkspaceStart, codersdk.ActionUpdate, codersdk.ActionRead}, + codersdk.ResourceOrganizationMember: {codersdk.ActionRead}, + }), + }) + require.NoError(t, err) + + // use admin for setting up test + admin, adminID := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleTemplateAdmin()) + + // try the test action with this user & custom role + creator, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleMember(), rbac.RoleIdentifier{ + Name: r.Name, + OrganizationID: first.OrganizationID, + }) + + version := coderdtest.CreateTemplateVersion(t, admin, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, admin, version.ID) + template := coderdtest.CreateTemplate(t, admin, first.OrganizationID, version.ID) + + ctx = testutil.Context(t, testutil.WaitLong*1000) // Reset the context to avoid timeouts. + + _, err = creator.CreateUserWorkspace(ctx, adminID.ID.String(), codersdk.CreateWorkspaceRequest{ + TemplateID: template.ID, + Name: "workspace", + }) + require.NoError(t, err) + }) + + // Asserting some authz calls when creating a workspace. + t.Run("AuthzStory", func(t *testing.T) { + t.Parallel() + owner, _, api, first := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureCustomRoles: 1, + codersdk.FeatureTemplateRBAC: 1, + }, + }, + }) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong*2000) + defer cancel() + + //nolint:gocritic // using owner to setup roles + creatorRole, err := owner.CreateOrganizationRole(ctx, codersdk.Role{ + Name: "creator", + OrganizationID: first.OrganizationID.String(), + OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{ + codersdk.ResourceWorkspace: {codersdk.ActionCreate, codersdk.ActionWorkspaceStart, codersdk.ActionUpdate, codersdk.ActionRead}, + codersdk.ResourceOrganizationMember: {codersdk.ActionRead}, + }), + }) + require.NoError(t, err) + + version := coderdtest.CreateTemplateVersion(t, owner, first.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, owner, version.ID) + template := coderdtest.CreateTemplate(t, owner, first.OrganizationID, version.ID) + _, userID := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID) + creator, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleIdentifier{ + Name: creatorRole.Name, + OrganizationID: first.OrganizationID, + }) + + // Create a workspace with the current api using an org admin. + authz := coderdtest.AssertRBAC(t, api.AGPL, creator) + authz.Reset() // Reset all previous checks done in setup. + _, err = creator.CreateUserWorkspace(ctx, userID.ID.String(), codersdk.CreateWorkspaceRequest{ + TemplateID: template.ID, + Name: "test-user", + }) + require.NoError(t, err) + + // Assert all authz properties + t.Run("OnlyOrganizationAuthzCalls", func(t *testing.T) { + // Creating workspaces is an organization action. So organization + // permissions should be sufficient to complete the action. + for _, call := range authz.AllCalls() { + if call.Action == policy.ActionRead && + call.Object.Equal(rbac.ResourceUser.WithOwner(userID.ID.String()).WithID(userID.ID)) { + // User read checks are called. If they fail, ignore them. + if call.Err != nil { + continue + } + } + + if call.Object.Type == rbac.ResourceDeploymentConfig.Type { + continue // Ignore + } + + assert.Falsef(t, call.Object.OrgID == "", + "call %q for object %q has no organization set. Site authz calls not expected here", + call.Action, call.Object.String(), + ) + } + }) + }) + t.Run("NoTemplateAccess", func(t *testing.T) { + // NoTemplateAccess intentionally does not use provisioners. The template + // version will be stuck in 'pending' forever. t.Parallel() client, first := coderdenttest.New(t, &coderdenttest.Options{ From f2fb0caf46188da70c4d508be5bb7817b8dfb6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Wed, 9 Apr 2025 14:35:10 -0700 Subject: [PATCH 044/384] chore: remove usage of github.com/go-chi/render (#17324) --- provisionersdk/agent_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/provisionersdk/agent_test.go b/provisionersdk/agent_test.go index b415b2396f94b..cd642d6765269 100644 --- a/provisionersdk/agent_test.go +++ b/provisionersdk/agent_test.go @@ -21,7 +21,6 @@ import ( "testing" "time" - "github.com/go-chi/render" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/testutil" @@ -141,8 +140,8 @@ func serveScript(t *testing.T, in string) string { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - render.Status(r, http.StatusOK) - render.Data(rw, r, []byte(in)) + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte(in)) })) t.Cleanup(srv.Close) srvURL, err := url.Parse(srv.URL) From 8faaa148205fb16ea99c35c80ba51c43de6c4f6d Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Wed, 9 Apr 2025 22:50:15 -0400 Subject: [PATCH 045/384] chore: update Terraform to 1.11.4 (#17323) Co-authored-by: Claude --- .github/actions/setup-tf/action.yaml | 2 +- dogfood/coder/Dockerfile | 4 ++-- install.sh | 2 +- provisioner/terraform/install.go | 2 +- .../terraform/testdata/resources/presets/presets.tfplan.json | 4 ++-- .../terraform/testdata/resources/presets/presets.tfstate.json | 2 +- provisioner/terraform/testdata/resources/version.txt | 2 +- provisioner/terraform/testdata/version.txt | 2 +- scripts/Dockerfile.base | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/actions/setup-tf/action.yaml b/.github/actions/setup-tf/action.yaml index 43c7264b8f4b0..a29d107826ad8 100644 --- a/.github/actions/setup-tf/action.yaml +++ b/.github/actions/setup-tf/action.yaml @@ -7,5 +7,5 @@ runs: - name: Install Terraform uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: - terraform_version: 1.11.3 + terraform_version: 1.11.4 terraform_wrapper: false diff --git a/dogfood/coder/Dockerfile b/dogfood/coder/Dockerfile index fb3bc15e04836..b17d4c49563d3 100644 --- a/dogfood/coder/Dockerfile +++ b/dogfood/coder/Dockerfile @@ -198,9 +198,9 @@ RUN apt-get update --quiet && apt-get install --yes \ # Configure FIPS-compliant policies update-crypto-policies --set FIPS -# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.11.3. +# NOTE: In scripts/Dockerfile.base we specifically install Terraform version 1.11.4. # 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" && \ +RUN wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.11.4/terraform_1.11.4_linux_amd64.zip" && \ unzip /tmp/terraform.zip -d /usr/local/bin && \ rm -f /tmp/terraform.zip && \ chmod +x /usr/local/bin/terraform && \ diff --git a/install.sh b/install.sh index f725141c1c27a..0ce3d862325cd 100755 --- a/install.sh +++ b/install.sh @@ -273,7 +273,7 @@ EOF main() { MAINLINE=1 STABLE=0 - TERRAFORM_VERSION="1.11.3" + TERRAFORM_VERSION="1.11.4" if [ "${TRACE-}" ]; then set -x diff --git a/provisioner/terraform/install.go b/provisioner/terraform/install.go index 05935d0c90437..0f65f07d17a9c 100644 --- a/provisioner/terraform/install.go +++ b/provisioner/terraform/install.go @@ -22,7 +22,7 @@ var ( // when Terraform is not available on the system. // NOTE: Keep this in sync with the version in scripts/Dockerfile.base. // NOTE: Keep this in sync with the version in install.sh. - TerraformVersion = version.Must(version.NewVersion("1.11.3")) + TerraformVersion = version.Must(version.NewVersion("1.11.4")) minTerraformVersion = version.Must(version.NewVersion("1.1.0")) maxTerraformVersion = version.Must(version.NewVersion("1.11.9")) // use .9 to automatically allow patch releases diff --git a/provisioner/terraform/testdata/resources/presets/presets.tfplan.json b/provisioner/terraform/testdata/resources/presets/presets.tfplan.json index 57bdf0fe19188..0d21d2dc71e6d 100644 --- a/provisioner/terraform/testdata/resources/presets/presets.tfplan.json +++ b/provisioner/terraform/testdata/resources/presets/presets.tfplan.json @@ -1,6 +1,6 @@ { "format_version": "1.2", - "terraform_version": "1.11.3", + "terraform_version": "1.11.4", "planned_values": { "root_module": { "resources": [ @@ -118,7 +118,7 @@ ], "prior_state": { "format_version": "1.0", - "terraform_version": "1.11.3", + "terraform_version": "1.11.4", "values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/resources/presets/presets.tfstate.json b/provisioner/terraform/testdata/resources/presets/presets.tfstate.json index 1ae43c857fc69..234df9c6d9087 100644 --- a/provisioner/terraform/testdata/resources/presets/presets.tfstate.json +++ b/provisioner/terraform/testdata/resources/presets/presets.tfstate.json @@ -1,6 +1,6 @@ { "format_version": "1.0", - "terraform_version": "1.11.3", + "terraform_version": "1.11.4", "values": { "root_module": { "resources": [ diff --git a/provisioner/terraform/testdata/resources/version.txt b/provisioner/terraform/testdata/resources/version.txt index 0a5af26df3fdb..3d0e62313ced1 100644 --- a/provisioner/terraform/testdata/resources/version.txt +++ b/provisioner/terraform/testdata/resources/version.txt @@ -1 +1 @@ -1.11.3 +1.11.4 diff --git a/provisioner/terraform/testdata/version.txt b/provisioner/terraform/testdata/version.txt index 0a5af26df3fdb..3d0e62313ced1 100644 --- a/provisioner/terraform/testdata/version.txt +++ b/provisioner/terraform/testdata/version.txt @@ -1 +1 @@ -1.11.3 +1.11.4 diff --git a/scripts/Dockerfile.base b/scripts/Dockerfile.base index 3ed1f48791124..fdadd87e55a3a 100644 --- a/scripts/Dockerfile.base +++ b/scripts/Dockerfile.base @@ -26,7 +26,7 @@ RUN apk add --no-cache \ # Terraform was disabled in the edge repo due to a build issue. # https://gitlab.alpinelinux.org/alpine/aports/-/commit/f3e263d94cfac02d594bef83790c280e045eba35 # Using wget for now. Note that busybox unzip doesn't support streaming. -RUN ARCH="$(arch)"; if [ "${ARCH}" == "x86_64" ]; then ARCH="amd64"; elif [ "${ARCH}" == "aarch64" ]; then ARCH="arm64"; fi; wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.11.3/terraform_1.11.3_linux_${ARCH}.zip" && \ +RUN ARCH="$(arch)"; if [ "${ARCH}" == "x86_64" ]; then ARCH="amd64"; elif [ "${ARCH}" == "aarch64" ]; then ARCH="arm64"; elif [ "${ARCH}" == "armv7l" ]; then ARCH="arm"; fi; wget -O /tmp/terraform.zip "https://releases.hashicorp.com/terraform/1.11.4/terraform_1.11.4_linux_${ARCH}.zip" && \ busybox unzip /tmp/terraform.zip -d /usr/local/bin && \ rm -f /tmp/terraform.zip && \ chmod +x /usr/local/bin/terraform && \ From c1816e3674bbd2867b5c409a9ddec23546be5d10 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 10 Apr 2025 12:46:19 +0400 Subject: [PATCH 046/384] fix(agent): fix deadlock if closed while starting listeners (#17329) fixes #17328 Fixes a deadlock if we close the Agent in the middle of starting listeners on the tailnet. --- agent/agent.go | 49 +++++++++++++++++++++++++++------------------ agent/agent_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index cf784a2702bfe..a7434b90d4854 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -229,13 +229,21 @@ type agent struct { // we track 2 contexts and associated cancel functions: "graceful" which is Done when it is time // to start gracefully shutting down and "hard" which is Done when it is time to close // everything down (regardless of whether graceful shutdown completed). - gracefulCtx context.Context - gracefulCancel context.CancelFunc - hardCtx context.Context - hardCancel context.CancelFunc - closeWaitGroup sync.WaitGroup + gracefulCtx context.Context + gracefulCancel context.CancelFunc + hardCtx context.Context + hardCancel context.CancelFunc + + // closeMutex protects the following: closeMutex sync.Mutex + closeWaitGroup sync.WaitGroup coordDisconnected chan struct{} + closing bool + // note that once the network is set to non-nil, it is never modified, as with the statsReporter. So, routines + // that run after createOrUpdateNetwork and check the networkOK checkpoint do not need to hold the lock to use them. + network *tailnet.Conn + statsReporter *statsReporter + // end fields protected by closeMutex environmentVariables map[string]string @@ -259,9 +267,7 @@ type agent struct { reportConnectionsMu sync.Mutex reportConnections []*proto.ReportConnectionRequest - network *tailnet.Conn - statsReporter *statsReporter - logSender *agentsdk.LogSender + logSender *agentsdk.LogSender prometheusRegistry *prometheus.Registry // metrics are prometheus registered metrics that will be collected and @@ -274,6 +280,8 @@ type agent struct { } func (a *agent) TailnetConn() *tailnet.Conn { + a.closeMutex.Lock() + defer a.closeMutex.Unlock() return a.network } @@ -1205,15 +1213,15 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co } a.closeMutex.Lock() // Re-check if agent was closed while initializing the network. - closed := a.isClosed() - if !closed { + closing := a.closing + if !closing { a.network = network a.statsReporter = newStatsReporter(a.logger, network, a) } a.closeMutex.Unlock() - if closed { + if closing { _ = network.Close() - return xerrors.New("agent is closed") + return xerrors.New("agent is closing") } } else { // Update the wireguard IPs if the agent ID changed. @@ -1328,8 +1336,8 @@ func (*agent) wireguardAddresses(agentID uuid.UUID) []netip.Prefix { func (a *agent) trackGoroutine(fn func()) error { a.closeMutex.Lock() defer a.closeMutex.Unlock() - if a.isClosed() { - return xerrors.New("track conn goroutine: agent is closed") + if a.closing { + return xerrors.New("track conn goroutine: agent is closing") } a.closeWaitGroup.Add(1) go func() { @@ -1547,7 +1555,7 @@ func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTai func (a *agent) setCoordDisconnected() chan struct{} { a.closeMutex.Lock() defer a.closeMutex.Unlock() - if a.isClosed() { + if a.closing { return nil } disconnected := make(chan struct{}) @@ -1772,7 +1780,10 @@ func (a *agent) HTTPDebug() http.Handler { func (a *agent) Close() error { a.closeMutex.Lock() - defer a.closeMutex.Unlock() + network := a.network + coordDisconnected := a.coordDisconnected + a.closing = true + a.closeMutex.Unlock() if a.isClosed() { return nil } @@ -1849,7 +1860,7 @@ lifecycleWaitLoop: select { case <-a.hardCtx.Done(): a.logger.Warn(context.Background(), "timed out waiting for Coordinator RPC disconnect") - case <-a.coordDisconnected: + case <-coordDisconnected: a.logger.Debug(context.Background(), "coordinator RPC disconnected") } @@ -1860,8 +1871,8 @@ lifecycleWaitLoop: } a.hardCancel() - if a.network != nil { - _ = a.network.Close() + if network != nil { + _ = network.Close() } a.closeWaitGroup.Wait() diff --git a/agent/agent_test.go b/agent/agent_test.go index bbf0221ab5259..69423a2f83be7 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -68,6 +68,54 @@ func TestMain(m *testing.M) { var sshPorts = []uint16{workspacesdk.AgentSSHPort, workspacesdk.AgentStandardSSHPort} +// TestAgent_CloseWhileStarting is a regression test for https://github.com/coder/coder/issues/17328 +func TestAgent_ImmediateClose(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + logger := slogtest.Make(t, &slogtest.Options{ + // Agent can drop errors when shutting down, and some, like the + // fasthttplistener connection closed error, are unexported. + IgnoreErrors: true, + }).Leveled(slog.LevelDebug) + manifest := agentsdk.Manifest{ + AgentID: uuid.New(), + AgentName: "test-agent", + WorkspaceName: "test-workspace", + WorkspaceID: uuid.New(), + } + + coordinator := tailnet.NewCoordinator(logger) + t.Cleanup(func() { + _ = coordinator.Close() + }) + statsCh := make(chan *proto.Stats, 50) + fs := afero.NewMemMapFs() + client := agenttest.NewClient(t, logger.Named("agenttest"), manifest.AgentID, manifest, statsCh, coordinator) + t.Cleanup(client.Close) + + options := agent.Options{ + Client: client, + Filesystem: fs, + Logger: logger.Named("agent"), + ReconnectingPTYTimeout: 0, + EnvironmentVariables: map[string]string{}, + } + + agentUnderTest := agent.New(options) + t.Cleanup(func() { + _ = agentUnderTest.Close() + }) + + // wait until the agent has connected and is starting to find races in the startup code + _ = testutil.RequireRecvCtx(ctx, t, client.GetStartup()) + t.Log("Closing Agent") + err := agentUnderTest.Close() + require.NoError(t, err) +} + // NOTE: These tests only work when your default shell is bash for some reason. func TestAgent_Stats_SSH(t *testing.T) { From 33b948789962ccaa28114888175900bcbc2a413a Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 10 Apr 2025 15:10:58 +0300 Subject: [PATCH 047/384] fix(agent/agentcontainers/dcspec): generate unmarshalers and add tests (#17330) This change allows proper unmarshaling of `devcontainer.json` and will no longer break EnvInfoer or the Web Terminal. Fixes coder/internal#556 --- agent/agentcontainers/dcspec/dcspec_gen.go | 246 ++++++++++++++++++ agent/agentcontainers/dcspec/dcspec_test.go | 148 +++++++++++ agent/agentcontainers/dcspec/gen.sh | 5 +- .../dcspec/testdata/arrays.json | 5 + .../devcontainers-template-starter.json | 12 + .../dcspec/testdata/minimal.json | 1 + 6 files changed, 414 insertions(+), 3 deletions(-) create mode 100644 agent/agentcontainers/dcspec/dcspec_test.go create mode 100644 agent/agentcontainers/dcspec/testdata/arrays.json create mode 100644 agent/agentcontainers/dcspec/testdata/devcontainers-template-starter.json create mode 100644 agent/agentcontainers/dcspec/testdata/minimal.json diff --git a/agent/agentcontainers/dcspec/dcspec_gen.go b/agent/agentcontainers/dcspec/dcspec_gen.go index 1f0291063dd99..87dc3ac9f9615 100644 --- a/agent/agentcontainers/dcspec/dcspec_gen.go +++ b/agent/agentcontainers/dcspec/dcspec_gen.go @@ -1,6 +1,30 @@ // Code generated by dcspec/gen.sh. DO NOT EDIT. +// +// This file was generated from JSON Schema using quicktype, do not modify it directly. +// To parse and unparse this JSON data, add this code to your project and do: +// +// devContainer, err := UnmarshalDevContainer(bytes) +// bytes, err = devContainer.Marshal() + package dcspec +import ( + "bytes" + "errors" +) + +import "encoding/json" + +func UnmarshalDevContainer(data []byte) (DevContainer, error) { + var r DevContainer + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *DevContainer) Marshal() ([]byte, error) { + return json.Marshal(r) +} + // Defines a dev container type DevContainer struct { // Docker build-related options. @@ -284,6 +308,21 @@ type DevContainerAppPort struct { UnionArray []AppPortElement } +func (x *DevContainerAppPort) UnmarshalJSON(data []byte) error { + x.UnionArray = nil + object, err := unmarshalUnion(data, &x.Integer, nil, nil, &x.String, true, &x.UnionArray, false, nil, false, nil, false, nil, false) + if err != nil { + return err + } + if object { + } + return nil +} + +func (x *DevContainerAppPort) MarshalJSON() ([]byte, error) { + return marshalUnion(x.Integer, nil, nil, x.String, x.UnionArray != nil, x.UnionArray, false, nil, false, nil, false, nil, false) +} + // Application ports that are exposed by the container. This can be a single port or an // array of ports. Each port can be a number or a string. A number is mapped to the same // port on the host. A string is passed to Docker unchanged and can be used to map ports @@ -293,6 +332,20 @@ type AppPortElement struct { String *string } +func (x *AppPortElement) UnmarshalJSON(data []byte) error { + object, err := unmarshalUnion(data, &x.Integer, nil, nil, &x.String, false, nil, false, nil, false, nil, false, nil, false) + if err != nil { + return err + } + if object { + } + return nil +} + +func (x *AppPortElement) MarshalJSON() ([]byte, error) { + return marshalUnion(x.Integer, nil, nil, x.String, false, nil, false, nil, false, nil, false, nil, false) +} + // The image to consider as a cache. Use an array to specify multiple images. // // The name of the docker-compose file(s) used to start the services. @@ -301,17 +354,64 @@ type CacheFrom struct { StringArray []string } +func (x *CacheFrom) UnmarshalJSON(data []byte) error { + x.StringArray = nil + object, err := unmarshalUnion(data, nil, nil, nil, &x.String, true, &x.StringArray, false, nil, false, nil, false, nil, false) + if err != nil { + return err + } + if object { + } + return nil +} + +func (x *CacheFrom) MarshalJSON() ([]byte, error) { + return marshalUnion(nil, nil, nil, x.String, x.StringArray != nil, x.StringArray, false, nil, false, nil, false, nil, false) +} + type ForwardPort struct { Integer *int64 String *string } +func (x *ForwardPort) UnmarshalJSON(data []byte) error { + object, err := unmarshalUnion(data, &x.Integer, nil, nil, &x.String, false, nil, false, nil, false, nil, false, nil, false) + if err != nil { + return err + } + if object { + } + return nil +} + +func (x *ForwardPort) MarshalJSON() ([]byte, error) { + return marshalUnion(x.Integer, nil, nil, x.String, false, nil, false, nil, false, nil, false, nil, false) +} + type GPUUnion struct { Bool *bool Enum *GPUEnum GPUClass *GPUClass } +func (x *GPUUnion) UnmarshalJSON(data []byte) error { + x.GPUClass = nil + x.Enum = nil + var c GPUClass + object, err := unmarshalUnion(data, nil, nil, &x.Bool, nil, false, nil, true, &c, false, nil, true, &x.Enum, false) + if err != nil { + return err + } + if object { + x.GPUClass = &c + } + return nil +} + +func (x *GPUUnion) MarshalJSON() ([]byte, error) { + return marshalUnion(nil, nil, x.Bool, nil, false, nil, x.GPUClass != nil, x.GPUClass, false, nil, x.Enum != nil, x.Enum, false) +} + // A command to run locally (i.e Your host machine, cloud VM) before anything else. This // command is run before "onCreateCommand". If this is a single string, it will be run in a // shell. If this is an array of strings, it will be run as a single command without shell. @@ -349,7 +449,153 @@ type Command struct { UnionMap map[string]*CacheFrom } +func (x *Command) UnmarshalJSON(data []byte) error { + x.StringArray = nil + x.UnionMap = nil + object, err := unmarshalUnion(data, nil, nil, nil, &x.String, true, &x.StringArray, false, nil, true, &x.UnionMap, false, nil, false) + if err != nil { + return err + } + if object { + } + return nil +} + +func (x *Command) MarshalJSON() ([]byte, error) { + return marshalUnion(nil, nil, nil, x.String, x.StringArray != nil, x.StringArray, false, nil, x.UnionMap != nil, x.UnionMap, false, nil, false) +} + type MountElement struct { Mount *Mount String *string } + +func (x *MountElement) UnmarshalJSON(data []byte) error { + x.Mount = nil + var c Mount + object, err := unmarshalUnion(data, nil, nil, nil, &x.String, false, nil, true, &c, false, nil, false, nil, false) + if err != nil { + return err + } + if object { + x.Mount = &c + } + return nil +} + +func (x *MountElement) MarshalJSON() ([]byte, error) { + return marshalUnion(nil, nil, nil, x.String, false, nil, x.Mount != nil, x.Mount, false, nil, false, nil, false) +} + +func unmarshalUnion(data []byte, pi **int64, pf **float64, pb **bool, ps **string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) (bool, error) { + if pi != nil { + *pi = nil + } + if pf != nil { + *pf = nil + } + if pb != nil { + *pb = nil + } + if ps != nil { + *ps = nil + } + + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + tok, err := dec.Token() + if err != nil { + return false, err + } + + switch v := tok.(type) { + case json.Number: + if pi != nil { + i, err := v.Int64() + if err == nil { + *pi = &i + return false, nil + } + } + if pf != nil { + f, err := v.Float64() + if err == nil { + *pf = &f + return false, nil + } + return false, errors.New("Unparsable number") + } + return false, errors.New("Union does not contain number") + case float64: + return false, errors.New("Decoder should not return float64") + case bool: + if pb != nil { + *pb = &v + return false, nil + } + return false, errors.New("Union does not contain bool") + case string: + if haveEnum { + return false, json.Unmarshal(data, pe) + } + if ps != nil { + *ps = &v + return false, nil + } + return false, errors.New("Union does not contain string") + case nil: + if nullable { + return false, nil + } + return false, errors.New("Union does not contain null") + case json.Delim: + if v == '{' { + if haveObject { + return true, json.Unmarshal(data, pc) + } + if haveMap { + return false, json.Unmarshal(data, pm) + } + return false, errors.New("Union does not contain object") + } + if v == '[' { + if haveArray { + return false, json.Unmarshal(data, pa) + } + return false, errors.New("Union does not contain array") + } + return false, errors.New("Cannot handle delimiter") + } + return false, errors.New("Cannot unmarshal union") +} + +func marshalUnion(pi *int64, pf *float64, pb *bool, ps *string, haveArray bool, pa interface{}, haveObject bool, pc interface{}, haveMap bool, pm interface{}, haveEnum bool, pe interface{}, nullable bool) ([]byte, error) { + if pi != nil { + return json.Marshal(*pi) + } + if pf != nil { + return json.Marshal(*pf) + } + if pb != nil { + return json.Marshal(*pb) + } + if ps != nil { + return json.Marshal(*ps) + } + if haveArray { + return json.Marshal(pa) + } + if haveObject { + return json.Marshal(pc) + } + if haveMap { + return json.Marshal(pm) + } + if haveEnum { + return json.Marshal(pe) + } + if nullable { + return json.Marshal(nil) + } + return nil, errors.New("Union must not be null") +} diff --git a/agent/agentcontainers/dcspec/dcspec_test.go b/agent/agentcontainers/dcspec/dcspec_test.go new file mode 100644 index 0000000000000..c3dae042031ee --- /dev/null +++ b/agent/agentcontainers/dcspec/dcspec_test.go @@ -0,0 +1,148 @@ +package dcspec_test + +import ( + "encoding/json" + "os" + "path/filepath" + "slices" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/agent/agentcontainers/dcspec" + "github.com/coder/coder/v2/coderd/util/ptr" +) + +func TestUnmarshalDevContainer(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + file string + wantErr bool + want dcspec.DevContainer + } + tests := []testCase{ + { + name: "minimal", + file: filepath.Join("testdata", "minimal.json"), + want: dcspec.DevContainer{ + Image: ptr.Ref("test-image"), + }, + }, + { + name: "arrays", + file: filepath.Join("testdata", "arrays.json"), + want: dcspec.DevContainer{ + Image: ptr.Ref("test-image"), + RunArgs: []string{"--network=host", "--privileged"}, + ForwardPorts: []dcspec.ForwardPort{ + { + Integer: ptr.Ref[int64](8080), + }, + { + String: ptr.Ref("3000:3000"), + }, + }, + }, + }, + { + name: "devcontainers/template-starter", + file: filepath.Join("testdata", "devcontainers-template-starter.json"), + wantErr: false, + want: dcspec.DevContainer{ + Image: ptr.Ref("mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye"), + Features: &dcspec.Features{}, + Customizations: map[string]interface{}{ + "vscode": map[string]interface{}{ + "extensions": []interface{}{ + "mads-hartmann.bash-ide-vscode", + "dbaeumer.vscode-eslint", + }, + }, + }, + PostCreateCommand: &dcspec.Command{ + String: ptr.Ref("npm install -g @devcontainers/cli"), + }, + }, + }, + } + + var missingTests []string + files, err := filepath.Glob("testdata/*.json") + require.NoError(t, err, "glob test files failed") + for _, file := range files { + if !slices.ContainsFunc(tests, func(tt testCase) bool { + return tt.file == file + }) { + missingTests = append(missingTests, file) + } + } + require.Empty(t, missingTests, "missing tests case for files: %v", missingTests) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + data, err := os.ReadFile(tt.file) + require.NoError(t, err, "read test file failed") + + got, err := dcspec.UnmarshalDevContainer(data) + if tt.wantErr { + require.Error(t, err, "want error but got nil") + return + } + require.NoError(t, err, "unmarshal DevContainer failed") + + // Compare the unmarshaled data with the expected data. + if diff := cmp.Diff(tt.want, got); diff != "" { + require.Empty(t, diff, "UnmarshalDevContainer() mismatch (-want +got):\n%s", diff) + } + + // Test that marshaling works (without comparing to original). + marshaled, err := got.Marshal() + require.NoError(t, err, "marshal DevContainer back to JSON failed") + require.NotEmpty(t, marshaled, "marshaled JSON should not be empty") + + // Verify the marshaled JSON can be unmarshaled back. + var unmarshaled interface{} + err = json.Unmarshal(marshaled, &unmarshaled) + require.NoError(t, err, "unmarshal marshaled JSON failed") + }) + } +} + +func TestUnmarshalDevContainer_EdgeCases(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + wantErr bool + }{ + { + name: "empty JSON", + json: "{}", + wantErr: false, + }, + { + name: "invalid JSON", + json: "{not valid json", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, err := dcspec.UnmarshalDevContainer([]byte(tt.json)) + if tt.wantErr { + require.Error(t, err, "want error but got nil") + return + } + require.NoError(t, err, "unmarshal DevContainer failed") + }) + } +} diff --git a/agent/agentcontainers/dcspec/gen.sh b/agent/agentcontainers/dcspec/gen.sh index c74efe2efb0d5..276cb24cb4123 100755 --- a/agent/agentcontainers/dcspec/gen.sh +++ b/agent/agentcontainers/dcspec/gen.sh @@ -43,7 +43,6 @@ fi if ! pnpm exec quicktype \ --src-lang schema \ --lang go \ - --just-types-and-package \ --top-level "DevContainer" \ --out "${TMPDIR}/${DEST_FILENAME}" \ --package "dcspec" \ @@ -67,9 +66,9 @@ go run mvdan.cc/gofumpt@v0.4.0 -w -l "${TMPDIR}/${DEST_FILENAME}" # Add a header so that Go recognizes this as a generated file. if grep -q -- "\[-i extension\]" < <(sed -h 2>&1); then # darwin sed - sed -i '' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n/' "${TMPDIR}/${DEST_FILENAME}" + sed -i '' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n\/\/\n/' "${TMPDIR}/${DEST_FILENAME}" else - sed -i'' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n/' "${TMPDIR}/${DEST_FILENAME}" + sed -i'' '1s/^/\/\/ Code generated by dcspec\/gen.sh. DO NOT EDIT.\n\/\/\n/' "${TMPDIR}/${DEST_FILENAME}" fi mv -v "${TMPDIR}/${DEST_FILENAME}" "${DEST_PATH}" diff --git a/agent/agentcontainers/dcspec/testdata/arrays.json b/agent/agentcontainers/dcspec/testdata/arrays.json new file mode 100644 index 0000000000000..70dbda4893a91 --- /dev/null +++ b/agent/agentcontainers/dcspec/testdata/arrays.json @@ -0,0 +1,5 @@ +{ + "image": "test-image", + "runArgs": ["--network=host", "--privileged"], + "forwardPorts": [8080, "3000:3000"] +} diff --git a/agent/agentcontainers/dcspec/testdata/devcontainers-template-starter.json b/agent/agentcontainers/dcspec/testdata/devcontainers-template-starter.json new file mode 100644 index 0000000000000..5400151b1d678 --- /dev/null +++ b/agent/agentcontainers/dcspec/testdata/devcontainers-template-starter.json @@ -0,0 +1,12 @@ +{ + "image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "customizations": { + "vscode": { + "extensions": ["mads-hartmann.bash-ide-vscode", "dbaeumer.vscode-eslint"] + } + }, + "postCreateCommand": "npm install -g @devcontainers/cli" +} diff --git a/agent/agentcontainers/dcspec/testdata/minimal.json b/agent/agentcontainers/dcspec/testdata/minimal.json new file mode 100644 index 0000000000000..1e409346c61be --- /dev/null +++ b/agent/agentcontainers/dcspec/testdata/minimal.json @@ -0,0 +1 @@ +{ "image": "test-image" } From 6dd10560250a92ac82ed92e8623afafc408a909e Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Thu, 10 Apr 2025 13:32:19 +0100 Subject: [PATCH 048/384] feat(coderd/notifications): group workspace build failure report (#17306) Closes https://github.com/coder/coder/issues/15745 Instead of sending X many reports to a single template admin, we instead send only 1. --- coderd/database/dbmem/dbmem.go | 1 + ...group_build_failure_notifications.down.sql | 21 ++ ...6_group_build_failure_notifications.up.sql | 29 +++ coderd/database/queries.sql.go | 11 +- coderd/database/queries/workspacebuilds.sql | 1 + coderd/notifications/notifications_test.go | 111 +++++++--- coderd/notifications/reports/generator.go | 160 ++++++++------ .../reports/generator_internal_test.go | 202 +++++++++++------- ...ateWorkspaceBuildsFailedReport.html.golden | 131 +++++++++--- ...ateWorkspaceBuildsFailedReport.json.golden | 129 +++++++---- 10 files changed, 551 insertions(+), 245 deletions(-) create mode 100644 coderd/database/migrations/000316_group_build_failure_notifications.down.sql create mode 100644 coderd/database/migrations/000316_group_build_failure_notifications.up.sql diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index deafdc42e0216..cf8cf00ca9eed 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -3291,6 +3291,7 @@ func (q *FakeQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, } workspaceBuildStats = append(workspaceBuildStats, database.GetFailedWorkspaceBuildsByTemplateIDRow{ + WorkspaceID: w.ID, WorkspaceName: w.Name, WorkspaceOwnerUsername: workspaceOwner.Username, TemplateVersionName: templateVersion.Name, diff --git a/coderd/database/migrations/000316_group_build_failure_notifications.down.sql b/coderd/database/migrations/000316_group_build_failure_notifications.down.sql new file mode 100644 index 0000000000000..3ea2e98ff19e1 --- /dev/null +++ b/coderd/database/migrations/000316_group_build_failure_notifications.down.sql @@ -0,0 +1,21 @@ +UPDATE notification_templates +SET + name = 'Report: Workspace Builds Failed For Template', + title_template = E'Workspace builds failed for template "{{.Labels.template_display_name}}"', + body_template = E'Template **{{.Labels.template_display_name}}** has failed to build {{.Data.failed_builds}}/{{.Data.total_builds}} times over the last {{.Data.report_frequency}}. + +**Report:** +{{range $version := .Data.template_versions}} +**{{$version.template_version_name}}** failed {{$version.failed_count}} time{{if gt $version.failed_count 1.0}}s{{end}}: +{{range $build := $version.failed_builds}} +* [{{$build.workspace_owner_username}} / {{$build.workspace_name}} / #{{$build.build_number}}]({{base_url}}/@{{$build.workspace_owner_username}}/{{$build.workspace_name}}/builds/{{$build.build_number}}) +{{- end}} +{{end}} +We recommend reviewing these issues to ensure future builds are successful.', + actions = '[ + { + "label": "View workspaces", + "url": "{{ base_url }}/workspaces?filter=template%3A{{.Labels.template_name}}" + } + ]'::jsonb +WHERE id = '34a20db2-e9cc-4a93-b0e4-8569699d7a00'; diff --git a/coderd/database/migrations/000316_group_build_failure_notifications.up.sql b/coderd/database/migrations/000316_group_build_failure_notifications.up.sql new file mode 100644 index 0000000000000..e3c4e79fc6d35 --- /dev/null +++ b/coderd/database/migrations/000316_group_build_failure_notifications.up.sql @@ -0,0 +1,29 @@ +UPDATE notification_templates +SET + name = 'Report: Workspace Builds Failed', + title_template = 'Failed workspace builds report', + body_template = +E'The following templates have had build failures over the last {{.Data.report_frequency}}: +{{range $template := .Data.templates}} +- **{{$template.display_name}}** failed to build {{$template.failed_builds}}/{{$template.total_builds}} times +{{end}} + +**Report:** +{{range $template := .Data.templates}} +**{{$template.display_name}}** +{{range $version := $template.versions}} +- **{{$version.template_version_name}}** failed {{$version.failed_count}} time{{if gt $version.failed_count 1.0}}s{{end}}: +{{range $build := $version.failed_builds}} + - [{{$build.workspace_owner_username}} / {{$build.workspace_name}} / #{{$build.build_number}}]({{base_url}}/@{{$build.workspace_owner_username}}/{{$build.workspace_name}}/builds/{{$build.build_number}}) +{{end}} +{{end}} +{{end}} + +We recommend reviewing these issues to ensure future builds are successful.', + actions = '[ + { + "label": "View workspaces", + "url": "{{ base_url }}/workspaces?filter={{$first := true}}{{range $template := .Data.templates}}{{range $version := $template.versions}}{{range $build := $version.failed_builds}}{{if not $first}}+{{else}}{{$first = false}}{{end}}id%3A{{$build.workspace_id}}{{end}}{{end}}{{end}}" + } + ]'::jsonb +WHERE id = '34a20db2-e9cc-4a93-b0e4-8569699d7a00'; diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b93ad49f8f9d4..25bfe1db63bb3 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -16334,6 +16334,7 @@ SELECT tv.name AS template_version_name, u.username AS workspace_owner_username, w.name AS workspace_name, + w.id AS workspace_id, wb.build_number AS workspace_build_number FROM workspace_build_with_user AS wb @@ -16372,10 +16373,11 @@ type GetFailedWorkspaceBuildsByTemplateIDParams struct { } type GetFailedWorkspaceBuildsByTemplateIDRow struct { - TemplateVersionName string `db:"template_version_name" json:"template_version_name"` - WorkspaceOwnerUsername string `db:"workspace_owner_username" json:"workspace_owner_username"` - WorkspaceName string `db:"workspace_name" json:"workspace_name"` - WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"` + TemplateVersionName string `db:"template_version_name" json:"template_version_name"` + WorkspaceOwnerUsername string `db:"workspace_owner_username" json:"workspace_owner_username"` + WorkspaceName string `db:"workspace_name" json:"workspace_name"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + WorkspaceBuildNumber int32 `db:"workspace_build_number" json:"workspace_build_number"` } func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, arg GetFailedWorkspaceBuildsByTemplateIDParams) ([]GetFailedWorkspaceBuildsByTemplateIDRow, error) { @@ -16391,6 +16393,7 @@ func (q *sqlQuerier) GetFailedWorkspaceBuildsByTemplateID(ctx context.Context, a &i.TemplateVersionName, &i.WorkspaceOwnerUsername, &i.WorkspaceName, + &i.WorkspaceID, &i.WorkspaceBuildNumber, ); err != nil { return nil, err diff --git a/coderd/database/queries/workspacebuilds.sql b/coderd/database/queries/workspacebuilds.sql index da349fa1441b3..34ef639a1694b 100644 --- a/coderd/database/queries/workspacebuilds.sql +++ b/coderd/database/queries/workspacebuilds.sql @@ -213,6 +213,7 @@ SELECT tv.name AS template_version_name, u.username AS workspace_owner_username, w.name AS workspace_name, + w.id AS workspace_id, wb.build_number AS workspace_build_number FROM workspace_build_with_user AS wb diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 60858f1b641b1..5f6c221e7beb5 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -978,45 +978,102 @@ func TestNotificationTemplates_Golden(t *testing.T) { UserName: "Bobby", UserEmail: "bobby@coder.com", UserUsername: "bobby", - Labels: map[string]string{ - "template_name": "bobby-first-template", - "template_display_name": "Bobby First Template", - }, + Labels: map[string]string{}, // We need to use floats as `json.Unmarshal` unmarshal numbers in `map[string]any` to floats. Data: map[string]any{ - "failed_builds": 4.0, - "total_builds": 55.0, "report_frequency": "week", - "template_versions": []map[string]any{ + "templates": []map[string]any{ { - "template_version_name": "bobby-template-version-1", - "failed_count": 3.0, - "failed_builds": []map[string]any{ + "name": "bobby-first-template", + "display_name": "Bobby First Template", + "failed_builds": 4.0, + "total_builds": 55.0, + "versions": []map[string]any{ { - "workspace_owner_username": "mtojek", - "workspace_name": "workspace-1", - "build_number": 1234.0, + "template_version_name": "bobby-template-version-1", + "failed_count": 3.0, + "failed_builds": []map[string]any{ + { + "workspace_owner_username": "mtojek", + "workspace_name": "workspace-1", + "workspace_id": "24f5bd8f-1566-4374-9734-c3efa0454dc7", + "build_number": 1234.0, + }, + { + "workspace_owner_username": "johndoe", + "workspace_name": "my-workspace-3", + "workspace_id": "372a194b-dcde-43f1-b7cf-8a2f3d3114a0", + "build_number": 5678.0, + }, + { + "workspace_owner_username": "jack", + "workspace_name": "workwork", + "workspace_id": "1386d294-19c1-4351-89e2-6cae1afb9bfe", + "build_number": 774.0, + }, + }, }, { - "workspace_owner_username": "johndoe", - "workspace_name": "my-workspace-3", - "build_number": 5678.0, - }, - { - "workspace_owner_username": "jack", - "workspace_name": "workwork", - "build_number": 774.0, + "template_version_name": "bobby-template-version-2", + "failed_count": 1.0, + "failed_builds": []map[string]any{ + { + "workspace_owner_username": "ben", + "workspace_name": "cool-workspace", + "workspace_id": "86fd99b1-1b6e-4b7e-b58e-0aee6e35c159", + "build_number": 8888.0, + }, + }, }, }, }, { - "template_version_name": "bobby-template-version-2", - "failed_count": 1.0, - "failed_builds": []map[string]any{ + "name": "bobby-second-template", + "display_name": "Bobby Second Template", + "failed_builds": 5.0, + "total_builds": 50.0, + "versions": []map[string]any{ + { + "template_version_name": "bobby-template-version-1", + "failed_count": 3.0, + "failed_builds": []map[string]any{ + { + "workspace_owner_username": "daniellemaywood", + "workspace_name": "workspace-9", + "workspace_id": "cd469690-b6eb-4123-b759-980be7a7b278", + "build_number": 9234.0, + }, + { + "workspace_owner_username": "johndoe", + "workspace_name": "my-workspace-7", + "workspace_id": "c447d472-0800-4529-a836-788754d5e27d", + "build_number": 8678.0, + }, + { + "workspace_owner_username": "jack", + "workspace_name": "workworkwork", + "workspace_id": "919db6df-48f0-4dc1-b357-9036a2c40f86", + "build_number": 374.0, + }, + }, + }, { - "workspace_owner_username": "ben", - "workspace_name": "cool-workspace", - "build_number": 8888.0, + "template_version_name": "bobby-template-version-2", + "failed_count": 2.0, + "failed_builds": []map[string]any{ + { + "workspace_owner_username": "ben", + "workspace_name": "more-cool-workspace", + "workspace_id": "c8fb0652-9290-4bf2-a711-71b910243ac2", + "build_number": 8878.0, + }, + { + "workspace_owner_username": "ben", + "workspace_name": "less-cool-workspace", + "workspace_id": "703d718d-2234-4990-9a02-5b1df6cf462a", + "build_number": 8848.0, + }, + }, }, }, }, diff --git a/coderd/notifications/reports/generator.go b/coderd/notifications/reports/generator.go index 2424498146c60..6b7dbd0c5b7b9 100644 --- a/coderd/notifications/reports/generator.go +++ b/coderd/notifications/reports/generator.go @@ -18,6 +18,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" ) @@ -102,6 +103,11 @@ const ( failedWorkspaceBuildsReportFrequencyLabel = "week" ) +type adminReport struct { + stats database.GetWorkspaceBuildStatsByTemplatesRow + failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow +} + func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db database.Store, enqueuer notifications.Enqueuer, clk quartz.Clock) error { now := clk.Now() since := now.Add(-failedWorkspaceBuildsReportFrequency) @@ -136,6 +142,8 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat return xerrors.Errorf("unable to fetch failed workspace builds: %w", err) } + reports := make(map[uuid.UUID][]adminReport) + for _, stats := range templateStatsRows { select { case <-ctx.Done(): @@ -165,33 +173,40 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat logger.Error(ctx, "unable to fetch failed workspace builds", slog.F("template_id", stats.TemplateID), slog.Error(err)) continue } - reportData := buildDataForReportFailedWorkspaceBuilds(stats, failedBuilds) - // Send reports to template admins - templateDisplayName := stats.TemplateDisplayName - if templateDisplayName == "" { - templateDisplayName = stats.TemplateName + for _, templateAdmin := range templateAdmins { + adminReports := reports[templateAdmin.ID] + adminReports = append(adminReports, adminReport{ + failedBuilds: failedBuilds, + stats: stats, + }) + + reports[templateAdmin.ID] = adminReports } + } - for _, templateAdmin := range templateAdmins { - select { - case <-ctx.Done(): - logger.Debug(ctx, "context is canceled, quitting", slog.Error(ctx.Err())) - break - default: - } + for templateAdmin, reports := range reports { + select { + case <-ctx.Done(): + logger.Debug(ctx, "context is canceled, quitting", slog.Error(ctx.Err())) + break + default: + } - if _, err := enqueuer.EnqueueWithData(ctx, templateAdmin.ID, notifications.TemplateWorkspaceBuildsFailedReport, - map[string]string{ - "template_name": stats.TemplateName, - "template_display_name": templateDisplayName, - }, - reportData, - "report_generator", - stats.TemplateID, stats.TemplateOrganizationID, - ); err != nil { - logger.Warn(ctx, "failed to send a report with failed workspace builds", slog.Error(err)) - } + reportData := buildDataForReportFailedWorkspaceBuilds(reports) + + targets := []uuid.UUID{} + for _, report := range reports { + targets = append(targets, report.stats.TemplateID, report.stats.TemplateOrganizationID) + } + + if _, err := enqueuer.EnqueueWithData(ctx, templateAdmin, notifications.TemplateWorkspaceBuildsFailedReport, + map[string]string{}, + reportData, + "report_generator", + slice.Unique(targets)..., + ); err != nil { + logger.Warn(ctx, "failed to send a report with failed workspace builds", slog.Error(err)) } } @@ -213,54 +228,71 @@ func reportFailedWorkspaceBuilds(ctx context.Context, logger slog.Logger, db dat const workspaceBuildsLimitPerTemplateVersion = 10 -func buildDataForReportFailedWorkspaceBuilds(stats database.GetWorkspaceBuildStatsByTemplatesRow, failedBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow) map[string]any { - // Build notification model for template versions and failed workspace builds. - // - // Failed builds are sorted by template version ascending, workspace build number descending. - // Review builds, group them by template versions, and assign to builds to template versions. - // The map requires `[]map[string]any{}` to be compatible with data passed to `NotificationEnqueuer`. - templateVersions := []map[string]any{} - for _, failedBuild := range failedBuilds { - c := len(templateVersions) - - if c == 0 || templateVersions[c-1]["template_version_name"] != failedBuild.TemplateVersionName { - templateVersions = append(templateVersions, map[string]any{ - "template_version_name": failedBuild.TemplateVersionName, - "failed_count": 1, - "failed_builds": []map[string]any{ - { - "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, - "workspace_name": failedBuild.WorkspaceName, - "build_number": failedBuild.WorkspaceBuildNumber, +func buildDataForReportFailedWorkspaceBuilds(reports []adminReport) map[string]any { + templates := []map[string]any{} + + for _, report := range reports { + // Build notification model for template versions and failed workspace builds. + // + // Failed builds are sorted by template version ascending, workspace build number descending. + // Review builds, group them by template versions, and assign to builds to template versions. + // The map requires `[]map[string]any{}` to be compatible with data passed to `NotificationEnqueuer`. + templateVersions := []map[string]any{} + for _, failedBuild := range report.failedBuilds { + c := len(templateVersions) + + if c == 0 || templateVersions[c-1]["template_version_name"] != failedBuild.TemplateVersionName { + templateVersions = append(templateVersions, map[string]any{ + "template_version_name": failedBuild.TemplateVersionName, + "failed_count": 1, + "failed_builds": []map[string]any{ + { + "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, + "workspace_name": failedBuild.WorkspaceName, + "workspace_id": failedBuild.WorkspaceID, + "build_number": failedBuild.WorkspaceBuildNumber, + }, }, - }, - }) - continue + }) + continue + } + + tv := templateVersions[c-1] + //nolint:errorlint,forcetypeassert // only this function prepares the notification model + tv["failed_count"] = tv["failed_count"].(int) + 1 + + //nolint:errorlint,forcetypeassert // only this function prepares the notification model + builds := tv["failed_builds"].([]map[string]any) + if len(builds) < workspaceBuildsLimitPerTemplateVersion { + // return N last builds to prevent long email reports + builds = append(builds, map[string]any{ + "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, + "workspace_name": failedBuild.WorkspaceName, + "workspace_id": failedBuild.WorkspaceID, + "build_number": failedBuild.WorkspaceBuildNumber, + }) + tv["failed_builds"] = builds + } + templateVersions[c-1] = tv } - tv := templateVersions[c-1] - //nolint:errorlint,forcetypeassert // only this function prepares the notification model - tv["failed_count"] = tv["failed_count"].(int) + 1 - - //nolint:errorlint,forcetypeassert // only this function prepares the notification model - builds := tv["failed_builds"].([]map[string]any) - if len(builds) < workspaceBuildsLimitPerTemplateVersion { - // return N last builds to prevent long email reports - builds = append(builds, map[string]any{ - "workspace_owner_username": failedBuild.WorkspaceOwnerUsername, - "workspace_name": failedBuild.WorkspaceName, - "build_number": failedBuild.WorkspaceBuildNumber, - }) - tv["failed_builds"] = builds + templateDisplayName := report.stats.TemplateDisplayName + if templateDisplayName == "" { + templateDisplayName = report.stats.TemplateName } - templateVersions[c-1] = tv + + templates = append(templates, map[string]any{ + "failed_builds": report.stats.FailedBuilds, + "total_builds": report.stats.TotalBuilds, + "versions": templateVersions, + "name": report.stats.TemplateName, + "display_name": templateDisplayName, + }) } return map[string]any{ - "failed_builds": stats.FailedBuilds, - "total_builds": stats.TotalBuilds, - "report_frequency": failedWorkspaceBuildsReportFrequencyLabel, - "template_versions": templateVersions, + "report_frequency": failedWorkspaceBuildsReportFrequencyLabel, + "templates": templates, } } diff --git a/coderd/notifications/reports/generator_internal_test.go b/coderd/notifications/reports/generator_internal_test.go index b2cc5e82aadaf..f61064c4e0b23 100644 --- a/coderd/notifications/reports/generator_internal_test.go +++ b/coderd/notifications/reports/generator_internal_test.go @@ -3,6 +3,7 @@ package reports import ( "context" "database/sql" + "sort" "testing" "time" @@ -118,17 +119,13 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Run("FailedBuilds_SecondRun_Report_ThirdRunTooEarly_NoReport_FourthRun_Report", func(t *testing.T) { t.Parallel() - verifyNotification := func(t *testing.T, recipient database.User, notif *notificationstest.FakeNotification, tmpl database.Template, failedBuilds, totalBuilds int64, templateVersions []map[string]interface{}) { + verifyNotification := func(t *testing.T, recipientID uuid.UUID, notif *notificationstest.FakeNotification, templates []map[string]any) { t.Helper() - require.Equal(t, recipient.ID, notif.UserID) + require.Equal(t, recipientID, notif.UserID) require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notif.TemplateID) - require.Equal(t, tmpl.Name, notif.Labels["template_name"]) - require.Equal(t, tmpl.DisplayName, notif.Labels["template_display_name"]) - require.Equal(t, failedBuilds, notif.Data["failed_builds"]) - require.Equal(t, totalBuilds, notif.Data["total_builds"]) require.Equal(t, "week", notif.Data["report_frequency"]) - require.Equal(t, templateVersions, notif.Data["template_versions"]) + require.Equal(t, templates, notif.Data["templates"]) } // Setup @@ -212,43 +209,65 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { require.NoError(t, err) sent := notifEnq.Sent() - require.Len(t, sent, 4) // 2 templates, 2 template admins - for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { - verifyNotification(t, templateAdmin, sent[i], t1, 3, 4, []map[string]interface{}{ - { - "failed_builds": []map[string]interface{}{ - {"build_number": int32(7), "workspace_name": w3.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(1), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - }, - "failed_count": 2, - "template_version_name": t1v1.Name, - }, - { - "failed_builds": []map[string]interface{}{ - {"build_number": int32(3), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - }, - "failed_count": 1, - "template_version_name": t1v2.Name, - }, - }) - } + require.Len(t, sent, 2) // 2 templates, 2 template admins - for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { - verifyNotification(t, templateAdmin, sent[i+2], t2, 3, 5, []map[string]interface{}{ + templateAdmins := []uuid.UUID{templateAdmin1.ID, templateAdmin2.ID} + + // Ensure consistent order for tests + sort.Slice(templateAdmins, func(i, j int) bool { + return templateAdmins[i].String() < templateAdmins[j].String() + }) + sort.Slice(sent, func(i, j int) bool { + return sent[i].UserID.String() < sent[j].UserID.String() + }) + + for i, templateAdmin := range templateAdmins { + verifyNotification(t, templateAdmin, sent[i], []map[string]any{ { - "failed_builds": []map[string]interface{}{ - {"build_number": int32(8), "workspace_name": w4.Name, "workspace_owner_username": user2.Username}, + "name": t1.Name, + "display_name": t1.DisplayName, + "failed_builds": int64(3), + "total_builds": int64(4), + "versions": []map[string]any{ + { + "failed_builds": []map[string]any{ + {"build_number": int32(7), "workspace_name": w3.Name, "workspace_id": w3.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(1), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + }, + "failed_count": 2, + "template_version_name": t1v1.Name, + }, + { + "failed_builds": []map[string]any{ + {"build_number": int32(3), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + }, + "failed_count": 1, + "template_version_name": t1v2.Name, + }, }, - "failed_count": 1, - "template_version_name": t2v1.Name, }, { - "failed_builds": []map[string]interface{}{ - {"build_number": int32(6), "workspace_name": w2.Name, "workspace_owner_username": user2.Username}, - {"build_number": int32(5), "workspace_name": w2.Name, "workspace_owner_username": user2.Username}, + "name": t2.Name, + "display_name": t2.DisplayName, + "failed_builds": int64(3), + "total_builds": int64(5), + "versions": []map[string]any{ + { + "failed_builds": []map[string]any{ + {"build_number": int32(8), "workspace_name": w4.Name, "workspace_id": w4.ID, "workspace_owner_username": user2.Username}, + }, + "failed_count": 1, + "template_version_name": t2v1.Name, + }, + { + "failed_builds": []map[string]any{ + {"build_number": int32(6), "workspace_name": w2.Name, "workspace_id": w2.ID, "workspace_owner_username": user2.Username}, + {"build_number": int32(5), "workspace_name": w2.Name, "workspace_id": w2.ID, "workspace_owner_username": user2.Username}, + }, + "failed_count": 2, + "template_version_name": t2v2.Name, + }, }, - "failed_count": 2, - "template_version_name": t2v2.Name, }, }) } @@ -279,14 +298,33 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { // Then: we should see the failed job in the report sent = notifEnq.Sent() require.Len(t, sent, 2) // a new failed job should be reported - for i, templateAdmin := range []database.User{templateAdmin1, templateAdmin2} { - verifyNotification(t, templateAdmin, sent[i], t1, 1, 1, []map[string]interface{}{ + + templateAdmins = []uuid.UUID{templateAdmin1.ID, templateAdmin2.ID} + + // Ensure consistent order for tests + sort.Slice(templateAdmins, func(i, j int) bool { + return templateAdmins[i].String() < templateAdmins[j].String() + }) + sort.Slice(sent, func(i, j int) bool { + return sent[i].UserID.String() < sent[j].UserID.String() + }) + + for i, templateAdmin := range templateAdmins { + verifyNotification(t, templateAdmin, sent[i], []map[string]any{ { - "failed_builds": []map[string]interface{}{ - {"build_number": int32(77), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + "name": t1.Name, + "display_name": t1.DisplayName, + "failed_builds": int64(1), + "total_builds": int64(1), + "versions": []map[string]any{ + { + "failed_builds": []map[string]any{ + {"build_number": int32(77), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + }, + "failed_count": 1, + "template_version_name": t1v2.Name, + }, }, - "failed_count": 1, - "template_version_name": t1v2.Name, }, }) } @@ -295,17 +333,13 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { t.Run("TooManyFailedBuilds_SecondRun_Report", func(t *testing.T) { t.Parallel() - verifyNotification := func(t *testing.T, recipient database.User, notif *notificationstest.FakeNotification, tmpl database.Template, failedBuilds, totalBuilds int64, templateVersions []map[string]interface{}) { + verifyNotification := func(t *testing.T, recipient database.User, notif *notificationstest.FakeNotification, templates []map[string]any) { t.Helper() require.Equal(t, recipient.ID, notif.UserID) require.Equal(t, notifications.TemplateWorkspaceBuildsFailedReport, notif.TemplateID) - require.Equal(t, tmpl.Name, notif.Labels["template_name"]) - require.Equal(t, tmpl.DisplayName, notif.Labels["template_display_name"]) - require.Equal(t, failedBuilds, notif.Data["failed_builds"]) - require.Equal(t, totalBuilds, notif.Data["total_builds"]) require.Equal(t, "week", notif.Data["report_frequency"]) - require.Equal(t, templateVersions, notif.Data["template_versions"]) + require.Equal(t, templates, notif.Data["templates"]) } // Setup @@ -369,38 +403,46 @@ func TestReportFailedWorkspaceBuilds(t *testing.T) { sent := notifEnq.Sent() require.Len(t, sent, 1) // 1 template, 1 template admin - verifyNotification(t, templateAdmin1, sent[0], t1, 46, 47, []map[string]interface{}{ + verifyNotification(t, templateAdmin1, sent[0], []map[string]any{ { - "failed_builds": []map[string]interface{}{ - {"build_number": int32(23), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(22), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(21), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(20), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(19), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(18), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(17), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(16), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(15), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(14), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - }, - "failed_count": 23, - "template_version_name": t1v1.Name, - }, - { - "failed_builds": []map[string]interface{}{ - {"build_number": int32(123), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(122), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(121), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(120), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(119), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(118), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(117), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(116), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(115), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, - {"build_number": int32(114), "workspace_name": w1.Name, "workspace_owner_username": user1.Username}, + "name": t1.Name, + "display_name": t1.DisplayName, + "failed_builds": int64(46), + "total_builds": int64(47), + "versions": []map[string]any{ + { + "failed_builds": []map[string]any{ + {"build_number": int32(23), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(22), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(21), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(20), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(19), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(18), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(17), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(16), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(15), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(14), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + }, + "failed_count": 23, + "template_version_name": t1v1.Name, + }, + { + "failed_builds": []map[string]any{ + {"build_number": int32(123), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(122), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(121), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(120), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(119), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(118), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(117), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(116), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(115), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + {"build_number": int32(114), "workspace_name": w1.Name, "workspace_id": w1.ID, "workspace_owner_username": user1.Username}, + }, + "failed_count": 23, + "template_version_name": t1v2.Name, + }, }, - "failed_count": 23, - "template_version_name": t1v2.Name, }, }) }) diff --git a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceBuildsFailedReport.html.golden b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceBuildsFailedReport.html.golden index f3edc6ac05d02..9699486bf9cc8 100644 --- a/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceBuildsFailedReport.html.golden +++ b/coderd/notifications/testdata/rendered-templates/smtp/TemplateWorkspaceBuildsFailedReport.html.golden @@ -1,6 +1,6 @@ From: system@coder.com To: bobby@coder.com -Subject: Workspace builds failed for template "Bobby First Template" +Subject: Failed workspace builds report 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 @@ -12,29 +12,51 @@ Content-Type: text/plain; charset=UTF-8 Hi Bobby, -Template Bobby First Template has failed to build 4/55 times over the last = -week. +The following templates have had build failures over the last week: + +Bobby First Template failed to build 4/55 times +Bobby Second Template failed to build 5/50 times Report: +Bobby First Template + bobby-template-version-1 failed 3 times: + mtojek / workspace-1 / #1234 (http://test.com/@mtojek/workspace-1/build= +s/1234) + johndoe / my-workspace-3 / #5678 (http://test.com/@johndoe/my-workspace= +-3/builds/5678) + jack / workwork / #774 (http://test.com/@jack/workwork/builds/774) +bobby-template-version-2 failed 1 time: + ben / cool-workspace / #8888 (http://test.com/@ben/cool-workspace/build= +s/8888) -mtojek / workspace-1 / #1234 (http://test.com/@mtojek/workspace-1/builds/12= -34) -johndoe / my-workspace-3 / #5678 (http://test.com/@johndoe/my-workspace-3/b= -uilds/5678) -jack / workwork / #774 (http://test.com/@jack/workwork/builds/774) -bobby-template-version-2 failed 1 time: +Bobby Second Template + +bobby-template-version-1 failed 3 times: + daniellemaywood / workspace-9 / #9234 (http://test.com/@daniellemaywood= +/workspace-9/builds/9234) + johndoe / my-workspace-7 / #8678 (http://test.com/@johndoe/my-workspace= +-7/builds/8678) + jack / workworkwork / #374 (http://test.com/@jack/workworkwork/builds/3= +74) +bobby-template-version-2 failed 2 times: + ben / more-cool-workspace / #8878 (http://test.com/@ben/more-cool-works= +pace/builds/8878) + ben / less-cool-workspace / #8848 (http://test.com/@ben/less-cool-works= +pace/builds/8848) -ben / cool-workspace / #8888 (http://test.com/@ben/cool-workspace/builds/88= -88) We recommend reviewing these issues to ensure future builds are successful. -View workspaces: http://test.com/workspaces?filter=3Dtemplate%3Abobby-first= --template +View workspaces: http://test.com/workspaces?filter=3Did%3A24f5bd8f-1566-437= +4-9734-c3efa0454dc7+id%3A372a194b-dcde-43f1-b7cf-8a2f3d3114a0+id%3A1386d294= +-19c1-4351-89e2-6cae1afb9bfe+id%3A86fd99b1-1b6e-4b7e-b58e-0aee6e35c159+id%3= +Acd469690-b6eb-4123-b759-980be7a7b278+id%3Ac447d472-0800-4529-a836-788754d5= +e27d+id%3A919db6df-48f0-4dc1-b357-9036a2c40f86+id%3Ac8fb0652-9290-4bf2-a711= +-71b910243ac2+id%3A703d718d-2234-4990-9a02-5b1df6cf462a --bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4 Content-Transfer-Encoding: quoted-printable @@ -46,8 +68,7 @@ Content-Type: text/html; charset=UTF-8 - Codestin Search App

    - Workspace builds failed for template "Bobby First Template" + Failed workspace builds report

    Hi Bobby,

    -

    Template Bobby First Template has failed to bui= -ld 455 times over the last week.

    +

    The following templates have had build failures over the last we= +ek:

    + +
      +
    • Bobby First Template failed to build 4&f= +rasl;55 times

    • + +
    • Bobby Second Template failed to build 5&= +frasl;50 times

    • +

    Report:

    -

    bobby-template-version-1 failed 3 times:

    +

    Bobby First Template

    +
  • bobby-template-version-1 failed 3 times:

    + +
  • + +
  • bobby-template-version-2 failed 1 time:

  • + + +

    Bobby Second Template

    + +

    We recommend reviewing these issues to ensure future builds are successf= @@ -98,10 +157,14 @@ ul.

    =20 - + View workspaces =20 diff --git a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceBuildsFailedReport.json.golden b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceBuildsFailedReport.json.golden index 987d97b91c029..78c8ba2a3195c 100644 --- a/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceBuildsFailedReport.json.golden +++ b/coderd/notifications/testdata/rendered-templates/webhook/TemplateWorkspaceBuildsFailedReport.json.golden @@ -3,7 +3,7 @@ "msg_id": "00000000-0000-0000-0000-000000000000", "payload": { "_version": "1.2", - "notification_name": "Report: Workspace Builds Failed For Template", + "notification_name": "Report: Workspace Builds Failed", "notification_template_id": "00000000-0000-0000-0000-000000000000", "user_id": "00000000-0000-0000-0000-000000000000", "user_email": "bobby@coder.com", @@ -12,56 +12,113 @@ "actions": [ { "label": "View workspaces", - "url": "http://test.com/workspaces?filter=template%3Abobby-first-template" + "url": "http://test.com/workspaces?filter=id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000+id%3A00000000-0000-0000-0000-000000000000" } ], - "labels": { - "template_display_name": "Bobby First Template", - "template_name": "bobby-first-template" - }, + "labels": {}, "data": { - "failed_builds": 4, "report_frequency": "week", - "template_versions": [ + "templates": [ { - "failed_builds": [ - { - "build_number": 1234, - "workspace_name": "workspace-1", - "workspace_owner_username": "mtojek" - }, + "display_name": "Bobby First Template", + "failed_builds": 4, + "name": "bobby-first-template", + "total_builds": 55, + "versions": [ { - "build_number": 5678, - "workspace_name": "my-workspace-3", - "workspace_owner_username": "johndoe" + "failed_builds": [ + { + "build_number": 1234, + "workspace_id": "00000000-0000-0000-0000-000000000000", + "workspace_name": "workspace-1", + "workspace_owner_username": "mtojek" + }, + { + "build_number": 5678, + "workspace_id": "00000000-0000-0000-0000-000000000000", + "workspace_name": "my-workspace-3", + "workspace_owner_username": "johndoe" + }, + { + "build_number": 774, + "workspace_id": "00000000-0000-0000-0000-000000000000", + "workspace_name": "workwork", + "workspace_owner_username": "jack" + } + ], + "failed_count": 3, + "template_version_name": "bobby-template-version-1" }, { - "build_number": 774, - "workspace_name": "workwork", - "workspace_owner_username": "jack" + "failed_builds": [ + { + "build_number": 8888, + "workspace_id": "00000000-0000-0000-0000-000000000000", + "workspace_name": "cool-workspace", + "workspace_owner_username": "ben" + } + ], + "failed_count": 1, + "template_version_name": "bobby-template-version-2" } - ], - "failed_count": 3, - "template_version_name": "bobby-template-version-1" + ] }, { - "failed_builds": [ + "display_name": "Bobby Second Template", + "failed_builds": 5, + "name": "bobby-second-template", + "total_builds": 50, + "versions": [ + { + "failed_builds": [ + { + "build_number": 9234, + "workspace_id": "00000000-0000-0000-0000-000000000000", + "workspace_name": "workspace-9", + "workspace_owner_username": "daniellemaywood" + }, + { + "build_number": 8678, + "workspace_id": "00000000-0000-0000-0000-000000000000", + "workspace_name": "my-workspace-7", + "workspace_owner_username": "johndoe" + }, + { + "build_number": 374, + "workspace_id": "00000000-0000-0000-0000-000000000000", + "workspace_name": "workworkwork", + "workspace_owner_username": "jack" + } + ], + "failed_count": 3, + "template_version_name": "bobby-template-version-1" + }, { - "build_number": 8888, - "workspace_name": "cool-workspace", - "workspace_owner_username": "ben" + "failed_builds": [ + { + "build_number": 8878, + "workspace_id": "00000000-0000-0000-0000-000000000000", + "workspace_name": "more-cool-workspace", + "workspace_owner_username": "ben" + }, + { + "build_number": 8848, + "workspace_id": "00000000-0000-0000-0000-000000000000", + "workspace_name": "less-cool-workspace", + "workspace_owner_username": "ben" + } + ], + "failed_count": 2, + "template_version_name": "bobby-template-version-2" } - ], - "failed_count": 1, - "template_version_name": "bobby-template-version-2" + ] } - ], - "total_builds": 55 + ] }, "targets": null }, - "title": "Workspace builds failed for template \"Bobby First Template\"", - "title_markdown": "Workspace builds failed for template \"Bobby First Template\"", - "body": "Template Bobby First Template has failed to build 4/55 times over the last week.\n\nReport:\n\nbobby-template-version-1 failed 3 times:\n\nmtojek / workspace-1 / #1234 (http://test.com/@mtojek/workspace-1/builds/1234)\njohndoe / my-workspace-3 / #5678 (http://test.com/@johndoe/my-workspace-3/builds/5678)\njack / workwork / #774 (http://test.com/@jack/workwork/builds/774)\n\nbobby-template-version-2 failed 1 time:\n\nben / cool-workspace / #8888 (http://test.com/@ben/cool-workspace/builds/8888)\n\nWe recommend reviewing these issues to ensure future builds are successful.", - "body_markdown": "Template **Bobby First Template** has failed to build 4/55 times over the last week.\n\n**Report:**\n\n**bobby-template-version-1** failed 3 times:\n\n* [mtojek / workspace-1 / #1234](http://test.com/@mtojek/workspace-1/builds/1234)\n* [johndoe / my-workspace-3 / #5678](http://test.com/@johndoe/my-workspace-3/builds/5678)\n* [jack / workwork / #774](http://test.com/@jack/workwork/builds/774)\n\n**bobby-template-version-2** failed 1 time:\n\n* [ben / cool-workspace / #8888](http://test.com/@ben/cool-workspace/builds/8888)\n\nWe recommend reviewing these issues to ensure future builds are successful." + "title": "Failed workspace builds report", + "title_markdown": "Failed workspace builds report", + "body": "The following templates have had build failures over the last week:\n\nBobby First Template failed to build 4/55 times\nBobby Second Template failed to build 5/50 times\n\nReport:\n\nBobby First Template\n\nbobby-template-version-1 failed 3 times:\n mtojek / workspace-1 / #1234 (http://test.com/@mtojek/workspace-1/builds/1234)\n johndoe / my-workspace-3 / #5678 (http://test.com/@johndoe/my-workspace-3/builds/5678)\n jack / workwork / #774 (http://test.com/@jack/workwork/builds/774)\nbobby-template-version-2 failed 1 time:\n ben / cool-workspace / #8888 (http://test.com/@ben/cool-workspace/builds/8888)\n\n\nBobby Second Template\n\nbobby-template-version-1 failed 3 times:\n daniellemaywood / workspace-9 / #9234 (http://test.com/@daniellemaywood/workspace-9/builds/9234)\n johndoe / my-workspace-7 / #8678 (http://test.com/@johndoe/my-workspace-7/builds/8678)\n jack / workworkwork / #374 (http://test.com/@jack/workworkwork/builds/374)\nbobby-template-version-2 failed 2 times:\n ben / more-cool-workspace / #8878 (http://test.com/@ben/more-cool-workspace/builds/8878)\n ben / less-cool-workspace / #8848 (http://test.com/@ben/less-cool-workspace/builds/8848)\n\n\nWe recommend reviewing these issues to ensure future builds are successful.", + "body_markdown": "The following templates have had build failures over the last week:\n\n- **Bobby First Template** failed to build 4/55 times\n\n- **Bobby Second Template** failed to build 5/50 times\n\n\n**Report:**\n\n**Bobby First Template**\n\n- **bobby-template-version-1** failed 3 times:\n\n - [mtojek / workspace-1 / #1234](http://test.com/@mtojek/workspace-1/builds/1234)\n\n - [johndoe / my-workspace-3 / #5678](http://test.com/@johndoe/my-workspace-3/builds/5678)\n\n - [jack / workwork / #774](http://test.com/@jack/workwork/builds/774)\n\n\n- **bobby-template-version-2** failed 1 time:\n\n - [ben / cool-workspace / #8888](http://test.com/@ben/cool-workspace/builds/8888)\n\n\n\n**Bobby Second Template**\n\n- **bobby-template-version-1** failed 3 times:\n\n - [daniellemaywood / workspace-9 / #9234](http://test.com/@daniellemaywood/workspace-9/builds/9234)\n\n - [johndoe / my-workspace-7 / #8678](http://test.com/@johndoe/my-workspace-7/builds/8678)\n\n - [jack / workworkwork / #374](http://test.com/@jack/workworkwork/builds/374)\n\n\n- **bobby-template-version-2** failed 2 times:\n\n - [ben / more-cool-workspace / #8878](http://test.com/@ben/more-cool-workspace/builds/8878)\n\n - [ben / less-cool-workspace / #8848](http://test.com/@ben/less-cool-workspace/builds/8848)\n\n\n\n\nWe recommend reviewing these issues to ensure future builds are successful." } \ No newline at end of file From 25fb34cabead44e7825e7dc8d8a9df23985b7ea2 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 10 Apr 2025 16:16:16 +0300 Subject: [PATCH 049/384] feat(agent): implement recreate for devcontainers (#17308) This change implements an interface for running `@devcontainers/cli up` and an API endpoint on the agent for triggering recreate for a running devcontainer. A couple of limitations: 1. Synchronous HTTP request, meaning the browser might choose to time it out before it's done => no result/error (and devcontainer cli command probably gets killed via ctx cancel). 2. Logs are only written to agent logs via slog, not as a "script" in the UI. Both 1 and 2 will be improved in future refactors. Fixes coder/internal#481 Fixes coder/internal#482 --- .gitattributes | 1 + .github/workflows/typos.toml | 3 +- agent/agentcontainers/containers.go | 85 ++++- .../containers_internal_test.go | 2 +- agent/agentcontainers/containers_test.go | 166 +++++++++ agent/agentcontainers/devcontainer.go | 15 +- agent/agentcontainers/devcontainer_test.go | 16 +- agent/agentcontainers/devcontainercli.go | 193 ++++++++++ agent/agentcontainers/devcontainercli_test.go | 351 ++++++++++++++++++ .../parse/up-already-exists.log | 68 ++++ .../parse/up-error-bad-outcome.log | 1 + .../devcontainercli/parse/up-error-docker.log | 13 + .../parse/up-error-does-not-exist.log | 15 + .../parse/up-remove-existing.log | 212 +++++++++++ .../testdata/devcontainercli/parse/up.log | 206 ++++++++++ agent/api.go | 3 +- codersdk/workspaceagents.go | 10 + 17 files changed, 1338 insertions(+), 22 deletions(-) create mode 100644 agent/agentcontainers/containers_test.go create mode 100644 agent/agentcontainers/devcontainercli.go create mode 100644 agent/agentcontainers/devcontainercli_test.go create mode 100644 agent/agentcontainers/testdata/devcontainercli/parse/up-already-exists.log create mode 100644 agent/agentcontainers/testdata/devcontainercli/parse/up-error-bad-outcome.log create mode 100644 agent/agentcontainers/testdata/devcontainercli/parse/up-error-docker.log create mode 100644 agent/agentcontainers/testdata/devcontainercli/parse/up-error-does-not-exist.log create mode 100644 agent/agentcontainers/testdata/devcontainercli/parse/up-remove-existing.log create mode 100644 agent/agentcontainers/testdata/devcontainercli/parse/up.log diff --git a/.gitattributes b/.gitattributes index 15671f0cc8ac4..1da452829a70a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ # Generated files agent/agentcontainers/acmock/acmock.go linguist-generated=true agent/agentcontainers/dcspec/dcspec_gen.go linguist-generated=true +agent/agentcontainers/testdata/devcontainercli/*/*.log linguist-generated=true coderd/apidoc/docs.go linguist-generated=true docs/reference/api/*.md linguist-generated=true docs/reference/cli/*.md linguist-generated=true diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index 7be99fd037d88..fffd2afbd20a1 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -42,5 +42,6 @@ extend-exclude = [ "site/src/pages/SetupPage/countries.tsx", "provisioner/terraform/testdata/**", # notifications' golden files confuse the detector because of quoted-printable encoding - "coderd/notifications/testdata/**" + "coderd/notifications/testdata/**", + "agent/agentcontainers/testdata/devcontainercli/**" ] diff --git a/agent/agentcontainers/containers.go b/agent/agentcontainers/containers.go index 031d3c7208424..edd099dd842c5 100644 --- a/agent/agentcontainers/containers.go +++ b/agent/agentcontainers/containers.go @@ -9,6 +9,8 @@ import ( "golang.org/x/xerrors" + "github.com/go-chi/chi/v5" + "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" "github.com/coder/quartz" @@ -20,9 +22,10 @@ const ( getContainersTimeout = 5 * time.Second ) -type devcontainersHandler struct { +type Handler struct { cacheDuration time.Duration cl Lister + dccli DevcontainerCLI clock quartz.Clock // lockCh protects the below fields. We use a channel instead of a mutex so we @@ -32,20 +35,26 @@ type devcontainersHandler struct { mtime time.Time } -// Option is a functional option for devcontainersHandler. -type Option func(*devcontainersHandler) +// Option is a functional option for Handler. +type Option func(*Handler) // WithLister sets the agentcontainers.Lister implementation to use. // The default implementation uses the Docker CLI to list containers. func WithLister(cl Lister) Option { - return func(ch *devcontainersHandler) { + return func(ch *Handler) { ch.cl = cl } } -// New returns a new devcontainersHandler with the given options applied. -func New(options ...Option) http.Handler { - ch := &devcontainersHandler{ +func WithDevcontainerCLI(dccli DevcontainerCLI) Option { + return func(ch *Handler) { + ch.dccli = dccli + } +} + +// New returns a new Handler with the given options applied. +func New(options ...Option) *Handler { + ch := &Handler{ lockCh: make(chan struct{}, 1), } for _, opt := range options { @@ -54,7 +63,7 @@ func New(options ...Option) http.Handler { return ch } -func (ch *devcontainersHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { +func (ch *Handler) List(rw http.ResponseWriter, r *http.Request) { select { case <-r.Context().Done(): // Client went away. @@ -80,7 +89,7 @@ func (ch *devcontainersHandler) ServeHTTP(rw http.ResponseWriter, r *http.Reques } } -func (ch *devcontainersHandler) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { +func (ch *Handler) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { select { case <-ctx.Done(): return codersdk.WorkspaceAgentListContainersResponse{}, ctx.Err() @@ -149,3 +158,61 @@ var _ Lister = NoopLister{} func (NoopLister) List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { return codersdk.WorkspaceAgentListContainersResponse{}, nil } + +func (ch *Handler) Recreate(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := chi.URLParam(r, "id") + + if id == "" { + 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.", + }) + return + } + + containers, err := ch.cl.List(ctx) + if err != nil { + httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ + Message: "Could not list containers", + Detail: err.Error(), + }) + return + } + + containerIdx := slices.IndexFunc(containers.Containers, func(c codersdk.WorkspaceAgentContainer) bool { + return c.Match(id) + }) + if containerIdx == -1 { + httpapi.Write(ctx, w, http.StatusNotFound, codersdk.Response{ + Message: "Container not found", + Detail: "Container ID or name not found in the list of containers.", + }) + return + } + + container := containers.Containers[containerIdx] + workspaceFolder := container.Labels[DevcontainerLocalFolderLabel] + configPath := container.Labels[DevcontainerConfigFileLabel] + + // Workspace folder is required to recreate a container, we don't verify + // the config path here because it's optional. + 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.", + }) + return + } + + _, err = ch.dccli.Up(ctx, workspaceFolder, configPath, WithRemoveExistingContainer()) + if err != nil { + httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ + Message: "Could not recreate devcontainer", + Detail: err.Error(), + }) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/agent/agentcontainers/containers_internal_test.go b/agent/agentcontainers/containers_internal_test.go index 81f73bb0e3f17..6b59da407f789 100644 --- a/agent/agentcontainers/containers_internal_test.go +++ b/agent/agentcontainers/containers_internal_test.go @@ -277,7 +277,7 @@ func TestContainersHandler(t *testing.T) { ctrl = gomock.NewController(t) mockLister = acmock.NewMockLister(ctrl) now = time.Now().UTC() - ch = devcontainersHandler{ + ch = Handler{ cacheDuration: tc.cacheDur, cl: mockLister, clock: clk, diff --git a/agent/agentcontainers/containers_test.go b/agent/agentcontainers/containers_test.go new file mode 100644 index 0000000000000..ac479de25419a --- /dev/null +++ b/agent/agentcontainers/containers_test.go @@ -0,0 +1,166 @@ +package agentcontainers_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/agent/agentcontainers" + "github.com/coder/coder/v2/codersdk" +) + +// fakeLister implements the agentcontainers.Lister interface for +// testing. +type fakeLister struct { + containers codersdk.WorkspaceAgentListContainersResponse + err error +} + +func (f *fakeLister) List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { + return f.containers, f.err +} + +// fakeDevcontainerCLI implements the agentcontainers.DevcontainerCLI +// interface for testing. +type fakeDevcontainerCLI struct { + id string + err error +} + +func (f *fakeDevcontainerCLI) Up(_ context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIUpOptions) (string, error) { + return f.id, f.err +} + +func TestHandler(t *testing.T) { + t.Parallel() + + t.Run("Recreate", func(t *testing.T) { + t.Parallel() + + validContainer := codersdk.WorkspaceAgentContainer{ + ID: "container-id", + FriendlyName: "container-name", + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspace", + agentcontainers.DevcontainerConfigFileLabel: "/workspace/.devcontainer/devcontainer.json", + }, + } + + missingFolderContainer := codersdk.WorkspaceAgentContainer{ + ID: "missing-folder-container", + FriendlyName: "missing-folder-container", + Labels: map[string]string{}, + } + + tests := []struct { + name string + containerID string + lister *fakeLister + devcontainerCLI *fakeDevcontainerCLI + wantStatus int + wantBody string + }{ + { + name: "Missing ID", + containerID: "", + lister: &fakeLister{}, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusBadRequest, + wantBody: "Missing container ID or name", + }, + { + name: "List error", + containerID: "container-id", + lister: &fakeLister{ + err: xerrors.New("list error"), + }, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusInternalServerError, + wantBody: "Could not list containers", + }, + { + name: "Container not found", + containerID: "nonexistent-container", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{validContainer}, + }, + }, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusNotFound, + wantBody: "Container not found", + }, + { + name: "Missing workspace folder label", + containerID: "missing-folder-container", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{missingFolderContainer}, + }, + }, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusBadRequest, + wantBody: "Missing workspace folder label", + }, + { + name: "Devcontainer CLI error", + containerID: "container-id", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{validContainer}, + }, + }, + devcontainerCLI: &fakeDevcontainerCLI{ + err: xerrors.New("devcontainer CLI error"), + }, + wantStatus: http.StatusInternalServerError, + wantBody: "Could not recreate devcontainer", + }, + { + name: "OK", + containerID: "container-id", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{validContainer}, + }, + }, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusNoContent, + wantBody: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup router with the handler under test. + r := chi.NewRouter() + handler := agentcontainers.New( + agentcontainers.WithLister(tt.lister), + agentcontainers.WithDevcontainerCLI(tt.devcontainerCLI), + ) + r.Post("/containers/{id}/recreate", handler.Recreate) + + // Simulate HTTP request to the recreate endpoint. + req := httptest.NewRequest(http.MethodPost, "/containers/"+tt.containerID+"/recreate", nil) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + + // Check the response status code and body. + require.Equal(t, tt.wantStatus, rec.Code, "status code mismatch") + if tt.wantBody != "" { + assert.Contains(t, rec.Body.String(), tt.wantBody, "response body mismatch") + } else if tt.wantStatus == http.StatusNoContent { + assert.Empty(t, rec.Body.String(), "expected empty response body") + } + }) + } + }) +} diff --git a/agent/agentcontainers/devcontainer.go b/agent/agentcontainers/devcontainer.go index 59fa9a5e35e82..f93e0722c75b9 100644 --- a/agent/agentcontainers/devcontainer.go +++ b/agent/agentcontainers/devcontainer.go @@ -12,6 +12,15 @@ import ( "github.com/coder/coder/v2/codersdk" ) +const ( + // DevcontainerLocalFolderLabel is the label that contains the path to + // the local workspace folder for a devcontainer. + DevcontainerLocalFolderLabel = "devcontainer.local_folder" + // DevcontainerConfigFileLabel is the label that contains the path to + // the devcontainer.json configuration file. + DevcontainerConfigFileLabel = "devcontainer.config_file" +) + const devcontainerUpScriptTemplate = ` if ! which devcontainer > /dev/null 2>&1; then echo "ERROR: Unable to start devcontainer, @devcontainers/cli is not installed." @@ -52,8 +61,10 @@ ScriptLoop: } func devcontainerStartupScript(dc codersdk.WorkspaceAgentDevcontainer, script codersdk.WorkspaceAgentScript) codersdk.WorkspaceAgentScript { - var args []string - args = append(args, fmt.Sprintf("--workspace-folder %q", dc.WorkspaceFolder)) + args := []string{ + "--log-format json", + fmt.Sprintf("--workspace-folder %q", dc.WorkspaceFolder), + } if dc.ConfigPath != "" { args = append(args, fmt.Sprintf("--config %q", dc.ConfigPath)) } diff --git a/agent/agentcontainers/devcontainer_test.go b/agent/agentcontainers/devcontainer_test.go index eb836af928a50..5e0f5d8dae7bc 100644 --- a/agent/agentcontainers/devcontainer_test.go +++ b/agent/agentcontainers/devcontainer_test.go @@ -101,12 +101,12 @@ func TestExtractAndInitializeDevcontainerScripts(t *testing.T) { wantDevcontainerScripts: []codersdk.WorkspaceAgentScript{ { ID: devcontainerIDs[0], - Script: "devcontainer up --workspace-folder \"workspace1\"", + Script: "devcontainer up --log-format json --workspace-folder \"workspace1\"", RunOnStart: false, }, { ID: devcontainerIDs[1], - Script: "devcontainer up --workspace-folder \"workspace2\"", + Script: "devcontainer up --log-format json --workspace-folder \"workspace2\"", RunOnStart: false, }, }, @@ -136,12 +136,12 @@ func TestExtractAndInitializeDevcontainerScripts(t *testing.T) { wantDevcontainerScripts: []codersdk.WorkspaceAgentScript{ { ID: devcontainerIDs[0], - Script: "devcontainer up --workspace-folder \"workspace1\" --config \"workspace1/config1\"", + Script: "devcontainer up --log-format json --workspace-folder \"workspace1\" --config \"workspace1/config1\"", RunOnStart: false, }, { ID: devcontainerIDs[1], - Script: "devcontainer up --workspace-folder \"workspace2\" --config \"workspace2/config2\"", + Script: "devcontainer up --log-format json --workspace-folder \"workspace2\" --config \"workspace2/config2\"", RunOnStart: false, }, }, @@ -174,12 +174,12 @@ func TestExtractAndInitializeDevcontainerScripts(t *testing.T) { wantDevcontainerScripts: []codersdk.WorkspaceAgentScript{ { ID: devcontainerIDs[0], - Script: "devcontainer up --workspace-folder \"/home/workspace1\" --config \"/home/workspace1/config1\"", + Script: "devcontainer up --log-format json --workspace-folder \"/home/workspace1\" --config \"/home/workspace1/config1\"", RunOnStart: false, }, { ID: devcontainerIDs[1], - Script: "devcontainer up --workspace-folder \"/home/workspace2\" --config \"/home/workspace2/config2\"", + Script: "devcontainer up --log-format json --workspace-folder \"/home/workspace2\" --config \"/home/workspace2/config2\"", RunOnStart: false, }, }, @@ -216,12 +216,12 @@ func TestExtractAndInitializeDevcontainerScripts(t *testing.T) { wantDevcontainerScripts: []codersdk.WorkspaceAgentScript{ { ID: devcontainerIDs[0], - Script: "devcontainer up --workspace-folder \"/home/workspace1\" --config \"/home/config1\"", + Script: "devcontainer up --log-format json --workspace-folder \"/home/workspace1\" --config \"/home/config1\"", RunOnStart: false, }, { ID: devcontainerIDs[1], - Script: "devcontainer up --workspace-folder \"/home/workspace2\" --config \"/config2\"", + Script: "devcontainer up --log-format json --workspace-folder \"/home/workspace2\" --config \"/config2\"", RunOnStart: false, }, }, diff --git a/agent/agentcontainers/devcontainercli.go b/agent/agentcontainers/devcontainercli.go new file mode 100644 index 0000000000000..d6060f862cb40 --- /dev/null +++ b/agent/agentcontainers/devcontainercli.go @@ -0,0 +1,193 @@ +package agentcontainers + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "io" + + "golang.org/x/xerrors" + + "cdr.dev/slog" + "github.com/coder/coder/v2/agent/agentexec" +) + +// DevcontainerCLI is an interface for the devcontainer CLI. +type DevcontainerCLI interface { + Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (id string, err error) +} + +// DevcontainerCLIUpOptions are options for the devcontainer CLI up +// command. +type DevcontainerCLIUpOptions func(*devcontainerCLIUpConfig) + +// WithRemoveExistingContainer is an option to remove the existing +// container. +func WithRemoveExistingContainer() DevcontainerCLIUpOptions { + return func(o *devcontainerCLIUpConfig) { + o.removeExistingContainer = true + } +} + +type devcontainerCLIUpConfig struct { + removeExistingContainer bool +} + +func applyDevcontainerCLIUpOptions(opts []DevcontainerCLIUpOptions) devcontainerCLIUpConfig { + conf := devcontainerCLIUpConfig{ + removeExistingContainer: false, + } + for _, opt := range opts { + if opt != nil { + opt(&conf) + } + } + return conf +} + +type devcontainerCLI struct { + logger slog.Logger + execer agentexec.Execer +} + +var _ DevcontainerCLI = &devcontainerCLI{} + +func NewDevcontainerCLI(logger slog.Logger, execer agentexec.Execer) DevcontainerCLI { + return &devcontainerCLI{ + execer: execer, + logger: logger, + } +} + +func (d *devcontainerCLI) Up(ctx context.Context, workspaceFolder, configPath string, opts ...DevcontainerCLIUpOptions) (string, error) { + conf := applyDevcontainerCLIUpOptions(opts) + logger := d.logger.With(slog.F("workspace_folder", workspaceFolder), slog.F("config_path", configPath), slog.F("recreate", conf.removeExistingContainer)) + + args := []string{ + "up", + "--log-format", "json", + "--workspace-folder", workspaceFolder, + } + if configPath != "" { + args = append(args, "--config", configPath) + } + if conf.removeExistingContainer { + args = append(args, "--remove-existing-container") + } + 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))} + + if err := cmd.Run(); err != nil { + if _, err2 := parseDevcontainerCLILastLine(ctx, logger, stdout.Bytes()); err2 != nil { + err = errors.Join(err, err2) + } + return "", err + } + + result, err := parseDevcontainerCLILastLine(ctx, logger, stdout.Bytes()) + if err != nil { + return "", err + } + + return result.ContainerID, nil +} + +// parseDevcontainerCLILastLine parses the last line of the devcontainer CLI output +// which is a JSON object. +func parseDevcontainerCLILastLine(ctx context.Context, logger slog.Logger, p []byte) (result devcontainerCLIResult, err error) { + s := bufio.NewScanner(bytes.NewReader(p)) + var lastLine []byte + for s.Scan() { + b := s.Bytes() + if len(b) == 0 || b[0] != '{' { + continue + } + lastLine = b + } + if err = s.Err(); err != nil { + return result, err + } + if len(lastLine) == 0 || lastLine[0] != '{' { + logger.Error(ctx, "devcontainer result is not json", slog.F("result", string(lastLine))) + return result, xerrors.Errorf("devcontainer result is not json: %q", string(lastLine)) + } + if err = json.Unmarshal(lastLine, &result); err != nil { + logger.Error(ctx, "parse devcontainer result failed", slog.Error(err), slog.F("result", string(lastLine))) + return result, err + } + + return result, result.Err() +} + +// devcontainerCLIResult is the result of the devcontainer CLI command. +// It is parsed from the last line of the devcontainer CLI stdout which +// is a JSON object. +type devcontainerCLIResult struct { + Outcome string `json:"outcome"` // "error", "success". + + // The following fields are set if outcome is success. + ContainerID string `json:"containerId"` + RemoteUser string `json:"remoteUser"` + RemoteWorkspaceFolder string `json:"remoteWorkspaceFolder"` + + // The following fields are set if outcome is error. + Message string `json:"message"` + Description string `json:"description"` +} + +func (r devcontainerCLIResult) Err() error { + if r.Outcome == "success" { + return nil + } + return xerrors.Errorf("devcontainer up failed: %s (description: %s, message: %s)", r.Outcome, r.Description, r.Message) +} + +// devcontainerCLIJSONLogLine is a log line from the devcontainer CLI. +type devcontainerCLIJSONLogLine struct { + Type string `json:"type"` // "progress", "raw", "start", "stop", "text", etc. + Level int `json:"level"` // 1, 2, 3. + Timestamp int `json:"timestamp"` // Unix timestamp in milliseconds. + Text string `json:"text"` + + // More fields can be added here as needed. +} + +// devcontainerCLILogWriter splits on newlines and logs each line +// separately. +type devcontainerCLILogWriter struct { + ctx context.Context + logger slog.Logger +} + +func (l *devcontainerCLILogWriter) Write(p []byte) (n int, err error) { + s := bufio.NewScanner(bytes.NewReader(p)) + for s.Scan() { + line := s.Bytes() + if len(line) == 0 { + continue + } + if line[0] != '{' { + l.logger.Debug(l.ctx, "@devcontainer/cli", slog.F("line", string(line))) + continue + } + var logLine devcontainerCLIJSONLogLine + if err := json.Unmarshal(line, &logLine); err != nil { + l.logger.Error(l.ctx, "parse devcontainer json log line failed", slog.Error(err), slog.F("line", string(line))) + continue + } + if logLine.Level >= 3 { + l.logger.Info(l.ctx, "@devcontainer/cli", slog.F("line", string(line))) + continue + } + l.logger.Debug(l.ctx, "@devcontainer/cli", slog.F("line", string(line))) + } + if err := s.Err(); err != nil { + l.logger.Error(l.ctx, "devcontainer log line scan failed", slog.Error(err)) + } + return len(p), nil +} diff --git a/agent/agentcontainers/devcontainercli_test.go b/agent/agentcontainers/devcontainercli_test.go new file mode 100644 index 0000000000000..22a81fb8e38a2 --- /dev/null +++ b/agent/agentcontainers/devcontainercli_test.go @@ -0,0 +1,351 @@ +package agentcontainers_test + +import ( + "bytes" + "context" + "errors" + "flag" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/agent/agentcontainers" + "github.com/coder/coder/v2/agent/agentexec" + "github.com/coder/coder/v2/pty" + "github.com/coder/coder/v2/testutil" +) + +func TestDevcontainerCLI_ArgsAndParsing(t *testing.T) { + t.Parallel() + + testExePath, err := os.Executable() + require.NoError(t, err, "get test executable path") + + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + + t.Run("Up", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + logFile string + workspace string + config string + opts []agentcontainers.DevcontainerCLIUpOptions + wantArgs string + wantError bool + }{ + { + name: "success", + logFile: "up.log", + workspace: "/test/workspace", + wantArgs: "up --log-format json --workspace-folder /test/workspace", + wantError: false, + }, + { + name: "success with config", + logFile: "up.log", + workspace: "/test/workspace", + config: "/test/config.json", + wantArgs: "up --log-format json --workspace-folder /test/workspace --config /test/config.json", + wantError: false, + }, + { + name: "already exists", + logFile: "up-already-exists.log", + workspace: "/test/workspace", + wantArgs: "up --log-format json --workspace-folder /test/workspace", + wantError: false, + }, + { + name: "docker error", + logFile: "up-error-docker.log", + workspace: "/test/workspace", + wantArgs: "up --log-format json --workspace-folder /test/workspace", + wantError: true, + }, + { + name: "bad outcome", + logFile: "up-error-bad-outcome.log", + workspace: "/test/workspace", + wantArgs: "up --log-format json --workspace-folder /test/workspace", + wantError: true, + }, + { + name: "does not exist", + logFile: "up-error-does-not-exist.log", + workspace: "/test/workspace", + wantArgs: "up --log-format json --workspace-folder /test/workspace", + wantError: true, + }, + { + name: "with remove existing container", + logFile: "up.log", + workspace: "/test/workspace", + opts: []agentcontainers.DevcontainerCLIUpOptions{ + agentcontainers.WithRemoveExistingContainer(), + }, + wantArgs: "up --log-format json --workspace-folder /test/workspace --remove-existing-container", + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + + testExecer := &testDevcontainerExecer{ + testExePath: testExePath, + wantArgs: tt.wantArgs, + wantError: tt.wantError, + logFile: filepath.Join("testdata", "devcontainercli", "parse", tt.logFile), + } + + dccli := agentcontainers.NewDevcontainerCLI(logger, testExecer) + containerID, err := dccli.Up(ctx, tt.workspace, tt.config, tt.opts...) + if tt.wantError { + assert.Error(t, err, "want error") + assert.Empty(t, containerID, "expected empty container ID") + } else { + assert.NoError(t, err, "want no error") + assert.NotEmpty(t, containerID, "expected non-empty container ID") + } + }) + } + }) +} + +// testDevcontainerExecer implements the agentexec.Execer interface for testing. +type testDevcontainerExecer struct { + testExePath string + wantArgs string + wantError bool + logFile string +} + +// CommandContext returns a test binary command that simulates devcontainer responses. +func (e *testDevcontainerExecer) CommandContext(ctx context.Context, name string, args ...string) *exec.Cmd { + // Only handle "devcontainer" commands. + if name != "devcontainer" { + // For non-devcontainer commands, use a standard execer. + return agentexec.DefaultExecer.CommandContext(ctx, name, args...) + } + + // Create a command that runs the test binary with special flags + // that tell it to simulate a devcontainer command. + testArgs := []string{ + "-test.run=TestDevcontainerHelperProcess", + "--", + name, + } + testArgs = append(testArgs, args...) + + //nolint:gosec // This is a test binary, so we don't need to worry about command injection. + cmd := exec.CommandContext(ctx, e.testExePath, testArgs...) + // Set this environment variable so the child process knows it's the helper. + cmd.Env = append(os.Environ(), + "TEST_DEVCONTAINER_WANT_HELPER_PROCESS=1", + "TEST_DEVCONTAINER_WANT_ARGS="+e.wantArgs, + "TEST_DEVCONTAINER_WANT_ERROR="+fmt.Sprintf("%v", e.wantError), + "TEST_DEVCONTAINER_LOG_FILE="+e.logFile, + ) + + return cmd +} + +// PTYCommandContext returns a PTY command. +func (*testDevcontainerExecer) PTYCommandContext(_ context.Context, name string, args ...string) *pty.Cmd { + // This method shouldn't be called for our devcontainer tests. + panic("PTYCommandContext not expected in devcontainer tests") +} + +// This is a special test helper that is executed as a subprocess. +// It simulates the behavior of the devcontainer CLI. +// +//nolint:revive,paralleltest // This is a test helper function. +func TestDevcontainerHelperProcess(t *testing.T) { + // If not called by the test as a helper process, do nothing. + if os.Getenv("TEST_DEVCONTAINER_WANT_HELPER_PROCESS") != "1" { + return + } + + helperArgs := flag.Args() + if len(helperArgs) < 1 { + fmt.Fprintf(os.Stderr, "No command\n") + os.Exit(2) + } + + if helperArgs[0] != "devcontainer" { + fmt.Fprintf(os.Stderr, "Unknown command: %s\n", helperArgs[0]) + os.Exit(2) + } + + // Verify arguments against expected arguments and skip + // "devcontainer", it's not included in the input args. + wantArgs := os.Getenv("TEST_DEVCONTAINER_WANT_ARGS") + gotArgs := strings.Join(helperArgs[1:], " ") + if gotArgs != wantArgs { + fmt.Fprintf(os.Stderr, "Arguments don't match.\nWant: %q\nGot: %q\n", + wantArgs, gotArgs) + os.Exit(2) + } + + logFilePath := os.Getenv("TEST_DEVCONTAINER_LOG_FILE") + output, err := os.ReadFile(logFilePath) + if err != nil { + fmt.Fprintf(os.Stderr, "Reading log file %s failed: %v\n", logFilePath, err) + os.Exit(2) + } + + _, _ = io.Copy(os.Stdout, bytes.NewReader(output)) + if os.Getenv("TEST_DEVCONTAINER_WANT_ERROR") == "true" { + os.Exit(1) + } + os.Exit(0) +} + +// TestDockerDevcontainerCLI tests the DevcontainerCLI component with real Docker containers. +// This test verifies that containers can be created and recreated using the actual +// devcontainer CLI and Docker. It is skipped by default and can be run with: +// +// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerDevcontainerCLI +// +// The test requires Docker to be installed and running. +func TestDockerDevcontainerCLI(t *testing.T) { + t.Parallel() + if os.Getenv("CODER_TEST_USE_DOCKER") != "1" { + t.Skip("skipping Docker test; set CODER_TEST_USE_DOCKER=1 to run") + } + + // Connect to Docker. + pool, err := dockertest.NewPool("") + require.NoError(t, err, "connect to Docker") + + t.Run("ContainerLifecycle", func(t *testing.T) { + t.Parallel() + + // Set up workspace directory with a devcontainer configuration. + workspaceFolder := t.TempDir() + configPath := setupDevcontainerWorkspace(t, workspaceFolder) + + // Use a long timeout because container operations are slow. + ctx := testutil.Context(t, testutil.WaitLong) + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + + // Create the devcontainer CLI under test. + dccli := agentcontainers.NewDevcontainerCLI(logger, agentexec.DefaultExecer) + + // Create a container. + firstID, err := dccli.Up(ctx, workspaceFolder, configPath) + require.NoError(t, err, "create container") + require.NotEmpty(t, firstID, "container ID should not be empty") + defer removeDevcontainerByID(t, pool, firstID) + + // Verify container exists. + firstContainer, found := findDevcontainerByID(t, pool, firstID) + require.True(t, found, "container should exist") + + // Remember the container creation time. + firstCreated := firstContainer.Created + + // Recreate the container. + secondID, err := dccli.Up(ctx, workspaceFolder, configPath, agentcontainers.WithRemoveExistingContainer()) + require.NoError(t, err, "recreate container") + require.NotEmpty(t, secondID, "recreated container ID should not be empty") + defer removeDevcontainerByID(t, pool, secondID) + + // Verify the new container exists and is different. + secondContainer, found := findDevcontainerByID(t, pool, secondID) + require.True(t, found, "recreated container should exist") + + // Verify it's a different container by checking creation time. + secondCreated := secondContainer.Created + assert.NotEqual(t, firstCreated, secondCreated, "recreated container should have different creation time") + + // Verify the first container is removed by the recreation. + _, found = findDevcontainerByID(t, pool, firstID) + assert.False(t, found, "first container should be removed") + }) +} + +// setupDevcontainerWorkspace prepares a test environment with a minimal +// devcontainer.json configuration and returns the path to the config file. +func setupDevcontainerWorkspace(t *testing.T, workspaceFolder string) string { + t.Helper() + + // Create the devcontainer directory structure. + devcontainerDir := filepath.Join(workspaceFolder, ".devcontainer") + err := os.MkdirAll(devcontainerDir, 0o755) + require.NoError(t, err, "create .devcontainer directory") + + // Write a minimal configuration with test labels for identification. + configPath := filepath.Join(devcontainerDir, "devcontainer.json") + content := `{ + "image": "alpine:latest", + "containerEnv": { + "TEST_CONTAINER": "true" + }, + "runArgs": ["--label", "com.coder.test=devcontainercli"] +}` + err = os.WriteFile(configPath, []byte(content), 0o600) + require.NoError(t, err, "create devcontainer.json file") + + return configPath +} + +// findDevcontainerByID locates a container by its ID and verifies it has our +// test label. Returns the container and whether it was found. +func findDevcontainerByID(t *testing.T, pool *dockertest.Pool, id string) (*docker.Container, bool) { + t.Helper() + + container, err := pool.Client.InspectContainer(id) + if err != nil { + t.Logf("Inspect container failed: %v", err) + return nil, false + } + require.Equal(t, "devcontainercli", container.Config.Labels["com.coder.test"], "sanity check failed: container should have the test label") + + return container, true +} + +// removeDevcontainerByID safely cleans up a test container by ID, verifying +// it has our test label before removal to prevent accidental deletion. +func removeDevcontainerByID(t *testing.T, pool *dockertest.Pool, id string) { + t.Helper() + + errNoSuchContainer := &docker.NoSuchContainer{} + + // Check if the container has the expected label. + container, err := pool.Client.InspectContainer(id) + if err != nil { + if errors.As(err, &errNoSuchContainer) { + t.Logf("Container %s not found, skipping removal", id) + return + } + require.NoError(t, err, "inspect container") + } + require.Equal(t, "devcontainercli", container.Config.Labels["com.coder.test"], "sanity check failed: container should have the test label") + + t.Logf("Removing container with ID: %s", id) + err = pool.Client.RemoveContainer(docker.RemoveContainerOptions{ + ID: container.ID, + Force: true, + RemoveVolumes: true, + }) + if err != nil && !errors.As(err, &errNoSuchContainer) { + assert.NoError(t, err, "remove container failed") + } +} diff --git a/agent/agentcontainers/testdata/devcontainercli/parse/up-already-exists.log b/agent/agentcontainers/testdata/devcontainercli/parse/up-already-exists.log new file mode 100644 index 0000000000000..de5375e23a234 --- /dev/null +++ b/agent/agentcontainers/testdata/devcontainercli/parse/up-already-exists.log @@ -0,0 +1,68 @@ +{"type":"text","level":3,"timestamp":1744102135254,"text":"@devcontainers/cli 0.75.0. Node.js v23.9.0. darwin 24.4.0 arm64."} +{"type":"start","level":2,"timestamp":1744102135254,"text":"Run: docker buildx version"} +{"type":"stop","level":2,"timestamp":1744102135300,"text":"Run: docker buildx version","startTimestamp":1744102135254} +{"type":"text","level":2,"timestamp":1744102135300,"text":"github.com/docker/buildx v0.21.2 1360a9e8d25a2c3d03c2776d53ae62e6ff0a843d\r\n"} +{"type":"text","level":2,"timestamp":1744102135300,"text":"\u001b[1m\u001b[31m\u001b[39m\u001b[22m\r\n"} +{"type":"start","level":2,"timestamp":1744102135300,"text":"Run: docker -v"} +{"type":"stop","level":2,"timestamp":1744102135309,"text":"Run: docker -v","startTimestamp":1744102135300} +{"type":"start","level":2,"timestamp":1744102135309,"text":"Resolving Remote"} +{"type":"start","level":2,"timestamp":1744102135311,"text":"Run: git rev-parse --show-cdup"} +{"type":"stop","level":2,"timestamp":1744102135316,"text":"Run: git rev-parse --show-cdup","startTimestamp":1744102135311} +{"type":"start","level":2,"timestamp":1744102135316,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"stop","level":2,"timestamp":1744102135333,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744102135316} +{"type":"start","level":2,"timestamp":1744102135333,"text":"Run: docker inspect --type container 4f22413fe134"} +{"type":"stop","level":2,"timestamp":1744102135347,"text":"Run: docker inspect --type container 4f22413fe134","startTimestamp":1744102135333} +{"type":"start","level":2,"timestamp":1744102135348,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"stop","level":2,"timestamp":1744102135364,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744102135348} +{"type":"start","level":2,"timestamp":1744102135364,"text":"Run: docker inspect --type container 4f22413fe134"} +{"type":"stop","level":2,"timestamp":1744102135378,"text":"Run: docker inspect --type container 4f22413fe134","startTimestamp":1744102135364} +{"type":"start","level":2,"timestamp":1744102135379,"text":"Inspecting container"} +{"type":"start","level":2,"timestamp":1744102135379,"text":"Run: docker inspect --type container 4f22413fe13472200500a66ca057df5aafba6b45743afd499c3f26fc886eb236"} +{"type":"stop","level":2,"timestamp":1744102135393,"text":"Run: docker inspect --type container 4f22413fe13472200500a66ca057df5aafba6b45743afd499c3f26fc886eb236","startTimestamp":1744102135379} +{"type":"stop","level":2,"timestamp":1744102135393,"text":"Inspecting container","startTimestamp":1744102135379} +{"type":"start","level":2,"timestamp":1744102135393,"text":"Run in container: /bin/sh"} +{"type":"start","level":2,"timestamp":1744102135394,"text":"Run in container: uname -m"} +{"type":"text","level":2,"timestamp":1744102135428,"text":"aarch64\n"} +{"type":"text","level":2,"timestamp":1744102135428,"text":""} +{"type":"stop","level":2,"timestamp":1744102135428,"text":"Run in container: uname -m","startTimestamp":1744102135394} +{"type":"start","level":2,"timestamp":1744102135428,"text":"Run in container: (cat /etc/os-release || cat /usr/lib/os-release) 2>/dev/null"} +{"type":"text","level":2,"timestamp":1744102135428,"text":"PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n"} +{"type":"text","level":2,"timestamp":1744102135428,"text":""} +{"type":"stop","level":2,"timestamp":1744102135428,"text":"Run in container: (cat /etc/os-release || cat /usr/lib/os-release) 2>/dev/null","startTimestamp":1744102135428} +{"type":"start","level":2,"timestamp":1744102135429,"text":"Run in container: (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true)"} +{"type":"stop","level":2,"timestamp":1744102135429,"text":"Run in container: (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true)","startTimestamp":1744102135429} +{"type":"start","level":2,"timestamp":1744102135430,"text":"Run in container: test -f '/var/devcontainer/.patchEtcEnvironmentMarker'"} +{"type":"text","level":2,"timestamp":1744102135430,"text":""} +{"type":"text","level":2,"timestamp":1744102135430,"text":""} +{"type":"stop","level":2,"timestamp":1744102135430,"text":"Run in container: test -f '/var/devcontainer/.patchEtcEnvironmentMarker'","startTimestamp":1744102135430} +{"type":"start","level":2,"timestamp":1744102135430,"text":"Run in container: test -f '/var/devcontainer/.patchEtcProfileMarker'"} +{"type":"text","level":2,"timestamp":1744102135430,"text":""} +{"type":"text","level":2,"timestamp":1744102135430,"text":""} +{"type":"stop","level":2,"timestamp":1744102135430,"text":"Run in container: test -f '/var/devcontainer/.patchEtcProfileMarker'","startTimestamp":1744102135430} +{"type":"text","level":2,"timestamp":1744102135431,"text":"userEnvProbe: loginInteractiveShell (default)"} +{"type":"text","level":1,"timestamp":1744102135431,"text":"LifecycleCommandExecutionMap: {\n \"onCreateCommand\": [],\n \"updateContentCommand\": [],\n \"postCreateCommand\": [\n {\n \"origin\": \"devcontainer.json\",\n \"command\": \"npm install -g @devcontainers/cli\"\n }\n ],\n \"postStartCommand\": [],\n \"postAttachCommand\": [],\n \"initializeCommand\": []\n}"} +{"type":"text","level":2,"timestamp":1744102135431,"text":"userEnvProbe: not found in cache"} +{"type":"text","level":2,"timestamp":1744102135431,"text":"userEnvProbe shell: /bin/bash"} +{"type":"start","level":2,"timestamp":1744102135431,"text":"Run in container: /bin/bash -lic echo -n 5805f204-cd2b-4911-8a88-96de28d5deb7; cat /proc/self/environ; echo -n 5805f204-cd2b-4911-8a88-96de28d5deb7"} +{"type":"start","level":2,"timestamp":1744102135431,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.onCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.onCreateCommandMarker'"} +{"type":"text","level":2,"timestamp":1744102135432,"text":""} +{"type":"text","level":2,"timestamp":1744102135432,"text":""} +{"type":"text","level":2,"timestamp":1744102135432,"text":"Exit code 1"} +{"type":"stop","level":2,"timestamp":1744102135432,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.onCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.onCreateCommandMarker'","startTimestamp":1744102135431} +{"type":"start","level":2,"timestamp":1744102135432,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.updateContentCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.updateContentCommandMarker'"} +{"type":"text","level":2,"timestamp":1744102135434,"text":""} +{"type":"text","level":2,"timestamp":1744102135434,"text":""} +{"type":"text","level":2,"timestamp":1744102135434,"text":"Exit code 1"} +{"type":"stop","level":2,"timestamp":1744102135434,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.updateContentCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.updateContentCommandMarker'","startTimestamp":1744102135432} +{"type":"start","level":2,"timestamp":1744102135434,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.postCreateCommandMarker'"} +{"type":"text","level":2,"timestamp":1744102135435,"text":""} +{"type":"text","level":2,"timestamp":1744102135435,"text":""} +{"type":"text","level":2,"timestamp":1744102135435,"text":"Exit code 1"} +{"type":"stop","level":2,"timestamp":1744102135435,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-07T09:21:41.201379807Z}\" != '2025-04-07T09:21:41.201379807Z' ] && echo '2025-04-07T09:21:41.201379807Z' > '/home/node/.devcontainer/.postCreateCommandMarker'","startTimestamp":1744102135434} +{"type":"start","level":2,"timestamp":1744102135435,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postStartCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:48:29.406495039Z}\" != '2025-04-08T08:48:29.406495039Z' ] && echo '2025-04-08T08:48:29.406495039Z' > '/home/node/.devcontainer/.postStartCommandMarker'"} +{"type":"text","level":2,"timestamp":1744102135436,"text":""} +{"type":"text","level":2,"timestamp":1744102135436,"text":""} +{"type":"text","level":2,"timestamp":1744102135436,"text":"Exit code 1"} +{"type":"stop","level":2,"timestamp":1744102135436,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postStartCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:48:29.406495039Z}\" != '2025-04-08T08:48:29.406495039Z' ] && echo '2025-04-08T08:48:29.406495039Z' > '/home/node/.devcontainer/.postStartCommandMarker'","startTimestamp":1744102135435} +{"type":"stop","level":2,"timestamp":1744102135436,"text":"Resolving Remote","startTimestamp":1744102135309} +{"outcome":"success","containerId":"4f22413fe13472200500a66ca057df5aafba6b45743afd499c3f26fc886eb236","remoteUser":"node","remoteWorkspaceFolder":"/workspaces/devcontainers-template-starter"} diff --git a/agent/agentcontainers/testdata/devcontainercli/parse/up-error-bad-outcome.log b/agent/agentcontainers/testdata/devcontainercli/parse/up-error-bad-outcome.log new file mode 100644 index 0000000000000..386621d6dc800 --- /dev/null +++ b/agent/agentcontainers/testdata/devcontainercli/parse/up-error-bad-outcome.log @@ -0,0 +1 @@ +bad outcome diff --git a/agent/agentcontainers/testdata/devcontainercli/parse/up-error-docker.log b/agent/agentcontainers/testdata/devcontainercli/parse/up-error-docker.log new file mode 100644 index 0000000000000..d470079f17460 --- /dev/null +++ b/agent/agentcontainers/testdata/devcontainercli/parse/up-error-docker.log @@ -0,0 +1,13 @@ +{"type":"text","level":3,"timestamp":1744102042893,"text":"@devcontainers/cli 0.75.0. Node.js v23.9.0. darwin 24.4.0 arm64."} +{"type":"start","level":2,"timestamp":1744102042893,"text":"Run: docker buildx version"} +{"type":"stop","level":2,"timestamp":1744102042941,"text":"Run: docker buildx version","startTimestamp":1744102042893} +{"type":"text","level":2,"timestamp":1744102042941,"text":"github.com/docker/buildx v0.21.2 1360a9e8d25a2c3d03c2776d53ae62e6ff0a843d\r\n"} +{"type":"text","level":2,"timestamp":1744102042941,"text":"\u001b[1m\u001b[31m\u001b[39m\u001b[22m\r\n"} +{"type":"start","level":2,"timestamp":1744102042941,"text":"Run: docker -v"} +{"type":"stop","level":2,"timestamp":1744102042950,"text":"Run: docker -v","startTimestamp":1744102042941} +{"type":"start","level":2,"timestamp":1744102042950,"text":"Resolving Remote"} +{"type":"start","level":2,"timestamp":1744102042952,"text":"Run: git rev-parse --show-cdup"} +{"type":"stop","level":2,"timestamp":1744102042957,"text":"Run: git rev-parse --show-cdup","startTimestamp":1744102042952} +{"type":"start","level":2,"timestamp":1744102042957,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"stop","level":2,"timestamp":1744102042967,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744102042957} +{"outcome":"error","message":"Command failed: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json","description":"An error occurred setting up the container."} diff --git a/agent/agentcontainers/testdata/devcontainercli/parse/up-error-does-not-exist.log b/agent/agentcontainers/testdata/devcontainercli/parse/up-error-does-not-exist.log new file mode 100644 index 0000000000000..191bfc7fad6ff --- /dev/null +++ b/agent/agentcontainers/testdata/devcontainercli/parse/up-error-does-not-exist.log @@ -0,0 +1,15 @@ +{"type":"text","level":3,"timestamp":1744102555495,"text":"@devcontainers/cli 0.75.0. Node.js v23.9.0. darwin 24.4.0 arm64."} +{"type":"start","level":2,"timestamp":1744102555495,"text":"Run: docker buildx version"} +{"type":"stop","level":2,"timestamp":1744102555539,"text":"Run: docker buildx version","startTimestamp":1744102555495} +{"type":"text","level":2,"timestamp":1744102555539,"text":"github.com/docker/buildx v0.21.2 1360a9e8d25a2c3d03c2776d53ae62e6ff0a843d\r\n"} +{"type":"text","level":2,"timestamp":1744102555539,"text":"\u001b[1m\u001b[31m\u001b[39m\u001b[22m\r\n"} +{"type":"start","level":2,"timestamp":1744102555539,"text":"Run: docker -v"} +{"type":"stop","level":2,"timestamp":1744102555548,"text":"Run: docker -v","startTimestamp":1744102555539} +{"type":"start","level":2,"timestamp":1744102555548,"text":"Resolving Remote"} +Error: Dev container config (/code/devcontainers-template-starter/foo/.devcontainer/devcontainer.json) not found. + at H6 (/opt/homebrew/Cellar/devcontainer/0.75.0/libexec/lib/node_modules/@devcontainers/cli/dist/spec-node/devContainersSpecCLI.js:484:3219) + at async BC (/opt/homebrew/Cellar/devcontainer/0.75.0/libexec/lib/node_modules/@devcontainers/cli/dist/spec-node/devContainersSpecCLI.js:484:4957) + at async d7 (/opt/homebrew/Cellar/devcontainer/0.75.0/libexec/lib/node_modules/@devcontainers/cli/dist/spec-node/devContainersSpecCLI.js:665:202) + at async f7 (/opt/homebrew/Cellar/devcontainer/0.75.0/libexec/lib/node_modules/@devcontainers/cli/dist/spec-node/devContainersSpecCLI.js:664:14804) + at async /opt/homebrew/Cellar/devcontainer/0.75.0/libexec/lib/node_modules/@devcontainers/cli/dist/spec-node/devContainersSpecCLI.js:484:1188 +{"outcome":"error","message":"Dev container config (/code/devcontainers-template-starter/foo/.devcontainer/devcontainer.json) not found.","description":"Dev container config (/code/devcontainers-template-starter/foo/.devcontainer/devcontainer.json) not found."} diff --git a/agent/agentcontainers/testdata/devcontainercli/parse/up-remove-existing.log b/agent/agentcontainers/testdata/devcontainercli/parse/up-remove-existing.log new file mode 100644 index 0000000000000..d1ae1b747b3e9 --- /dev/null +++ b/agent/agentcontainers/testdata/devcontainercli/parse/up-remove-existing.log @@ -0,0 +1,212 @@ +{"type":"text","level":3,"timestamp":1744115789408,"text":"@devcontainers/cli 0.75.0. Node.js v23.9.0. darwin 24.4.0 arm64."} +{"type":"start","level":2,"timestamp":1744115789408,"text":"Run: docker buildx version"} +{"type":"stop","level":2,"timestamp":1744115789460,"text":"Run: docker buildx version","startTimestamp":1744115789408} +{"type":"text","level":2,"timestamp":1744115789460,"text":"github.com/docker/buildx v0.21.2 1360a9e8d25a2c3d03c2776d53ae62e6ff0a843d\r\n"} +{"type":"text","level":2,"timestamp":1744115789460,"text":"\u001b[1m\u001b[31m\u001b[39m\u001b[22m\r\n"} +{"type":"start","level":2,"timestamp":1744115789460,"text":"Run: docker -v"} +{"type":"stop","level":2,"timestamp":1744115789470,"text":"Run: docker -v","startTimestamp":1744115789460} +{"type":"start","level":2,"timestamp":1744115789470,"text":"Resolving Remote"} +{"type":"start","level":2,"timestamp":1744115789472,"text":"Run: git rev-parse --show-cdup"} +{"type":"stop","level":2,"timestamp":1744115789477,"text":"Run: git rev-parse --show-cdup","startTimestamp":1744115789472} +{"type":"start","level":2,"timestamp":1744115789477,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/Users/maf/Documents/Code/devcontainers-template-starter --filter label=devcontainer.config_file=/Users/maf/Documents/Code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"stop","level":2,"timestamp":1744115789523,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/Users/maf/Documents/Code/devcontainers-template-starter --filter label=devcontainer.config_file=/Users/maf/Documents/Code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744115789477} +{"type":"start","level":2,"timestamp":1744115789523,"text":"Run: docker inspect --type container bc72db8d0c4c"} +{"type":"stop","level":2,"timestamp":1744115789539,"text":"Run: docker inspect --type container bc72db8d0c4c","startTimestamp":1744115789523} +{"type":"start","level":2,"timestamp":1744115789733,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/Users/maf/Documents/Code/devcontainers-template-starter --filter label=devcontainer.config_file=/Users/maf/Documents/Code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"stop","level":2,"timestamp":1744115789759,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/Users/maf/Documents/Code/devcontainers-template-starter --filter label=devcontainer.config_file=/Users/maf/Documents/Code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744115789733} +{"type":"start","level":2,"timestamp":1744115789759,"text":"Run: docker inspect --type container bc72db8d0c4c"} +{"type":"stop","level":2,"timestamp":1744115789779,"text":"Run: docker inspect --type container bc72db8d0c4c","startTimestamp":1744115789759} +{"type":"start","level":2,"timestamp":1744115789779,"text":"Removing Existing Container"} +{"type":"start","level":2,"timestamp":1744115789779,"text":"Run: docker rm -f bc72db8d0c4c4e941bd9ffc341aee64a18d3397fd45b87cd93d4746150967ba8"} +{"type":"stop","level":2,"timestamp":1744115789992,"text":"Run: docker rm -f bc72db8d0c4c4e941bd9ffc341aee64a18d3397fd45b87cd93d4746150967ba8","startTimestamp":1744115789779} +{"type":"stop","level":2,"timestamp":1744115789992,"text":"Removing Existing Container","startTimestamp":1744115789779} +{"type":"start","level":2,"timestamp":1744115789993,"text":"Run: docker inspect --type image mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye"} +{"type":"stop","level":2,"timestamp":1744115790007,"text":"Run: docker inspect --type image mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye","startTimestamp":1744115789993} +{"type":"text","level":1,"timestamp":1744115790008,"text":"workspace root: /Users/maf/Documents/Code/devcontainers-template-starter"} +{"type":"text","level":1,"timestamp":1744115790008,"text":"configPath: /Users/maf/Documents/Code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"text","level":1,"timestamp":1744115790008,"text":"--- Processing User Features ----"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"[* user-provided] ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":3,"timestamp":1744115790009,"text":"Resolving Feature dependencies for 'ghcr.io/devcontainers/features/docker-in-docker:2'..."} +{"type":"text","level":2,"timestamp":1744115790009,"text":"* Processing feature: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> input: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744115790009,"text":">"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> resource: ghcr.io/devcontainers/features/docker-in-docker"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> id: docker-in-docker"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> owner: devcontainers"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> namespace: devcontainers/features"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> registry: ghcr.io"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> path: devcontainers/features/docker-in-docker"} +{"type":"text","level":1,"timestamp":1744115790009,"text":">"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> version: 2"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> tag?: 2"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"> digest?: undefined"} +{"type":"text","level":1,"timestamp":1744115790009,"text":"manifest url: https://ghcr.io/v2/devcontainers/features/docker-in-docker/manifests/2"} +{"type":"text","level":1,"timestamp":1744115790290,"text":"[httpOci] Attempting to authenticate via 'Bearer' auth."} +{"type":"text","level":1,"timestamp":1744115790292,"text":"[httpOci] Invoking platform default credential helper 'osxkeychain'"} +{"type":"start","level":2,"timestamp":1744115790293,"text":"Run: docker-credential-osxkeychain get"} +{"type":"stop","level":2,"timestamp":1744115790316,"text":"Run: docker-credential-osxkeychain get","startTimestamp":1744115790293} +{"type":"text","level":1,"timestamp":1744115790316,"text":"[httpOci] Failed to query for 'ghcr.io' credential from 'docker-credential-osxkeychain': [object Object]"} +{"type":"text","level":1,"timestamp":1744115790316,"text":"[httpOci] No authentication credentials found for registry 'ghcr.io' via docker config or credential helper."} +{"type":"text","level":1,"timestamp":1744115790316,"text":"[httpOci] No authentication credentials found for registry 'ghcr.io'. Accessing anonymously."} +{"type":"text","level":1,"timestamp":1744115790316,"text":"[httpOci] Attempting to fetch bearer token from: https://ghcr.io/token?service=ghcr.io&scope=repository:devcontainers/features/docker-in-docker:pull"} +{"type":"text","level":1,"timestamp":1744115790843,"text":"[httpOci] 200 on reattempt after auth: https://ghcr.io/v2/devcontainers/features/docker-in-docker/manifests/2"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> input: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744115790845,"text":">"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> resource: ghcr.io/devcontainers/features/docker-in-docker"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> id: docker-in-docker"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> owner: devcontainers"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> namespace: devcontainers/features"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> registry: ghcr.io"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> path: devcontainers/features/docker-in-docker"} +{"type":"text","level":1,"timestamp":1744115790845,"text":">"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> version: 2"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> tag?: 2"} +{"type":"text","level":1,"timestamp":1744115790845,"text":"> digest?: undefined"} +{"type":"text","level":2,"timestamp":1744115790846,"text":"* Processing feature: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> input: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744115790846,"text":">"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> resource: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> id: common-utils"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> owner: devcontainers"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> namespace: devcontainers/features"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> registry: ghcr.io"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> path: devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744115790846,"text":">"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> version: latest"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> tag?: latest"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"> digest?: undefined"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"manifest url: https://ghcr.io/v2/devcontainers/features/common-utils/manifests/latest"} +{"type":"text","level":1,"timestamp":1744115790846,"text":"[httpOci] Applying cachedAuthHeader for registry ghcr.io..."} +{"type":"text","level":1,"timestamp":1744115791114,"text":"[httpOci] 200 (Cached): https://ghcr.io/v2/devcontainers/features/common-utils/manifests/latest"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> input: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744115791114,"text":">"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> resource: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> id: common-utils"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> owner: devcontainers"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> namespace: devcontainers/features"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> registry: ghcr.io"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> path: devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744115791114,"text":">"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> version: latest"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> tag?: latest"} +{"type":"text","level":1,"timestamp":1744115791114,"text":"> digest?: undefined"} +{"type":"text","level":1,"timestamp":1744115791115,"text":"[* resolved worklist] ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744115791115,"text":"[\n {\n \"type\": \"user-provided\",\n \"userFeatureId\": \"ghcr.io/devcontainers/features/docker-in-docker:2\",\n \"options\": {},\n \"dependsOn\": [],\n \"installsAfter\": [\n {\n \"type\": \"resolved\",\n \"userFeatureId\": \"ghcr.io/devcontainers/features/common-utils\",\n \"options\": {},\n \"featureSet\": {\n \"sourceInformation\": {\n \"type\": \"oci\",\n \"manifest\": {\n \"schemaVersion\": 2,\n \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n \"config\": {\n \"mediaType\": \"application/vnd.devcontainers\",\n \"digest\": \"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\n \"size\": 2\n },\n \"layers\": [\n {\n \"mediaType\": \"application/vnd.devcontainers.layer.v1+tar\",\n \"digest\": \"sha256:1ea70afedad2279cd746a4c0b7ac0e0fb481599303a1cbe1e57c9cb87dbe5de5\",\n \"size\": 50176,\n \"annotations\": {\n \"org.opencontainers.image.title\": \"devcontainer-feature-common-utils.tgz\"\n }\n }\n ],\n \"annotations\": {\n \"dev.containers.metadata\": \"{\\\"id\\\":\\\"common-utils\\\",\\\"version\\\":\\\"2.5.3\\\",\\\"name\\\":\\\"Common Utilities\\\",\\\"documentationURL\\\":\\\"https://github.com/devcontainers/features/tree/main/src/common-utils\\\",\\\"description\\\":\\\"Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-root user.\\\",\\\"options\\\":{\\\"installZsh\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install ZSH?\\\"},\\\"configureZshAsDefaultShell\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":false,\\\"description\\\":\\\"Change default shell to ZSH?\\\"},\\\"installOhMyZsh\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install Oh My Zsh!?\\\"},\\\"installOhMyZshConfig\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Allow installing the default dev container .zshrc templates?\\\"},\\\"upgradePackages\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Upgrade OS packages?\\\"},\\\"username\\\":{\\\"type\\\":\\\"string\\\",\\\"proposals\\\":[\\\"devcontainer\\\",\\\"vscode\\\",\\\"codespace\\\",\\\"none\\\",\\\"automatic\\\"],\\\"default\\\":\\\"automatic\\\",\\\"description\\\":\\\"Enter name of a non-root user to configure or none to skip\\\"},\\\"userUid\\\":{\\\"type\\\":\\\"string\\\",\\\"proposals\\\":[\\\"1001\\\",\\\"automatic\\\"],\\\"default\\\":\\\"automatic\\\",\\\"description\\\":\\\"Enter UID for non-root user\\\"},\\\"userGid\\\":{\\\"type\\\":\\\"string\\\",\\\"proposals\\\":[\\\"1001\\\",\\\"automatic\\\"],\\\"default\\\":\\\"automatic\\\",\\\"description\\\":\\\"Enter GID for non-root user\\\"},\\\"nonFreePackages\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":false,\\\"description\\\":\\\"Add packages from non-free Debian repository? (Debian only)\\\"}}}\",\n \"com.github.package.type\": \"devcontainer_feature\"\n }\n },\n \"manifestDigest\": \"sha256:3cf7ca93154faf9bdb128f3009cf1d1a91750ec97cc52082cf5d4edef5451f85\",\n \"featureRef\": {\n \"id\": \"common-utils\",\n \"owner\": \"devcontainers\",\n \"namespace\": \"devcontainers/features\",\n \"registry\": \"ghcr.io\",\n \"resource\": \"ghcr.io/devcontainers/features/common-utils\",\n \"path\": \"devcontainers/features/common-utils\",\n \"version\": \"latest\",\n \"tag\": \"latest\"\n },\n \"userFeatureId\": \"ghcr.io/devcontainers/features/common-utils\",\n \"userFeatureIdWithoutVersion\": \"ghcr.io/devcontainers/features/common-utils\"\n },\n \"features\": [\n {\n \"id\": \"common-utils\",\n \"included\": true,\n \"value\": {}\n }\n ]\n },\n \"dependsOn\": [],\n \"installsAfter\": [],\n \"roundPriority\": 0,\n \"featureIdAliases\": [\n \"common-utils\"\n ]\n }\n ],\n \"roundPriority\": 0,\n \"featureSet\": {\n \"sourceInformation\": {\n \"type\": \"oci\",\n \"manifest\": {\n \"schemaVersion\": 2,\n \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n \"config\": {\n \"mediaType\": \"application/vnd.devcontainers\",\n \"digest\": \"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\n \"size\": 2\n },\n \"layers\": [\n {\n \"mediaType\": \"application/vnd.devcontainers.layer.v1+tar\",\n \"digest\": \"sha256:52d59106dd0809d78a560aa2f71061a7228258364080ac745d68072064ec5a72\",\n \"size\": 40448,\n \"annotations\": {\n \"org.opencontainers.image.title\": \"devcontainer-feature-docker-in-docker.tgz\"\n }\n }\n ],\n \"annotations\": {\n \"dev.containers.metadata\": \"{\\\"id\\\":\\\"docker-in-docker\\\",\\\"version\\\":\\\"2.12.2\\\",\\\"name\\\":\\\"Docker (Docker-in-Docker)\\\",\\\"documentationURL\\\":\\\"https://github.com/devcontainers/features/tree/main/src/docker-in-docker\\\",\\\"description\\\":\\\"Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.\\\",\\\"options\\\":{\\\"version\\\":{\\\"type\\\":\\\"string\\\",\\\"proposals\\\":[\\\"latest\\\",\\\"none\\\",\\\"20.10\\\"],\\\"default\\\":\\\"latest\\\",\\\"description\\\":\\\"Select or enter a Docker/Moby Engine version. (Availability can vary by OS version.)\\\"},\\\"moby\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install OSS Moby build instead of Docker CE\\\"},\\\"mobyBuildxVersion\\\":{\\\"type\\\":\\\"string\\\",\\\"default\\\":\\\"latest\\\",\\\"description\\\":\\\"Install a specific version of moby-buildx when using Moby\\\"},\\\"dockerDashComposeVersion\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"none\\\",\\\"v1\\\",\\\"v2\\\"],\\\"default\\\":\\\"v2\\\",\\\"description\\\":\\\"Default version of Docker Compose (v1, v2 or none)\\\"},\\\"azureDnsAutoDetection\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Allow automatically setting the dockerd DNS server when the installation script detects it is running in Azure\\\"},\\\"dockerDefaultAddressPool\\\":{\\\"type\\\":\\\"string\\\",\\\"default\\\":\\\"\\\",\\\"proposals\\\":[],\\\"description\\\":\\\"Define default address pools for Docker networks. e.g. base=192.168.0.0/16,size=24\\\"},\\\"installDockerBuildx\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install Docker Buildx\\\"},\\\"installDockerComposeSwitch\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter.\\\"},\\\"disableIp6tables\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":false,\\\"description\\\":\\\"Disable ip6tables (this option is only applicable for Docker versions 27 and greater)\\\"}},\\\"entrypoint\\\":\\\"/usr/local/share/docker-init.sh\\\",\\\"privileged\\\":true,\\\"containerEnv\\\":{\\\"DOCKER_BUILDKIT\\\":\\\"1\\\"},\\\"customizations\\\":{\\\"vscode\\\":{\\\"extensions\\\":[\\\"ms-azuretools.vscode-docker\\\"],\\\"settings\\\":{\\\"github.copilot.chat.codeGeneration.instructions\\\":[{\\\"text\\\":\\\"This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container.\\\"}]}}},\\\"mounts\\\":[{\\\"source\\\":\\\"dind-var-lib-docker-${devcontainerId}\\\",\\\"target\\\":\\\"/var/lib/docker\\\",\\\"type\\\":\\\"volume\\\"}],\\\"installsAfter\\\":[\\\"ghcr.io/devcontainers/features/common-utils\\\"]}\",\n \"com.github.package.type\": \"devcontainer_feature\"\n }\n },\n \"manifestDigest\": \"sha256:842d2ed40827dc91b95ef727771e170b0e52272404f00dba063cee94eafac4bb\",\n \"featureRef\": {\n \"id\": \"docker-in-docker\",\n \"owner\": \"devcontainers\",\n \"namespace\": \"devcontainers/features\",\n \"registry\": \"ghcr.io\",\n \"resource\": \"ghcr.io/devcontainers/features/docker-in-docker\",\n \"path\": \"devcontainers/features/docker-in-docker\",\n \"version\": \"2\",\n \"tag\": \"2\"\n },\n \"userFeatureId\": \"ghcr.io/devcontainers/features/docker-in-docker:2\",\n \"userFeatureIdWithoutVersion\": \"ghcr.io/devcontainers/features/docker-in-docker\"\n },\n \"features\": [\n {\n \"id\": \"docker-in-docker\",\n \"included\": true,\n \"value\": {},\n \"version\": \"2.12.2\",\n \"name\": \"Docker (Docker-in-Docker)\",\n \"documentationURL\": \"https://github.com/devcontainers/features/tree/main/src/docker-in-docker\",\n \"description\": \"Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.\",\n \"options\": {\n \"version\": {\n \"type\": \"string\",\n \"proposals\": [\n \"latest\",\n \"none\",\n \"20.10\"\n ],\n \"default\": \"latest\",\n \"description\": \"Select or enter a Docker/Moby Engine version. (Availability can vary by OS version.)\"\n },\n \"moby\": {\n \"type\": \"boolean\",\n \"default\": true,\n \"description\": \"Install OSS Moby build instead of Docker CE\"\n },\n \"mobyBuildxVersion\": {\n \"type\": \"string\",\n \"default\": \"latest\",\n \"description\": \"Install a specific version of moby-buildx when using Moby\"\n },\n \"dockerDashComposeVersion\": {\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"v1\",\n \"v2\"\n ],\n \"default\": \"v2\",\n \"description\": \"Default version of Docker Compose (v1, v2 or none)\"\n },\n \"azureDnsAutoDetection\": {\n \"type\": \"boolean\",\n \"default\": true,\n \"description\": \"Allow automatically setting the dockerd DNS server when the installation script detects it is running in Azure\"\n },\n \"dockerDefaultAddressPool\": {\n \"type\": \"string\",\n \"default\": \"\",\n \"proposals\": [],\n \"description\": \"Define default address pools for Docker networks. e.g. base=192.168.0.0/16,size=24\"\n },\n \"installDockerBuildx\": {\n \"type\": \"boolean\",\n \"default\": true,\n \"description\": \"Install Docker Buildx\"\n },\n \"installDockerComposeSwitch\": {\n \"type\": \"boolean\",\n \"default\": true,\n \"description\": \"Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter.\"\n },\n \"disableIp6tables\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Disable ip6tables (this option is only applicable for Docker versions 27 and greater)\"\n }\n },\n \"entrypoint\": \"/usr/local/share/docker-init.sh\",\n \"privileged\": true,\n \"containerEnv\": {\n \"DOCKER_BUILDKIT\": \"1\"\n },\n \"customizations\": {\n \"vscode\": {\n \"extensions\": [\n \"ms-azuretools.vscode-docker\"\n ],\n \"settings\": {\n \"github.copilot.chat.codeGeneration.instructions\": [\n {\n \"text\": \"This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container.\"\n }\n ]\n }\n }\n },\n \"mounts\": [\n {\n \"source\": \"dind-var-lib-docker-${devcontainerId}\",\n \"target\": \"/var/lib/docker\",\n \"type\": \"volume\"\n }\n ],\n \"installsAfter\": [\n \"ghcr.io/devcontainers/features/common-utils\"\n ]\n }\n ]\n },\n \"featureIdAliases\": [\n \"docker-in-docker\"\n ]\n }\n]"} +{"type":"text","level":1,"timestamp":1744115791115,"text":"[raw worklist]: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":3,"timestamp":1744115791115,"text":"Soft-dependency 'ghcr.io/devcontainers/features/common-utils' is not required. Removing from installation order..."} +{"type":"text","level":1,"timestamp":1744115791115,"text":"[worklist-without-dangling-soft-deps]: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744115791115,"text":"Starting round-based Feature install order calculation from worklist..."} +{"type":"text","level":1,"timestamp":1744115791115,"text":"\n[round] ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744115791115,"text":"[round-candidates] ghcr.io/devcontainers/features/docker-in-docker:2 (0)"} +{"type":"text","level":1,"timestamp":1744115791115,"text":"[round-after-filter-priority] (maxPriority=0) ghcr.io/devcontainers/features/docker-in-docker:2 (0)"} +{"type":"text","level":1,"timestamp":1744115791116,"text":"[round-after-comparesTo] ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744115791116,"text":"--- Fetching User Features ----"} +{"type":"text","level":2,"timestamp":1744115791116,"text":"* Fetching feature: docker-in-docker_0_oci"} +{"type":"text","level":1,"timestamp":1744115791116,"text":"Fetching from OCI"} +{"type":"text","level":1,"timestamp":1744115791117,"text":"blob url: https://ghcr.io/v2/devcontainers/features/docker-in-docker/blobs/sha256:52d59106dd0809d78a560aa2f71061a7228258364080ac745d68072064ec5a72"} +{"type":"text","level":1,"timestamp":1744115791117,"text":"[httpOci] Applying cachedAuthHeader for registry ghcr.io..."} +{"type":"text","level":1,"timestamp":1744115791543,"text":"[httpOci] 200 (Cached): https://ghcr.io/v2/devcontainers/features/docker-in-docker/blobs/sha256:52d59106dd0809d78a560aa2f71061a7228258364080ac745d68072064ec5a72"} +{"type":"text","level":1,"timestamp":1744115791546,"text":"omitDuringExtraction: '"} +{"type":"text","level":3,"timestamp":1744115791546,"text":"Files to omit: ''"} +{"type":"text","level":1,"timestamp":1744115791551,"text":"Testing './'(Directory)"} +{"type":"text","level":1,"timestamp":1744115791553,"text":"Testing './NOTES.md'(File)"} +{"type":"text","level":1,"timestamp":1744115791554,"text":"Testing './README.md'(File)"} +{"type":"text","level":1,"timestamp":1744115791554,"text":"Testing './devcontainer-feature.json'(File)"} +{"type":"text","level":1,"timestamp":1744115791554,"text":"Testing './install.sh'(File)"} +{"type":"text","level":1,"timestamp":1744115791557,"text":"Files extracted from blob: ./NOTES.md, ./README.md, ./devcontainer-feature.json, ./install.sh"} +{"type":"text","level":2,"timestamp":1744115791559,"text":"* Fetched feature: docker-in-docker_0_oci version 2.12.2"} +{"type":"start","level":3,"timestamp":1744115791565,"text":"Run: docker buildx build --load --build-context dev_containers_feature_content_source=/var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/container-features/0.75.0-1744115790008 --build-arg _DEV_CONTAINERS_BASE_IMAGE=mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye --build-arg _DEV_CONTAINERS_IMAGE_USER=root --build-arg _DEV_CONTAINERS_FEATURE_CONTENT_SOURCE=dev_container_feature_content_temp --target dev_containers_target_stage -f /var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/container-features/0.75.0-1744115790008/Dockerfile.extended -t vsc-devcontainers-template-starter-81d8f17e32abef6d434cbb5a37fe05e5c8a6f8ccede47a61197f002dcbf60566-features /var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/empty-folder"} +{"type":"raw","level":3,"timestamp":1744115791955,"text":"#0 building with \"orbstack\" instance using docker driver\n\n#1 [internal] load build definition from Dockerfile.extended\n#1 transferring dockerfile: 3.09kB done\n#1 DONE 0.0s\n\n#2 resolve image config for docker-image://docker.io/docker/dockerfile:1.4\n"} +{"type":"raw","level":3,"timestamp":1744115793113,"text":"#2 DONE 1.3s\n"} +{"type":"raw","level":3,"timestamp":1744115793217,"text":"\n#3 docker-image://docker.io/docker/dockerfile:1.4@sha256:9ba7531bd80fb0a858632727cf7a112fbfd19b17e94c4e84ced81e24ef1a0dbc\n#3 CACHED\n\n#4 [internal] load .dockerignore\n#4 transferring context: 2B done\n#4 DONE 0.0s\n\n#5 [internal] load metadata for mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye\n#5 DONE 0.0s\n\n#6 [context dev_containers_feature_content_source] load .dockerignore\n#6 transferring dev_containers_feature_content_source: 2B done\n"} +{"type":"raw","level":3,"timestamp":1744115793217,"text":"#6 DONE 0.0s\n"} +{"type":"raw","level":3,"timestamp":1744115793307,"text":"\n#7 [dev_containers_feature_content_normalize 1/3] FROM mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye\n"} +{"type":"raw","level":3,"timestamp":1744115793307,"text":"#7 DONE 0.0s\n\n#8 [context dev_containers_feature_content_source] load from client\n#8 transferring dev_containers_feature_content_source: 46.07kB done\n#8 DONE 0.0s\n\n#9 [dev_containers_target_stage 2/5] RUN mkdir -p /tmp/dev-container-features\n#9 CACHED\n\n#10 [dev_containers_feature_content_normalize 2/3] COPY --from=dev_containers_feature_content_source devcontainer-features.builtin.env /tmp/build-features/\n#10 CACHED\n\n#11 [dev_containers_feature_content_normalize 3/3] RUN chmod -R 0755 /tmp/build-features/\n#11 CACHED\n\n#12 [dev_containers_target_stage 3/5] COPY --from=dev_containers_feature_content_normalize /tmp/build-features/ /tmp/dev-container-features\n#12 CACHED\n\n#13 [dev_containers_target_stage 4/5] RUN echo \"_CONTAINER_USER_HOME=$( (command -v getent >/dev/null 2>&1 && getent passwd 'root' || grep -E '^root|^[^:]*:[^:]*:root:' /etc/passwd || true) | cut -d: -f6)\" >> /tmp/dev-container-features/devcontainer-features.builtin.env && echo \"_REMOTE_USER_HOME=$( (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true) | cut -d: -f6)\" >> /tmp/dev-container-features/devcontainer-features.builtin.env\n#13 CACHED\n\n#14 [dev_containers_target_stage 5/5] RUN --mount=type=bind,from=dev_containers_feature_content_source,source=docker-in-docker_0,target=/tmp/build-features-src/docker-in-docker_0 cp -ar /tmp/build-features-src/docker-in-docker_0 /tmp/dev-container-features && chmod -R 0755 /tmp/dev-container-features/docker-in-docker_0 && cd /tmp/dev-container-features/docker-in-docker_0 && chmod +x ./devcontainer-features-install.sh && ./devcontainer-features-install.sh && rm -rf /tmp/dev-container-features/docker-in-docker_0\n#14 CACHED\n\n#15 exporting to image\n#15 exporting layers done\n#15 writing image sha256:275dc193c905d448ef3945e3fc86220cc315fe0cb41013988d6ff9f8d6ef2357 done\n#15 naming to docker.io/library/vsc-devcontainers-template-starter-81d8f17e32abef6d434cbb5a37fe05e5c8a6f8ccede47a61197f002dcbf60566-features done\n#15 DONE 0.0s\n"} +{"type":"stop","level":3,"timestamp":1744115793317,"text":"Run: docker buildx build --load --build-context dev_containers_feature_content_source=/var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/container-features/0.75.0-1744115790008 --build-arg _DEV_CONTAINERS_BASE_IMAGE=mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye --build-arg _DEV_CONTAINERS_IMAGE_USER=root --build-arg _DEV_CONTAINERS_FEATURE_CONTENT_SOURCE=dev_container_feature_content_temp --target dev_containers_target_stage -f /var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/container-features/0.75.0-1744115790008/Dockerfile.extended -t vsc-devcontainers-template-starter-81d8f17e32abef6d434cbb5a37fe05e5c8a6f8ccede47a61197f002dcbf60566-features /var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/empty-folder","startTimestamp":1744115791565} +{"type":"start","level":2,"timestamp":1744115793322,"text":"Run: docker events --format {{json .}} --filter event=start"} +{"type":"start","level":2,"timestamp":1744115793327,"text":"Starting container"} +{"type":"start","level":3,"timestamp":1744115793327,"text":"Run: docker run --sig-proxy=false -a STDOUT -a STDERR --mount type=bind,source=/Users/maf/Documents/Code/devcontainers-template-starter,target=/workspaces/devcontainers-template-starter,consistency=cached --mount type=volume,src=dind-var-lib-docker-0pctifo8bbg3pd06g3j5s9ae8j7lp5qfcd67m25kuahurel7v7jm,dst=/var/lib/docker -l devcontainer.local_folder=/Users/maf/Documents/Code/devcontainers-template-starter -l devcontainer.config_file=/Users/maf/Documents/Code/devcontainers-template-starter/.devcontainer/devcontainer.json --privileged --entrypoint /bin/sh vsc-devcontainers-template-starter-81d8f17e32abef6d434cbb5a37fe05e5c8a6f8ccede47a61197f002dcbf60566-features -c echo Container started"} +{"type":"raw","level":3,"timestamp":1744115793480,"text":"Container started\n"} +{"type":"stop","level":2,"timestamp":1744115793482,"text":"Starting container","startTimestamp":1744115793327} +{"type":"start","level":2,"timestamp":1744115793483,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/Users/maf/Documents/Code/devcontainers-template-starter --filter label=devcontainer.config_file=/Users/maf/Documents/Code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"raw","level":3,"timestamp":1744115793508,"text":"Not setting dockerd DNS manually.\n"} +{"type":"stop","level":2,"timestamp":1744115793508,"text":"Run: docker events --format {{json .}} --filter event=start","startTimestamp":1744115793322} +{"type":"stop","level":2,"timestamp":1744115793522,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/Users/maf/Documents/Code/devcontainers-template-starter --filter label=devcontainer.config_file=/Users/maf/Documents/Code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744115793483} +{"type":"start","level":2,"timestamp":1744115793522,"text":"Run: docker inspect --type container 2740894d889f"} +{"type":"stop","level":2,"timestamp":1744115793539,"text":"Run: docker inspect --type container 2740894d889f","startTimestamp":1744115793522} +{"type":"start","level":2,"timestamp":1744115793539,"text":"Inspecting container"} +{"type":"start","level":2,"timestamp":1744115793539,"text":"Run: docker inspect --type container 2740894d889f3937b28340a24f096ccdf446b8e3c4aa9e930cce85685b4714d5"} +{"type":"stop","level":2,"timestamp":1744115793554,"text":"Run: docker inspect --type container 2740894d889f3937b28340a24f096ccdf446b8e3c4aa9e930cce85685b4714d5","startTimestamp":1744115793539} +{"type":"stop","level":2,"timestamp":1744115793554,"text":"Inspecting container","startTimestamp":1744115793539} +{"type":"start","level":2,"timestamp":1744115793555,"text":"Run in container: /bin/sh"} +{"type":"start","level":2,"timestamp":1744115793556,"text":"Run in container: uname -m"} +{"type":"text","level":2,"timestamp":1744115793580,"text":"aarch64\n"} +{"type":"text","level":2,"timestamp":1744115793580,"text":""} +{"type":"stop","level":2,"timestamp":1744115793580,"text":"Run in container: uname -m","startTimestamp":1744115793556} +{"type":"start","level":2,"timestamp":1744115793580,"text":"Run in container: (cat /etc/os-release || cat /usr/lib/os-release) 2>/dev/null"} +{"type":"text","level":2,"timestamp":1744115793581,"text":"PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n"} +{"type":"text","level":2,"timestamp":1744115793581,"text":""} +{"type":"stop","level":2,"timestamp":1744115793581,"text":"Run in container: (cat /etc/os-release || cat /usr/lib/os-release) 2>/dev/null","startTimestamp":1744115793580} +{"type":"start","level":2,"timestamp":1744115793581,"text":"Run in container: (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true)"} +{"type":"stop","level":2,"timestamp":1744115793582,"text":"Run in container: (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true)","startTimestamp":1744115793581} +{"type":"start","level":2,"timestamp":1744115793582,"text":"Run in container: test -f '/var/devcontainer/.patchEtcEnvironmentMarker'"} +{"type":"text","level":2,"timestamp":1744115793583,"text":""} +{"type":"text","level":2,"timestamp":1744115793583,"text":""} +{"type":"text","level":2,"timestamp":1744115793583,"text":"Exit code 1"} +{"type":"stop","level":2,"timestamp":1744115793583,"text":"Run in container: test -f '/var/devcontainer/.patchEtcEnvironmentMarker'","startTimestamp":1744115793582} +{"type":"start","level":2,"timestamp":1744115793583,"text":"Run in container: /bin/sh"} +{"type":"start","level":2,"timestamp":1744115793584,"text":"Run in container: test ! -f '/var/devcontainer/.patchEtcEnvironmentMarker' && set -o noclobber && mkdir -p '/var/devcontainer' && { > '/var/devcontainer/.patchEtcEnvironmentMarker' ; } 2> /dev/null"} +{"type":"text","level":2,"timestamp":1744115793608,"text":""} +{"type":"text","level":2,"timestamp":1744115793608,"text":""} +{"type":"stop","level":2,"timestamp":1744115793608,"text":"Run in container: test ! -f '/var/devcontainer/.patchEtcEnvironmentMarker' && set -o noclobber && mkdir -p '/var/devcontainer' && { > '/var/devcontainer/.patchEtcEnvironmentMarker' ; } 2> /dev/null","startTimestamp":1744115793584} +{"type":"start","level":2,"timestamp":1744115793608,"text":"Run in container: cat >> /etc/environment <<'etcEnvrionmentEOF'"} +{"type":"text","level":2,"timestamp":1744115793609,"text":""} +{"type":"text","level":2,"timestamp":1744115793609,"text":""} +{"type":"stop","level":2,"timestamp":1744115793609,"text":"Run in container: cat >> /etc/environment <<'etcEnvrionmentEOF'","startTimestamp":1744115793608} +{"type":"start","level":2,"timestamp":1744115793609,"text":"Run in container: test -f '/var/devcontainer/.patchEtcProfileMarker'"} +{"type":"text","level":2,"timestamp":1744115793610,"text":""} +{"type":"text","level":2,"timestamp":1744115793610,"text":""} +{"type":"text","level":2,"timestamp":1744115793610,"text":"Exit code 1"} +{"type":"stop","level":2,"timestamp":1744115793610,"text":"Run in container: test -f '/var/devcontainer/.patchEtcProfileMarker'","startTimestamp":1744115793609} +{"type":"start","level":2,"timestamp":1744115793610,"text":"Run in container: test ! -f '/var/devcontainer/.patchEtcProfileMarker' && set -o noclobber && mkdir -p '/var/devcontainer' && { > '/var/devcontainer/.patchEtcProfileMarker' ; } 2> /dev/null"} +{"type":"text","level":2,"timestamp":1744115793611,"text":""} +{"type":"text","level":2,"timestamp":1744115793611,"text":""} +{"type":"stop","level":2,"timestamp":1744115793611,"text":"Run in container: test ! -f '/var/devcontainer/.patchEtcProfileMarker' && set -o noclobber && mkdir -p '/var/devcontainer' && { > '/var/devcontainer/.patchEtcProfileMarker' ; } 2> /dev/null","startTimestamp":1744115793610} +{"type":"start","level":2,"timestamp":1744115793611,"text":"Run in container: sed -i -E 's/((^|\\s)PATH=)([^\\$]*)$/\\1${PATH:-\\3}/g' /etc/profile || true"} +{"type":"text","level":2,"timestamp":1744115793612,"text":""} +{"type":"text","level":2,"timestamp":1744115793612,"text":""} +{"type":"stop","level":2,"timestamp":1744115793612,"text":"Run in container: sed -i -E 's/((^|\\s)PATH=)([^\\$]*)$/\\1${PATH:-\\3}/g' /etc/profile || true","startTimestamp":1744115793611} +{"type":"text","level":2,"timestamp":1744115793612,"text":"userEnvProbe: loginInteractiveShell (default)"} +{"type":"text","level":1,"timestamp":1744115793612,"text":"LifecycleCommandExecutionMap: {\n \"onCreateCommand\": [],\n \"updateContentCommand\": [],\n \"postCreateCommand\": [\n {\n \"origin\": \"devcontainer.json\",\n \"command\": \"npm install -g @devcontainers/cli\"\n }\n ],\n \"postStartCommand\": [],\n \"postAttachCommand\": [],\n \"initializeCommand\": []\n}"} +{"type":"text","level":2,"timestamp":1744115793612,"text":"userEnvProbe: not found in cache"} +{"type":"text","level":2,"timestamp":1744115793612,"text":"userEnvProbe shell: /bin/bash"} +{"type":"start","level":2,"timestamp":1744115793612,"text":"Run in container: /bin/bash -lic echo -n 58a6101c-d261-4fbf-a4f4-a1ed20d698e9; cat /proc/self/environ; echo -n 58a6101c-d261-4fbf-a4f4-a1ed20d698e9"} +{"type":"start","level":2,"timestamp":1744115793613,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.onCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T12:36:33.34647456Z}\" != '2025-04-08T12:36:33.34647456Z' ] && echo '2025-04-08T12:36:33.34647456Z' > '/home/node/.devcontainer/.onCreateCommandMarker'"} +{"type":"text","level":2,"timestamp":1744115793616,"text":""} +{"type":"text","level":2,"timestamp":1744115793616,"text":""} +{"type":"stop","level":2,"timestamp":1744115793616,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.onCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T12:36:33.34647456Z}\" != '2025-04-08T12:36:33.34647456Z' ] && echo '2025-04-08T12:36:33.34647456Z' > '/home/node/.devcontainer/.onCreateCommandMarker'","startTimestamp":1744115793613} +{"type":"start","level":2,"timestamp":1744115793616,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.updateContentCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T12:36:33.34647456Z}\" != '2025-04-08T12:36:33.34647456Z' ] && echo '2025-04-08T12:36:33.34647456Z' > '/home/node/.devcontainer/.updateContentCommandMarker'"} +{"type":"text","level":2,"timestamp":1744115793617,"text":""} +{"type":"text","level":2,"timestamp":1744115793617,"text":""} +{"type":"stop","level":2,"timestamp":1744115793617,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.updateContentCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T12:36:33.34647456Z}\" != '2025-04-08T12:36:33.34647456Z' ] && echo '2025-04-08T12:36:33.34647456Z' > '/home/node/.devcontainer/.updateContentCommandMarker'","startTimestamp":1744115793616} +{"type":"start","level":2,"timestamp":1744115793617,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T12:36:33.34647456Z}\" != '2025-04-08T12:36:33.34647456Z' ] && echo '2025-04-08T12:36:33.34647456Z' > '/home/node/.devcontainer/.postCreateCommandMarker'"} +{"type":"text","level":2,"timestamp":1744115793618,"text":""} +{"type":"text","level":2,"timestamp":1744115793618,"text":""} +{"type":"stop","level":2,"timestamp":1744115793618,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T12:36:33.34647456Z}\" != '2025-04-08T12:36:33.34647456Z' ] && echo '2025-04-08T12:36:33.34647456Z' > '/home/node/.devcontainer/.postCreateCommandMarker'","startTimestamp":1744115793617} +{"type":"raw","level":3,"timestamp":1744115793619,"text":"\u001b[1mRunning the postCreateCommand from devcontainer.json...\u001b[0m\r\n\r\n","channel":"postCreate"} +{"type":"progress","name":"Running postCreateCommand...","status":"running","stepDetail":"npm install -g @devcontainers/cli","channel":"postCreate"} +{"type":"stop","level":2,"timestamp":1744115793669,"text":"Run in container: /bin/bash -lic echo -n 58a6101c-d261-4fbf-a4f4-a1ed20d698e9; cat /proc/self/environ; echo -n 58a6101c-d261-4fbf-a4f4-a1ed20d698e9","startTimestamp":1744115793612} +{"type":"text","level":1,"timestamp":1744115793669,"text":"58a6101c-d261-4fbf-a4f4-a1ed20d698e9NVM_RC_VERSION=\u0000HOSTNAME=2740894d889f\u0000YARN_VERSION=1.22.22\u0000PWD=/\u0000HOME=/home/node\u0000LS_COLORS=\u0000NVM_SYMLINK_CURRENT=true\u0000DOCKER_BUILDKIT=1\u0000NVM_DIR=/usr/local/share/nvm\u0000USER=node\u0000SHLVL=1\u0000NVM_CD_FLAGS=\u0000PROMPT_DIRTRIM=4\u0000PATH=/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/node/.local/bin\u0000NODE_VERSION=18.20.8\u0000_=/bin/cat\u000058a6101c-d261-4fbf-a4f4-a1ed20d698e9"} +{"type":"text","level":1,"timestamp":1744115793670,"text":"\u001b[1m\u001b[31mbash: cannot set terminal process group (-1): Inappropriate ioctl for device\u001b[39m\u001b[22m\r\n\u001b[1m\u001b[31mbash: no job control in this shell\u001b[39m\u001b[22m\r\n\u001b[1m\u001b[31m\u001b[39m\u001b[22m\r\n"} +{"type":"text","level":1,"timestamp":1744115793670,"text":"userEnvProbe parsed: {\n \"NVM_RC_VERSION\": \"\",\n \"HOSTNAME\": \"2740894d889f\",\n \"YARN_VERSION\": \"1.22.22\",\n \"PWD\": \"/\",\n \"HOME\": \"/home/node\",\n \"LS_COLORS\": \"\",\n \"NVM_SYMLINK_CURRENT\": \"true\",\n \"DOCKER_BUILDKIT\": \"1\",\n \"NVM_DIR\": \"/usr/local/share/nvm\",\n \"USER\": \"node\",\n \"SHLVL\": \"1\",\n \"NVM_CD_FLAGS\": \"\",\n \"PROMPT_DIRTRIM\": \"4\",\n \"PATH\": \"/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/node/.local/bin\",\n \"NODE_VERSION\": \"18.20.8\",\n \"_\": \"/bin/cat\"\n}"} +{"type":"text","level":2,"timestamp":1744115793670,"text":"userEnvProbe PATHs:\nProbe: '/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/node/.local/bin'\nContainer: '/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'"} +{"type":"start","level":2,"timestamp":1744115793672,"text":"Run in container: /bin/sh -c npm install -g @devcontainers/cli","channel":"postCreate"} +{"type":"raw","level":3,"timestamp":1744115794568,"text":"\nadded 1 package in 806ms\n","channel":"postCreate"} +{"type":"stop","level":2,"timestamp":1744115794579,"text":"Run in container: /bin/sh -c npm install -g @devcontainers/cli","startTimestamp":1744115793672,"channel":"postCreate"} +{"type":"progress","name":"Running postCreateCommand...","status":"succeeded","channel":"postCreate"} +{"type":"start","level":2,"timestamp":1744115794579,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postStartCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T12:36:33.400704421Z}\" != '2025-04-08T12:36:33.400704421Z' ] && echo '2025-04-08T12:36:33.400704421Z' > '/home/node/.devcontainer/.postStartCommandMarker'"} +{"type":"text","level":2,"timestamp":1744115794581,"text":""} +{"type":"text","level":2,"timestamp":1744115794581,"text":""} +{"type":"stop","level":2,"timestamp":1744115794581,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postStartCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T12:36:33.400704421Z}\" != '2025-04-08T12:36:33.400704421Z' ] && echo '2025-04-08T12:36:33.400704421Z' > '/home/node/.devcontainer/.postStartCommandMarker'","startTimestamp":1744115794579} +{"type":"stop","level":2,"timestamp":1744115794582,"text":"Resolving Remote","startTimestamp":1744115789470} +{"outcome":"success","containerId":"2740894d889f3937b28340a24f096ccdf446b8e3c4aa9e930cce85685b4714d5","remoteUser":"node","remoteWorkspaceFolder":"/workspaces/devcontainers-template-starter"} diff --git a/agent/agentcontainers/testdata/devcontainercli/parse/up.log b/agent/agentcontainers/testdata/devcontainercli/parse/up.log new file mode 100644 index 0000000000000..ef4c43aa7b6b5 --- /dev/null +++ b/agent/agentcontainers/testdata/devcontainercli/parse/up.log @@ -0,0 +1,206 @@ +{"type":"text","level":3,"timestamp":1744102171070,"text":"@devcontainers/cli 0.75.0. Node.js v23.9.0. darwin 24.4.0 arm64."} +{"type":"start","level":2,"timestamp":1744102171070,"text":"Run: docker buildx version"} +{"type":"stop","level":2,"timestamp":1744102171115,"text":"Run: docker buildx version","startTimestamp":1744102171070} +{"type":"text","level":2,"timestamp":1744102171115,"text":"github.com/docker/buildx v0.21.2 1360a9e8d25a2c3d03c2776d53ae62e6ff0a843d\r\n"} +{"type":"text","level":2,"timestamp":1744102171115,"text":"\u001b[1m\u001b[31m\u001b[39m\u001b[22m\r\n"} +{"type":"start","level":2,"timestamp":1744102171115,"text":"Run: docker -v"} +{"type":"stop","level":2,"timestamp":1744102171125,"text":"Run: docker -v","startTimestamp":1744102171115} +{"type":"start","level":2,"timestamp":1744102171125,"text":"Resolving Remote"} +{"type":"start","level":2,"timestamp":1744102171127,"text":"Run: git rev-parse --show-cdup"} +{"type":"stop","level":2,"timestamp":1744102171131,"text":"Run: git rev-parse --show-cdup","startTimestamp":1744102171127} +{"type":"start","level":2,"timestamp":1744102171132,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"stop","level":2,"timestamp":1744102171149,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744102171132} +{"type":"start","level":2,"timestamp":1744102171149,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter"} +{"type":"stop","level":2,"timestamp":1744102171162,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter","startTimestamp":1744102171149} +{"type":"start","level":2,"timestamp":1744102171163,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"stop","level":2,"timestamp":1744102171177,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744102171163} +{"type":"start","level":2,"timestamp":1744102171177,"text":"Run: docker inspect --type image mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye"} +{"type":"stop","level":2,"timestamp":1744102171193,"text":"Run: docker inspect --type image mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye","startTimestamp":1744102171177} +{"type":"text","level":1,"timestamp":1744102171193,"text":"workspace root: /code/devcontainers-template-starter"} +{"type":"text","level":1,"timestamp":1744102171193,"text":"configPath: /code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"--- Processing User Features ----"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"[* user-provided] ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":3,"timestamp":1744102171194,"text":"Resolving Feature dependencies for 'ghcr.io/devcontainers/features/docker-in-docker:2'..."} +{"type":"text","level":2,"timestamp":1744102171194,"text":"* Processing feature: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> input: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744102171194,"text":">"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> resource: ghcr.io/devcontainers/features/docker-in-docker"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> id: docker-in-docker"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> owner: devcontainers"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> namespace: devcontainers/features"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> registry: ghcr.io"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> path: devcontainers/features/docker-in-docker"} +{"type":"text","level":1,"timestamp":1744102171194,"text":">"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> version: 2"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> tag?: 2"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"> digest?: undefined"} +{"type":"text","level":1,"timestamp":1744102171194,"text":"manifest url: https://ghcr.io/v2/devcontainers/features/docker-in-docker/manifests/2"} +{"type":"text","level":1,"timestamp":1744102171519,"text":"[httpOci] Attempting to authenticate via 'Bearer' auth."} +{"type":"text","level":1,"timestamp":1744102171521,"text":"[httpOci] Invoking platform default credential helper 'osxkeychain'"} +{"type":"start","level":2,"timestamp":1744102171521,"text":"Run: docker-credential-osxkeychain get"} +{"type":"stop","level":2,"timestamp":1744102171564,"text":"Run: docker-credential-osxkeychain get","startTimestamp":1744102171521} +{"type":"text","level":1,"timestamp":1744102171564,"text":"[httpOci] Failed to query for 'ghcr.io' credential from 'docker-credential-osxkeychain': [object Object]"} +{"type":"text","level":1,"timestamp":1744102171564,"text":"[httpOci] No authentication credentials found for registry 'ghcr.io' via docker config or credential helper."} +{"type":"text","level":1,"timestamp":1744102171564,"text":"[httpOci] No authentication credentials found for registry 'ghcr.io'. Accessing anonymously."} +{"type":"text","level":1,"timestamp":1744102171564,"text":"[httpOci] Attempting to fetch bearer token from: https://ghcr.io/token?service=ghcr.io&scope=repository:devcontainers/features/docker-in-docker:pull"} +{"type":"text","level":1,"timestamp":1744102172039,"text":"[httpOci] 200 on reattempt after auth: https://ghcr.io/v2/devcontainers/features/docker-in-docker/manifests/2"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> input: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744102172040,"text":">"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> resource: ghcr.io/devcontainers/features/docker-in-docker"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> id: docker-in-docker"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> owner: devcontainers"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> namespace: devcontainers/features"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> registry: ghcr.io"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> path: devcontainers/features/docker-in-docker"} +{"type":"text","level":1,"timestamp":1744102172040,"text":">"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> version: 2"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> tag?: 2"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> digest?: undefined"} +{"type":"text","level":2,"timestamp":1744102172040,"text":"* Processing feature: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744102172040,"text":"> input: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744102172041,"text":">"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"> resource: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"> id: common-utils"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"> owner: devcontainers"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"> namespace: devcontainers/features"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"> registry: ghcr.io"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"> path: devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744102172041,"text":">"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"> version: latest"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"> tag?: latest"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"> digest?: undefined"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"manifest url: https://ghcr.io/v2/devcontainers/features/common-utils/manifests/latest"} +{"type":"text","level":1,"timestamp":1744102172041,"text":"[httpOci] Applying cachedAuthHeader for registry ghcr.io..."} +{"type":"text","level":1,"timestamp":1744102172294,"text":"[httpOci] 200 (Cached): https://ghcr.io/v2/devcontainers/features/common-utils/manifests/latest"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> input: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744102172294,"text":">"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> resource: ghcr.io/devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> id: common-utils"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> owner: devcontainers"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> namespace: devcontainers/features"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> registry: ghcr.io"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> path: devcontainers/features/common-utils"} +{"type":"text","level":1,"timestamp":1744102172294,"text":">"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> version: latest"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> tag?: latest"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"> digest?: undefined"} +{"type":"text","level":1,"timestamp":1744102172294,"text":"[* resolved worklist] ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744102172295,"text":"[\n {\n \"type\": \"user-provided\",\n \"userFeatureId\": \"ghcr.io/devcontainers/features/docker-in-docker:2\",\n \"options\": {},\n \"dependsOn\": [],\n \"installsAfter\": [\n {\n \"type\": \"resolved\",\n \"userFeatureId\": \"ghcr.io/devcontainers/features/common-utils\",\n \"options\": {},\n \"featureSet\": {\n \"sourceInformation\": {\n \"type\": \"oci\",\n \"manifest\": {\n \"schemaVersion\": 2,\n \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n \"config\": {\n \"mediaType\": \"application/vnd.devcontainers\",\n \"digest\": \"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\n \"size\": 2\n },\n \"layers\": [\n {\n \"mediaType\": \"application/vnd.devcontainers.layer.v1+tar\",\n \"digest\": \"sha256:1ea70afedad2279cd746a4c0b7ac0e0fb481599303a1cbe1e57c9cb87dbe5de5\",\n \"size\": 50176,\n \"annotations\": {\n \"org.opencontainers.image.title\": \"devcontainer-feature-common-utils.tgz\"\n }\n }\n ],\n \"annotations\": {\n \"dev.containers.metadata\": \"{\\\"id\\\":\\\"common-utils\\\",\\\"version\\\":\\\"2.5.3\\\",\\\"name\\\":\\\"Common Utilities\\\",\\\"documentationURL\\\":\\\"https://github.com/devcontainers/features/tree/main/src/common-utils\\\",\\\"description\\\":\\\"Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-root user.\\\",\\\"options\\\":{\\\"installZsh\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install ZSH?\\\"},\\\"configureZshAsDefaultShell\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":false,\\\"description\\\":\\\"Change default shell to ZSH?\\\"},\\\"installOhMyZsh\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install Oh My Zsh!?\\\"},\\\"installOhMyZshConfig\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Allow installing the default dev container .zshrc templates?\\\"},\\\"upgradePackages\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Upgrade OS packages?\\\"},\\\"username\\\":{\\\"type\\\":\\\"string\\\",\\\"proposals\\\":[\\\"devcontainer\\\",\\\"vscode\\\",\\\"codespace\\\",\\\"none\\\",\\\"automatic\\\"],\\\"default\\\":\\\"automatic\\\",\\\"description\\\":\\\"Enter name of a non-root user to configure or none to skip\\\"},\\\"userUid\\\":{\\\"type\\\":\\\"string\\\",\\\"proposals\\\":[\\\"1001\\\",\\\"automatic\\\"],\\\"default\\\":\\\"automatic\\\",\\\"description\\\":\\\"Enter UID for non-root user\\\"},\\\"userGid\\\":{\\\"type\\\":\\\"string\\\",\\\"proposals\\\":[\\\"1001\\\",\\\"automatic\\\"],\\\"default\\\":\\\"automatic\\\",\\\"description\\\":\\\"Enter GID for non-root user\\\"},\\\"nonFreePackages\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":false,\\\"description\\\":\\\"Add packages from non-free Debian repository? (Debian only)\\\"}}}\",\n \"com.github.package.type\": \"devcontainer_feature\"\n }\n },\n \"manifestDigest\": \"sha256:3cf7ca93154faf9bdb128f3009cf1d1a91750ec97cc52082cf5d4edef5451f85\",\n \"featureRef\": {\n \"id\": \"common-utils\",\n \"owner\": \"devcontainers\",\n \"namespace\": \"devcontainers/features\",\n \"registry\": \"ghcr.io\",\n \"resource\": \"ghcr.io/devcontainers/features/common-utils\",\n \"path\": \"devcontainers/features/common-utils\",\n \"version\": \"latest\",\n \"tag\": \"latest\"\n },\n \"userFeatureId\": \"ghcr.io/devcontainers/features/common-utils\",\n \"userFeatureIdWithoutVersion\": \"ghcr.io/devcontainers/features/common-utils\"\n },\n \"features\": [\n {\n \"id\": \"common-utils\",\n \"included\": true,\n \"value\": {}\n }\n ]\n },\n \"dependsOn\": [],\n \"installsAfter\": [],\n \"roundPriority\": 0,\n \"featureIdAliases\": [\n \"common-utils\"\n ]\n }\n ],\n \"roundPriority\": 0,\n \"featureSet\": {\n \"sourceInformation\": {\n \"type\": \"oci\",\n \"manifest\": {\n \"schemaVersion\": 2,\n \"mediaType\": \"application/vnd.oci.image.manifest.v1+json\",\n \"config\": {\n \"mediaType\": \"application/vnd.devcontainers\",\n \"digest\": \"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a\",\n \"size\": 2\n },\n \"layers\": [\n {\n \"mediaType\": \"application/vnd.devcontainers.layer.v1+tar\",\n \"digest\": \"sha256:52d59106dd0809d78a560aa2f71061a7228258364080ac745d68072064ec5a72\",\n \"size\": 40448,\n \"annotations\": {\n \"org.opencontainers.image.title\": \"devcontainer-feature-docker-in-docker.tgz\"\n }\n }\n ],\n \"annotations\": {\n \"dev.containers.metadata\": \"{\\\"id\\\":\\\"docker-in-docker\\\",\\\"version\\\":\\\"2.12.2\\\",\\\"name\\\":\\\"Docker (Docker-in-Docker)\\\",\\\"documentationURL\\\":\\\"https://github.com/devcontainers/features/tree/main/src/docker-in-docker\\\",\\\"description\\\":\\\"Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.\\\",\\\"options\\\":{\\\"version\\\":{\\\"type\\\":\\\"string\\\",\\\"proposals\\\":[\\\"latest\\\",\\\"none\\\",\\\"20.10\\\"],\\\"default\\\":\\\"latest\\\",\\\"description\\\":\\\"Select or enter a Docker/Moby Engine version. (Availability can vary by OS version.)\\\"},\\\"moby\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install OSS Moby build instead of Docker CE\\\"},\\\"mobyBuildxVersion\\\":{\\\"type\\\":\\\"string\\\",\\\"default\\\":\\\"latest\\\",\\\"description\\\":\\\"Install a specific version of moby-buildx when using Moby\\\"},\\\"dockerDashComposeVersion\\\":{\\\"type\\\":\\\"string\\\",\\\"enum\\\":[\\\"none\\\",\\\"v1\\\",\\\"v2\\\"],\\\"default\\\":\\\"v2\\\",\\\"description\\\":\\\"Default version of Docker Compose (v1, v2 or none)\\\"},\\\"azureDnsAutoDetection\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Allow automatically setting the dockerd DNS server when the installation script detects it is running in Azure\\\"},\\\"dockerDefaultAddressPool\\\":{\\\"type\\\":\\\"string\\\",\\\"default\\\":\\\"\\\",\\\"proposals\\\":[],\\\"description\\\":\\\"Define default address pools for Docker networks. e.g. base=192.168.0.0/16,size=24\\\"},\\\"installDockerBuildx\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install Docker Buildx\\\"},\\\"installDockerComposeSwitch\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":true,\\\"description\\\":\\\"Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter.\\\"},\\\"disableIp6tables\\\":{\\\"type\\\":\\\"boolean\\\",\\\"default\\\":false,\\\"description\\\":\\\"Disable ip6tables (this option is only applicable for Docker versions 27 and greater)\\\"}},\\\"entrypoint\\\":\\\"/usr/local/share/docker-init.sh\\\",\\\"privileged\\\":true,\\\"containerEnv\\\":{\\\"DOCKER_BUILDKIT\\\":\\\"1\\\"},\\\"customizations\\\":{\\\"vscode\\\":{\\\"extensions\\\":[\\\"ms-azuretools.vscode-docker\\\"],\\\"settings\\\":{\\\"github.copilot.chat.codeGeneration.instructions\\\":[{\\\"text\\\":\\\"This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container.\\\"}]}}},\\\"mounts\\\":[{\\\"source\\\":\\\"dind-var-lib-docker-${devcontainerId}\\\",\\\"target\\\":\\\"/var/lib/docker\\\",\\\"type\\\":\\\"volume\\\"}],\\\"installsAfter\\\":[\\\"ghcr.io/devcontainers/features/common-utils\\\"]}\",\n \"com.github.package.type\": \"devcontainer_feature\"\n }\n },\n \"manifestDigest\": \"sha256:842d2ed40827dc91b95ef727771e170b0e52272404f00dba063cee94eafac4bb\",\n \"featureRef\": {\n \"id\": \"docker-in-docker\",\n \"owner\": \"devcontainers\",\n \"namespace\": \"devcontainers/features\",\n \"registry\": \"ghcr.io\",\n \"resource\": \"ghcr.io/devcontainers/features/docker-in-docker\",\n \"path\": \"devcontainers/features/docker-in-docker\",\n \"version\": \"2\",\n \"tag\": \"2\"\n },\n \"userFeatureId\": \"ghcr.io/devcontainers/features/docker-in-docker:2\",\n \"userFeatureIdWithoutVersion\": \"ghcr.io/devcontainers/features/docker-in-docker\"\n },\n \"features\": [\n {\n \"id\": \"docker-in-docker\",\n \"included\": true,\n \"value\": {},\n \"version\": \"2.12.2\",\n \"name\": \"Docker (Docker-in-Docker)\",\n \"documentationURL\": \"https://github.com/devcontainers/features/tree/main/src/docker-in-docker\",\n \"description\": \"Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.\",\n \"options\": {\n \"version\": {\n \"type\": \"string\",\n \"proposals\": [\n \"latest\",\n \"none\",\n \"20.10\"\n ],\n \"default\": \"latest\",\n \"description\": \"Select or enter a Docker/Moby Engine version. (Availability can vary by OS version.)\"\n },\n \"moby\": {\n \"type\": \"boolean\",\n \"default\": true,\n \"description\": \"Install OSS Moby build instead of Docker CE\"\n },\n \"mobyBuildxVersion\": {\n \"type\": \"string\",\n \"default\": \"latest\",\n \"description\": \"Install a specific version of moby-buildx when using Moby\"\n },\n \"dockerDashComposeVersion\": {\n \"type\": \"string\",\n \"enum\": [\n \"none\",\n \"v1\",\n \"v2\"\n ],\n \"default\": \"v2\",\n \"description\": \"Default version of Docker Compose (v1, v2 or none)\"\n },\n \"azureDnsAutoDetection\": {\n \"type\": \"boolean\",\n \"default\": true,\n \"description\": \"Allow automatically setting the dockerd DNS server when the installation script detects it is running in Azure\"\n },\n \"dockerDefaultAddressPool\": {\n \"type\": \"string\",\n \"default\": \"\",\n \"proposals\": [],\n \"description\": \"Define default address pools for Docker networks. e.g. base=192.168.0.0/16,size=24\"\n },\n \"installDockerBuildx\": {\n \"type\": \"boolean\",\n \"default\": true,\n \"description\": \"Install Docker Buildx\"\n },\n \"installDockerComposeSwitch\": {\n \"type\": \"boolean\",\n \"default\": true,\n \"description\": \"Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter.\"\n },\n \"disableIp6tables\": {\n \"type\": \"boolean\",\n \"default\": false,\n \"description\": \"Disable ip6tables (this option is only applicable for Docker versions 27 and greater)\"\n }\n },\n \"entrypoint\": \"/usr/local/share/docker-init.sh\",\n \"privileged\": true,\n \"containerEnv\": {\n \"DOCKER_BUILDKIT\": \"1\"\n },\n \"customizations\": {\n \"vscode\": {\n \"extensions\": [\n \"ms-azuretools.vscode-docker\"\n ],\n \"settings\": {\n \"github.copilot.chat.codeGeneration.instructions\": [\n {\n \"text\": \"This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container.\"\n }\n ]\n }\n }\n },\n \"mounts\": [\n {\n \"source\": \"dind-var-lib-docker-${devcontainerId}\",\n \"target\": \"/var/lib/docker\",\n \"type\": \"volume\"\n }\n ],\n \"installsAfter\": [\n \"ghcr.io/devcontainers/features/common-utils\"\n ]\n }\n ]\n },\n \"featureIdAliases\": [\n \"docker-in-docker\"\n ]\n }\n]"} +{"type":"text","level":1,"timestamp":1744102172295,"text":"[raw worklist]: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":3,"timestamp":1744102172295,"text":"Soft-dependency 'ghcr.io/devcontainers/features/common-utils' is not required. Removing from installation order..."} +{"type":"text","level":1,"timestamp":1744102172295,"text":"[worklist-without-dangling-soft-deps]: ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744102172295,"text":"Starting round-based Feature install order calculation from worklist..."} +{"type":"text","level":1,"timestamp":1744102172295,"text":"\n[round] ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744102172295,"text":"[round-candidates] ghcr.io/devcontainers/features/docker-in-docker:2 (0)"} +{"type":"text","level":1,"timestamp":1744102172295,"text":"[round-after-filter-priority] (maxPriority=0) ghcr.io/devcontainers/features/docker-in-docker:2 (0)"} +{"type":"text","level":1,"timestamp":1744102172295,"text":"[round-after-comparesTo] ghcr.io/devcontainers/features/docker-in-docker:2"} +{"type":"text","level":1,"timestamp":1744102172295,"text":"--- Fetching User Features ----"} +{"type":"text","level":2,"timestamp":1744102172295,"text":"* Fetching feature: docker-in-docker_0_oci"} +{"type":"text","level":1,"timestamp":1744102172295,"text":"Fetching from OCI"} +{"type":"text","level":1,"timestamp":1744102172296,"text":"blob url: https://ghcr.io/v2/devcontainers/features/docker-in-docker/blobs/sha256:52d59106dd0809d78a560aa2f71061a7228258364080ac745d68072064ec5a72"} +{"type":"text","level":1,"timestamp":1744102172296,"text":"[httpOci] Applying cachedAuthHeader for registry ghcr.io..."} +{"type":"text","level":1,"timestamp":1744102172575,"text":"[httpOci] 200 (Cached): https://ghcr.io/v2/devcontainers/features/docker-in-docker/blobs/sha256:52d59106dd0809d78a560aa2f71061a7228258364080ac745d68072064ec5a72"} +{"type":"text","level":1,"timestamp":1744102172576,"text":"omitDuringExtraction: '"} +{"type":"text","level":3,"timestamp":1744102172576,"text":"Files to omit: ''"} +{"type":"text","level":1,"timestamp":1744102172579,"text":"Testing './'(Directory)"} +{"type":"text","level":1,"timestamp":1744102172581,"text":"Testing './NOTES.md'(File)"} +{"type":"text","level":1,"timestamp":1744102172581,"text":"Testing './README.md'(File)"} +{"type":"text","level":1,"timestamp":1744102172581,"text":"Testing './devcontainer-feature.json'(File)"} +{"type":"text","level":1,"timestamp":1744102172581,"text":"Testing './install.sh'(File)"} +{"type":"text","level":1,"timestamp":1744102172583,"text":"Files extracted from blob: ./NOTES.md, ./README.md, ./devcontainer-feature.json, ./install.sh"} +{"type":"text","level":2,"timestamp":1744102172583,"text":"* Fetched feature: docker-in-docker_0_oci version 2.12.2"} +{"type":"start","level":3,"timestamp":1744102172588,"text":"Run: docker buildx build --load --build-context dev_containers_feature_content_source=/var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/container-features/0.75.0-1744102171193 --build-arg _DEV_CONTAINERS_BASE_IMAGE=mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye --build-arg _DEV_CONTAINERS_IMAGE_USER=root --build-arg _DEV_CONTAINERS_FEATURE_CONTENT_SOURCE=dev_container_feature_content_temp --target dev_containers_target_stage -f /var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/container-features/0.75.0-1744102171193/Dockerfile.extended -t vsc-devcontainers-template-starter-81d8f17e32abef6d434cbb5a37fe05e5c8a6f8ccede47a61197f002dcbf60566-features /var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/empty-folder"} +{"type":"raw","level":3,"timestamp":1744102172928,"text":"#0 building with \"orbstack\" instance using docker driver\n\n#1 [internal] load build definition from Dockerfile.extended\n"} +{"type":"raw","level":3,"timestamp":1744102172928,"text":"#1 transferring dockerfile: 3.09kB done\n#1 DONE 0.0s\n\n#2 resolve image config for docker-image://docker.io/docker/dockerfile:1.4\n"} +{"type":"raw","level":3,"timestamp":1744102174031,"text":"#2 DONE 1.3s\n"} +{"type":"raw","level":3,"timestamp":1744102174136,"text":"\n#3 docker-image://docker.io/docker/dockerfile:1.4@sha256:9ba7531bd80fb0a858632727cf7a112fbfd19b17e94c4e84ced81e24ef1a0dbc\n#3 CACHED\n"} +{"type":"raw","level":3,"timestamp":1744102174243,"text":"\n"} +{"type":"raw","level":3,"timestamp":1744102174243,"text":"#4 [internal] load .dockerignore\n#4 transferring context: 2B done\n#4 DONE 0.0s\n\n#5 [internal] load metadata for mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye\n#5 DONE 0.0s\n\n#6 [context dev_containers_feature_content_source] load .dockerignore\n#6 transferring dev_containers_feature_content_source: 2B done\n#6 DONE 0.0s\n\n#7 [dev_containers_feature_content_normalize 1/3] FROM mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye\n#7 DONE 0.0s\n\n#8 [context dev_containers_feature_content_source] load from client\n#8 transferring dev_containers_feature_content_source: 82.11kB 0.0s done\n#8 DONE 0.0s\n\n#9 [dev_containers_feature_content_normalize 2/3] COPY --from=dev_containers_feature_content_source devcontainer-features.builtin.env /tmp/build-features/\n#9 CACHED\n\n#10 [dev_containers_target_stage 2/5] RUN mkdir -p /tmp/dev-container-features\n#10 CACHED\n\n#11 [dev_containers_target_stage 3/5] COPY --from=dev_containers_feature_content_normalize /tmp/build-features/ /tmp/dev-container-features\n#11 CACHED\n\n#12 [dev_containers_target_stage 4/5] RUN echo \"_CONTAINER_USER_HOME=$( (command -v getent >/dev/null 2>&1 && getent passwd 'root' || grep -E '^root|^[^:]*:[^:]*:root:' /etc/passwd || true) | cut -d: -f6)\" >> /tmp/dev-container-features/devcontainer-features.builtin.env && echo \"_REMOTE_USER_HOME=$( (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true) | cut -d: -f6)\" >> /tmp/dev-container-features/devcontainer-features.builtin.env\n#12 CACHED\n\n#13 [dev_containers_feature_content_normalize 3/3] RUN chmod -R 0755 /tmp/build-features/\n#13 CACHED\n\n#14 [dev_containers_target_stage 5/5] RUN --mount=type=bind,from=dev_containers_feature_content_source,source=docker-in-docker_0,target=/tmp/build-features-src/docker-in-docker_0 cp -ar /tmp/build-features-src/docker-in-docker_0 /tmp/dev-container-features && chmod -R 0755 /tmp/dev-container-features/docker-in-docker_0 && cd /tmp/dev-container-features/docker-in-docker_0 && chmod +x ./devcontainer-features-install.sh && ./devcontainer-features-install.sh && rm -rf /tmp/dev-container-features/docker-in-docker_0\n#14 CACHED\n\n#15 exporting to image\n#15 exporting layers done\n#15 writing image sha256:275dc193c905d448ef3945e3fc86220cc315fe0cb41013988d6ff9f8d6ef2357 done\n#15 naming to docker.io/library/vsc-devcontainers-template-starter-81d8f17e32abef6d434cbb5a37fe05e5c8a6f8ccede47a61197f002dcbf60566-features done\n#15 DONE 0.0s\n"} +{"type":"stop","level":3,"timestamp":1744102174254,"text":"Run: docker buildx build --load --build-context dev_containers_feature_content_source=/var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/container-features/0.75.0-1744102171193 --build-arg _DEV_CONTAINERS_BASE_IMAGE=mcr.microsoft.com/devcontainers/javascript-node:1-18-bullseye --build-arg _DEV_CONTAINERS_IMAGE_USER=root --build-arg _DEV_CONTAINERS_FEATURE_CONTENT_SOURCE=dev_container_feature_content_temp --target dev_containers_target_stage -f /var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/container-features/0.75.0-1744102171193/Dockerfile.extended -t vsc-devcontainers-template-starter-81d8f17e32abef6d434cbb5a37fe05e5c8a6f8ccede47a61197f002dcbf60566-features /var/folders/1y/cm8mblxd7_x9cljwl_jvfprh0000gn/T/devcontainercli/empty-folder","startTimestamp":1744102172588} +{"type":"start","level":2,"timestamp":1744102174259,"text":"Run: docker events --format {{json .}} --filter event=start"} +{"type":"start","level":2,"timestamp":1744102174262,"text":"Starting container"} +{"type":"start","level":3,"timestamp":1744102174263,"text":"Run: docker run --sig-proxy=false -a STDOUT -a STDERR --mount type=bind,source=/code/devcontainers-template-starter,target=/workspaces/devcontainers-template-starter,consistency=cached --mount type=volume,src=dind-var-lib-docker-0pctifo8bbg3pd06g3j5s9ae8j7lp5qfcd67m25kuahurel7v7jm,dst=/var/lib/docker -l devcontainer.local_folder=/code/devcontainers-template-starter -l devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json --privileged --entrypoint /bin/sh vsc-devcontainers-template-starter-81d8f17e32abef6d434cbb5a37fe05e5c8a6f8ccede47a61197f002dcbf60566-features -c echo Container started"} +{"type":"raw","level":3,"timestamp":1744102174400,"text":"Container started\n"} +{"type":"stop","level":2,"timestamp":1744102174402,"text":"Starting container","startTimestamp":1744102174262} +{"type":"start","level":2,"timestamp":1744102174402,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json"} +{"type":"stop","level":2,"timestamp":1744102174405,"text":"Run: docker events --format {{json .}} --filter event=start","startTimestamp":1744102174259} +{"type":"raw","level":3,"timestamp":1744102174407,"text":"Not setting dockerd DNS manually.\n"} +{"type":"stop","level":2,"timestamp":1744102174457,"text":"Run: docker ps -q -a --filter label=devcontainer.local_folder=/code/devcontainers-template-starter --filter label=devcontainer.config_file=/code/devcontainers-template-starter/.devcontainer/devcontainer.json","startTimestamp":1744102174402} +{"type":"start","level":2,"timestamp":1744102174457,"text":"Run: docker inspect --type container bc72db8d0c4c"} +{"type":"stop","level":2,"timestamp":1744102174473,"text":"Run: docker inspect --type container bc72db8d0c4c","startTimestamp":1744102174457} +{"type":"start","level":2,"timestamp":1744102174473,"text":"Inspecting container"} +{"type":"start","level":2,"timestamp":1744102174473,"text":"Run: docker inspect --type container bc72db8d0c4c4e941bd9ffc341aee64a18d3397fd45b87cd93d4746150967ba8"} +{"type":"stop","level":2,"timestamp":1744102174487,"text":"Run: docker inspect --type container bc72db8d0c4c4e941bd9ffc341aee64a18d3397fd45b87cd93d4746150967ba8","startTimestamp":1744102174473} +{"type":"stop","level":2,"timestamp":1744102174487,"text":"Inspecting container","startTimestamp":1744102174473} +{"type":"start","level":2,"timestamp":1744102174488,"text":"Run in container: /bin/sh"} +{"type":"start","level":2,"timestamp":1744102174489,"text":"Run in container: uname -m"} +{"type":"text","level":2,"timestamp":1744102174514,"text":"aarch64\n"} +{"type":"text","level":2,"timestamp":1744102174514,"text":""} +{"type":"stop","level":2,"timestamp":1744102174514,"text":"Run in container: uname -m","startTimestamp":1744102174489} +{"type":"start","level":2,"timestamp":1744102174514,"text":"Run in container: (cat /etc/os-release || cat /usr/lib/os-release) 2>/dev/null"} +{"type":"text","level":2,"timestamp":1744102174515,"text":"PRETTY_NAME=\"Debian GNU/Linux 11 (bullseye)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"11\"\nVERSION=\"11 (bullseye)\"\nVERSION_CODENAME=bullseye\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n"} +{"type":"text","level":2,"timestamp":1744102174515,"text":""} +{"type":"stop","level":2,"timestamp":1744102174515,"text":"Run in container: (cat /etc/os-release || cat /usr/lib/os-release) 2>/dev/null","startTimestamp":1744102174514} +{"type":"start","level":2,"timestamp":1744102174515,"text":"Run in container: (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true)"} +{"type":"stop","level":2,"timestamp":1744102174516,"text":"Run in container: (command -v getent >/dev/null 2>&1 && getent passwd 'node' || grep -E '^node|^[^:]*:[^:]*:node:' /etc/passwd || true)","startTimestamp":1744102174515} +{"type":"start","level":2,"timestamp":1744102174516,"text":"Run in container: test -f '/var/devcontainer/.patchEtcEnvironmentMarker'"} +{"type":"text","level":2,"timestamp":1744102174516,"text":""} +{"type":"text","level":2,"timestamp":1744102174516,"text":""} +{"type":"text","level":2,"timestamp":1744102174516,"text":"Exit code 1"} +{"type":"stop","level":2,"timestamp":1744102174516,"text":"Run in container: test -f '/var/devcontainer/.patchEtcEnvironmentMarker'","startTimestamp":1744102174516} +{"type":"start","level":2,"timestamp":1744102174517,"text":"Run in container: /bin/sh"} +{"type":"start","level":2,"timestamp":1744102174517,"text":"Run in container: test ! -f '/var/devcontainer/.patchEtcEnvironmentMarker' && set -o noclobber && mkdir -p '/var/devcontainer' && { > '/var/devcontainer/.patchEtcEnvironmentMarker' ; } 2> /dev/null"} +{"type":"text","level":2,"timestamp":1744102174544,"text":""} +{"type":"text","level":2,"timestamp":1744102174544,"text":""} +{"type":"stop","level":2,"timestamp":1744102174544,"text":"Run in container: test ! -f '/var/devcontainer/.patchEtcEnvironmentMarker' && set -o noclobber && mkdir -p '/var/devcontainer' && { > '/var/devcontainer/.patchEtcEnvironmentMarker' ; } 2> /dev/null","startTimestamp":1744102174517} +{"type":"start","level":2,"timestamp":1744102174544,"text":"Run in container: cat >> /etc/environment <<'etcEnvrionmentEOF'"} +{"type":"text","level":2,"timestamp":1744102174545,"text":""} +{"type":"text","level":2,"timestamp":1744102174545,"text":""} +{"type":"stop","level":2,"timestamp":1744102174545,"text":"Run in container: cat >> /etc/environment <<'etcEnvrionmentEOF'","startTimestamp":1744102174544} +{"type":"start","level":2,"timestamp":1744102174545,"text":"Run in container: test -f '/var/devcontainer/.patchEtcProfileMarker'"} +{"type":"text","level":2,"timestamp":1744102174545,"text":""} +{"type":"text","level":2,"timestamp":1744102174545,"text":""} +{"type":"text","level":2,"timestamp":1744102174545,"text":"Exit code 1"} +{"type":"stop","level":2,"timestamp":1744102174545,"text":"Run in container: test -f '/var/devcontainer/.patchEtcProfileMarker'","startTimestamp":1744102174545} +{"type":"start","level":2,"timestamp":1744102174545,"text":"Run in container: test ! -f '/var/devcontainer/.patchEtcProfileMarker' && set -o noclobber && mkdir -p '/var/devcontainer' && { > '/var/devcontainer/.patchEtcProfileMarker' ; } 2> /dev/null"} +{"type":"text","level":2,"timestamp":1744102174546,"text":""} +{"type":"text","level":2,"timestamp":1744102174546,"text":""} +{"type":"stop","level":2,"timestamp":1744102174546,"text":"Run in container: test ! -f '/var/devcontainer/.patchEtcProfileMarker' && set -o noclobber && mkdir -p '/var/devcontainer' && { > '/var/devcontainer/.patchEtcProfileMarker' ; } 2> /dev/null","startTimestamp":1744102174545} +{"type":"start","level":2,"timestamp":1744102174546,"text":"Run in container: sed -i -E 's/((^|\\s)PATH=)([^\\$]*)$/\\1${PATH:-\\3}/g' /etc/profile || true"} +{"type":"text","level":2,"timestamp":1744102174547,"text":""} +{"type":"text","level":2,"timestamp":1744102174547,"text":""} +{"type":"stop","level":2,"timestamp":1744102174547,"text":"Run in container: sed -i -E 's/((^|\\s)PATH=)([^\\$]*)$/\\1${PATH:-\\3}/g' /etc/profile || true","startTimestamp":1744102174546} +{"type":"text","level":2,"timestamp":1744102174548,"text":"userEnvProbe: loginInteractiveShell (default)"} +{"type":"text","level":1,"timestamp":1744102174548,"text":"LifecycleCommandExecutionMap: {\n \"onCreateCommand\": [],\n \"updateContentCommand\": [],\n \"postCreateCommand\": [\n {\n \"origin\": \"devcontainer.json\",\n \"command\": \"npm install -g @devcontainers/cli\"\n }\n ],\n \"postStartCommand\": [],\n \"postAttachCommand\": [],\n \"initializeCommand\": []\n}"} +{"type":"text","level":2,"timestamp":1744102174548,"text":"userEnvProbe: not found in cache"} +{"type":"text","level":2,"timestamp":1744102174548,"text":"userEnvProbe shell: /bin/bash"} +{"type":"start","level":2,"timestamp":1744102174548,"text":"Run in container: /bin/bash -lic echo -n bcf9079d-76e7-4bc1-a6e2-9da4ca796acf; cat /proc/self/environ; echo -n bcf9079d-76e7-4bc1-a6e2-9da4ca796acf"} +{"type":"start","level":2,"timestamp":1744102174549,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.onCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:49:34.285146903Z}\" != '2025-04-08T08:49:34.285146903Z' ] && echo '2025-04-08T08:49:34.285146903Z' > '/home/node/.devcontainer/.onCreateCommandMarker'"} +{"type":"text","level":2,"timestamp":1744102174552,"text":""} +{"type":"text","level":2,"timestamp":1744102174552,"text":""} +{"type":"stop","level":2,"timestamp":1744102174552,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.onCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:49:34.285146903Z}\" != '2025-04-08T08:49:34.285146903Z' ] && echo '2025-04-08T08:49:34.285146903Z' > '/home/node/.devcontainer/.onCreateCommandMarker'","startTimestamp":1744102174549} +{"type":"start","level":2,"timestamp":1744102174552,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.updateContentCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:49:34.285146903Z}\" != '2025-04-08T08:49:34.285146903Z' ] && echo '2025-04-08T08:49:34.285146903Z' > '/home/node/.devcontainer/.updateContentCommandMarker'"} +{"type":"text","level":2,"timestamp":1744102174554,"text":""} +{"type":"text","level":2,"timestamp":1744102174554,"text":""} +{"type":"stop","level":2,"timestamp":1744102174554,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.updateContentCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:49:34.285146903Z}\" != '2025-04-08T08:49:34.285146903Z' ] && echo '2025-04-08T08:49:34.285146903Z' > '/home/node/.devcontainer/.updateContentCommandMarker'","startTimestamp":1744102174552} +{"type":"start","level":2,"timestamp":1744102174554,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:49:34.285146903Z}\" != '2025-04-08T08:49:34.285146903Z' ] && echo '2025-04-08T08:49:34.285146903Z' > '/home/node/.devcontainer/.postCreateCommandMarker'"} +{"type":"text","level":2,"timestamp":1744102174555,"text":""} +{"type":"text","level":2,"timestamp":1744102174555,"text":""} +{"type":"stop","level":2,"timestamp":1744102174555,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postCreateCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:49:34.285146903Z}\" != '2025-04-08T08:49:34.285146903Z' ] && echo '2025-04-08T08:49:34.285146903Z' > '/home/node/.devcontainer/.postCreateCommandMarker'","startTimestamp":1744102174554} +{"type":"raw","level":3,"timestamp":1744102174555,"text":"\u001b[1mRunning the postCreateCommand from devcontainer.json...\u001b[0m\r\n\r\n","channel":"postCreate"} +{"type":"progress","name":"Running postCreateCommand...","status":"running","stepDetail":"npm install -g @devcontainers/cli","channel":"postCreate"} +{"type":"stop","level":2,"timestamp":1744102174604,"text":"Run in container: /bin/bash -lic echo -n bcf9079d-76e7-4bc1-a6e2-9da4ca796acf; cat /proc/self/environ; echo -n bcf9079d-76e7-4bc1-a6e2-9da4ca796acf","startTimestamp":1744102174548} +{"type":"text","level":1,"timestamp":1744102174604,"text":"bcf9079d-76e7-4bc1-a6e2-9da4ca796acfNVM_RC_VERSION=\u0000HOSTNAME=bc72db8d0c4c\u0000YARN_VERSION=1.22.22\u0000PWD=/\u0000HOME=/home/node\u0000LS_COLORS=\u0000NVM_SYMLINK_CURRENT=true\u0000DOCKER_BUILDKIT=1\u0000NVM_DIR=/usr/local/share/nvm\u0000USER=node\u0000SHLVL=1\u0000NVM_CD_FLAGS=\u0000PROMPT_DIRTRIM=4\u0000PATH=/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/node/.local/bin\u0000NODE_VERSION=18.20.8\u0000_=/bin/cat\u0000bcf9079d-76e7-4bc1-a6e2-9da4ca796acf"} +{"type":"text","level":1,"timestamp":1744102174604,"text":"\u001b[1m\u001b[31mbash: cannot set terminal process group (-1): Inappropriate ioctl for device\u001b[39m\u001b[22m\r\n\u001b[1m\u001b[31mbash: no job control in this shell\u001b[39m\u001b[22m\r\n\u001b[1m\u001b[31m\u001b[39m\u001b[22m\r\n"} +{"type":"text","level":1,"timestamp":1744102174605,"text":"userEnvProbe parsed: {\n \"NVM_RC_VERSION\": \"\",\n \"HOSTNAME\": \"bc72db8d0c4c\",\n \"YARN_VERSION\": \"1.22.22\",\n \"PWD\": \"/\",\n \"HOME\": \"/home/node\",\n \"LS_COLORS\": \"\",\n \"NVM_SYMLINK_CURRENT\": \"true\",\n \"DOCKER_BUILDKIT\": \"1\",\n \"NVM_DIR\": \"/usr/local/share/nvm\",\n \"USER\": \"node\",\n \"SHLVL\": \"1\",\n \"NVM_CD_FLAGS\": \"\",\n \"PROMPT_DIRTRIM\": \"4\",\n \"PATH\": \"/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/node/.local/bin\",\n \"NODE_VERSION\": \"18.20.8\",\n \"_\": \"/bin/cat\"\n}"} +{"type":"text","level":2,"timestamp":1744102174605,"text":"userEnvProbe PATHs:\nProbe: '/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/node/.local/bin'\nContainer: '/usr/local/share/nvm/current/bin:/usr/local/share/npm-global/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'"} +{"type":"start","level":2,"timestamp":1744102174608,"text":"Run in container: /bin/sh -c npm install -g @devcontainers/cli","channel":"postCreate"} +{"type":"raw","level":3,"timestamp":1744102175615,"text":"\nadded 1 package in 784ms\n","channel":"postCreate"} +{"type":"stop","level":2,"timestamp":1744102175622,"text":"Run in container: /bin/sh -c npm install -g @devcontainers/cli","startTimestamp":1744102174608,"channel":"postCreate"} +{"type":"progress","name":"Running postCreateCommand...","status":"succeeded","channel":"postCreate"} +{"type":"start","level":2,"timestamp":1744102175624,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postStartCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:49:34.332032445Z}\" != '2025-04-08T08:49:34.332032445Z' ] && echo '2025-04-08T08:49:34.332032445Z' > '/home/node/.devcontainer/.postStartCommandMarker'"} +{"type":"text","level":2,"timestamp":1744102175627,"text":""} +{"type":"text","level":2,"timestamp":1744102175627,"text":""} +{"type":"stop","level":2,"timestamp":1744102175627,"text":"Run in container: mkdir -p '/home/node/.devcontainer' && CONTENT=\"$(cat '/home/node/.devcontainer/.postStartCommandMarker' 2>/dev/null || echo ENOENT)\" && [ \"${CONTENT:-2025-04-08T08:49:34.332032445Z}\" != '2025-04-08T08:49:34.332032445Z' ] && echo '2025-04-08T08:49:34.332032445Z' > '/home/node/.devcontainer/.postStartCommandMarker'","startTimestamp":1744102175624} +{"type":"stop","level":2,"timestamp":1744102175628,"text":"Resolving Remote","startTimestamp":1744102171125} +{"outcome":"success","containerId":"bc72db8d0c4c4e941bd9ffc341aee64a18d3397fd45b87cd93d4746150967ba8","remoteUser":"node","remoteWorkspaceFolder":"/workspaces/devcontainers-template-starter"} diff --git a/agent/api.go b/agent/api.go index 259866797a3c4..375338acfab18 100644 --- a/agent/api.go +++ b/agent/api.go @@ -38,7 +38,8 @@ func (a *agent) apiHandler() http.Handler { } ch := agentcontainers.New(agentcontainers.WithLister(a.lister)) promHandler := PrometheusMetricsHandler(a.prometheusRegistry, a.logger) - r.Get("/api/v0/containers", ch.ServeHTTP) + r.Get("/api/v0/containers", ch.List) + r.Post("/api/v0/containers/{id}/recreate", ch.Recreate) r.Get("/api/v0/listening-ports", lp.handler) r.Get("/api/v0/netcheck", a.HandleNetcheck) r.Post("/api/v0/list-directory", a.HandleLS) diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 6e8a32b2e81a5..ef770712c340a 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -429,6 +429,16 @@ type WorkspaceAgentContainer struct { Volumes map[string]string `json:"volumes"` } +func (c *WorkspaceAgentContainer) Match(idOrName string) bool { + if c.ID == idOrName { + return true + } + if c.FriendlyName == idOrName { + return true + } + return false +} + // WorkspaceAgentContainerPort describes a port as exposed by a container. type WorkspaceAgentContainerPort struct { // Port is the port number *inside* the container. From 46d4b28384139cad17a165c27e247642237eb62a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Thu, 10 Apr 2025 10:36:27 -0700 Subject: [PATCH 050/384] chore: add x-authz-checks debug header when running in dev mode (#16873) --- coderd/coderd.go | 11 +++- coderd/httpapi/httpapi.go | 19 ++++++ coderd/httpmw/authz.go | 13 ++++ coderd/rbac/authz.go | 116 +++++++++++++++++++++++++++++++++++- coderd/users.go | 4 +- coderd/util/syncmap/map.go | 4 +- enterprise/coderd/coderd.go | 3 + go.mod | 1 - go.sum | 2 - 9 files changed, 162 insertions(+), 11 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index ff566ed369a15..0434b9d9a17c4 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -314,6 +314,9 @@ func New(options *Options) *API { if options.Authorizer == nil { options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry) + if buildinfo.IsDev() { + options.Authorizer = rbac.Recorder(options.Authorizer) + } } if options.AccessControlStore == nil { @@ -456,8 +459,14 @@ func New(options *Options) *API { options.NotificationsEnqueuer = notifications.NewNoopEnqueuer() } - ctx, cancel := context.WithCancel(context.Background()) r := chi.NewRouter() + // We add this middleware early, to make sure that authorization checks made + // by other middleware get recorded. + if buildinfo.IsDev() { + r.Use(httpmw.RecordAuthzChecks) + } + + ctx, cancel := context.WithCancel(context.Background()) // nolint:gocritic // Load deployment ID. This never changes depID, err := options.Database.GetDeploymentID(dbauthz.AsSystemRestricted(ctx)) diff --git a/coderd/httpapi/httpapi.go b/coderd/httpapi/httpapi.go index c70290ffe56b0..5c5c623474a47 100644 --- a/coderd/httpapi/httpapi.go +++ b/coderd/httpapi/httpapi.go @@ -20,6 +20,7 @@ import ( "github.com/coder/websocket/wsjson" "github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/codersdk" ) @@ -198,6 +199,20 @@ func Write(ctx context.Context, rw http.ResponseWriter, status int, response int _, span := tracing.StartSpan(ctx) defer span.End() + if rec, ok := rbac.GetAuthzCheckRecorder(ctx); ok { + // If you're here because you saw this header in a response, and you're + // trying to investigate the code, here are a couple of notable things + // for you to know: + // - If any of the checks are `false`, they might not represent the whole + // picture. There could be additional checks that weren't performed, + // because processing stopped after the failure. + // - The checks are recorded by the `authzRecorder` type, which is + // configured on server startup for development and testing builds. + // - If this header is missing from a response, make sure the response is + // being written by calling `httpapi.Write`! + rw.Header().Set("x-authz-checks", rec.String()) + } + rw.Header().Set("Content-Type", "application/json; charset=utf-8") rw.WriteHeader(status) @@ -213,6 +228,10 @@ func WriteIndent(ctx context.Context, rw http.ResponseWriter, status int, respon _, span := tracing.StartSpan(ctx) defer span.End() + if rec, ok := rbac.GetAuthzCheckRecorder(ctx); ok { + rw.Header().Set("x-authz-checks", rec.String()) + } + rw.Header().Set("Content-Type", "application/json; charset=utf-8") rw.WriteHeader(status) diff --git a/coderd/httpmw/authz.go b/coderd/httpmw/authz.go index 4c94ce362be2a..53aadb6cb7a57 100644 --- a/coderd/httpmw/authz.go +++ b/coderd/httpmw/authz.go @@ -6,6 +6,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/rbac" ) // AsAuthzSystem is a chained handler that temporarily sets the dbauthz context @@ -35,3 +36,15 @@ func AsAuthzSystem(mws ...func(http.Handler) http.Handler) func(http.Handler) ht }) } } + +// RecordAuthzChecks enables recording all of the authorization checks that +// occurred in the processing of a request. This is mostly helpful for debugging +// and understanding what permissions are required for a given action. +// +// Requires using a Recorder Authorizer. +func RecordAuthzChecks(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + r = r.WithContext(rbac.WithAuthzCheckRecorder(r.Context())) + next.ServeHTTP(rw, r) + }) +} diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index aaba7d6eae3af..3239ea3c42dc5 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -6,6 +6,7 @@ import ( _ "embed" "encoding/json" "errors" + "fmt" "strings" "sync" "time" @@ -362,11 +363,11 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action p defer span.End() err := a.authorize(ctx, subject, action, object) - - span.SetAttributes(attribute.Bool("authorized", err == nil)) + authorized := err == nil + span.SetAttributes(attribute.Bool("authorized", authorized)) dur := time.Since(start) - if err != nil { + if !authorized { a.authorizeHist.WithLabelValues("false").Observe(dur.Seconds()) return err } @@ -741,3 +742,112 @@ func rbacTraceAttributes(actor Subject, action policy.Action, objectType string, attribute.String("object_type", objectType), )...) } + +type authRecorder struct { + authz Authorizer +} + +// Recorder returns an Authorizer that records any authorization checks made +// on the Context provided for the authorization check. +// +// Requires using the RecordAuthzChecks middleware. +func Recorder(authz Authorizer) Authorizer { + return &authRecorder{authz: authz} +} + +func (c *authRecorder) Authorize(ctx context.Context, subject Subject, action policy.Action, object Object) error { + err := c.authz.Authorize(ctx, subject, action, object) + authorized := err == nil + recordAuthzCheck(ctx, action, object, authorized) + return err +} + +func (c *authRecorder) Prepare(ctx context.Context, subject Subject, action policy.Action, objectType string) (PreparedAuthorized, error) { + return c.authz.Prepare(ctx, subject, action, objectType) +} + +type authzCheckRecorderKey struct{} + +type AuthzCheckRecorder struct { + // lock guards checks + lock sync.Mutex + // checks is a list preformatted authz check IDs and their result + checks []recordedCheck +} + +type recordedCheck struct { + name string + // true => authorized, false => not authorized + result bool +} + +func WithAuthzCheckRecorder(ctx context.Context) context.Context { + return context.WithValue(ctx, authzCheckRecorderKey{}, &AuthzCheckRecorder{}) +} + +func recordAuthzCheck(ctx context.Context, action policy.Action, object Object, authorized bool) { + r, ok := ctx.Value(authzCheckRecorderKey{}).(*AuthzCheckRecorder) + if !ok { + return + } + + // We serialize the check using the following syntax + var b strings.Builder + if object.OrgID != "" { + _, err := fmt.Fprintf(&b, "organization:%v::", object.OrgID) + if err != nil { + return + } + } + if object.AnyOrgOwner { + _, err := fmt.Fprint(&b, "organization:any::") + if err != nil { + return + } + } + if object.Owner != "" { + _, err := fmt.Fprintf(&b, "owner:%v::", object.Owner) + if err != nil { + return + } + } + if object.ID != "" { + _, err := fmt.Fprintf(&b, "id:%v::", object.ID) + if err != nil { + return + } + } + _, err := fmt.Fprintf(&b, "%v.%v", object.RBACObject().Type, action) + if err != nil { + return + } + + r.lock.Lock() + defer r.lock.Unlock() + r.checks = append(r.checks, recordedCheck{name: b.String(), result: authorized}) +} + +func GetAuthzCheckRecorder(ctx context.Context) (*AuthzCheckRecorder, bool) { + checks, ok := ctx.Value(authzCheckRecorderKey{}).(*AuthzCheckRecorder) + if !ok { + return nil, false + } + + return checks, true +} + +// String serializes all of the checks recorded, using the following syntax: +func (r *AuthzCheckRecorder) String() string { + r.lock.Lock() + defer r.lock.Unlock() + + if len(r.checks) == 0 { + return "nil" + } + + checks := make([]string, 0, len(r.checks)) + for _, check := range r.checks { + checks = append(checks, fmt.Sprintf("%v=%v", check.name, check.result)) + } + return strings.Join(checks, "; ") +} diff --git a/coderd/users.go b/coderd/users.go index 9b6407156cfa1..d97abc82b2fd1 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -9,7 +9,6 @@ import ( "slices" "github.com/go-chi/chi/v5" - "github.com/go-chi/render" "github.com/google/uuid" "golang.org/x/xerrors" @@ -273,8 +272,7 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) { organizationIDsByUserID[organizationIDsByMemberIDsRow.UserID] = organizationIDsByMemberIDsRow.OrganizationIDs } - render.Status(r, http.StatusOK) - render.JSON(rw, r, codersdk.GetUsersResponse{ + httpapi.Write(ctx, rw, http.StatusOK, codersdk.GetUsersResponse{ Users: convertUsers(users, organizationIDsByUserID), Count: int(userCount), }) diff --git a/coderd/util/syncmap/map.go b/coderd/util/syncmap/map.go index 178aa3e4f6fd0..f35973ea42690 100644 --- a/coderd/util/syncmap/map.go +++ b/coderd/util/syncmap/map.go @@ -1,6 +1,8 @@ package syncmap -import "sync" +import ( + "sync" +) // Map is a type safe sync.Map type Map[K, V any] struct { diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index cb2a342fb1c8a..c451e71fc445e 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -71,6 +71,9 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { } if options.Options.Authorizer == nil { options.Options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry) + if buildinfo.IsDev() { + options.Authorizer = rbac.Recorder(options.Authorizer) + } } if options.ReplicaErrorGracePeriod == 0 { // This will prevent the error from being shown for a minute diff --git a/go.mod b/go.mod index 7421d224d7c5d..56fdd053f407e 100644 --- a/go.mod +++ b/go.mod @@ -116,7 +116,6 @@ require ( github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.15.0 - github.com/go-chi/render v1.0.1 github.com/go-jose/go-jose/v4 v4.0.5 github.com/go-logr/logr v1.4.2 github.com/go-playground/validator/v10 v10.26.0 diff --git a/go.sum b/go.sum index 197ae825a2c5f..ca3e4d2caedf3 100644 --- a/go.sum +++ b/go.sum @@ -367,8 +367,6 @@ github.com/go-chi/hostrouter v0.2.0 h1:GwC7TZz8+SlJN/tV/aeJgx4F+mI5+sp+5H1PelQUj github.com/go-chi/hostrouter v0.2.0/go.mod h1:pJ49vWVmtsKRKZivQx0YMYv4h0aX+Gcn6V23Np9Wf1s= github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= -github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= -github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= From 1e0051a9a27db51b17a112ababafe733dd7b786f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 10 Apr 2025 19:08:38 +0100 Subject: [PATCH 051/384] feat(testutil): add GetRandomNameHyphenated (#17342) This started coming up more often for me, so time for a test helper! --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cli/templatepush_test.go | 2 +- coderd/templateversions_test.go | 2 +- testutil/names.go | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index 89fd024b0c33a..b8e4147e6bab4 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -534,7 +534,7 @@ func TestTemplatePush(t *testing.T) { "test_name": tt.name, })) - templateName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-") + templateName := testutil.GetRandomNameHyphenated(t) inv, root := clitest.New(t, "templates", "push", templateName, "-d", tempDir, "--yes") clitest.SetupConfig(t, templateAdmin, root) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 4e3e3d2f7f2b0..433441fdd4cf9 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -617,7 +617,7 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { require.NoError(t, err) // Create a template version from the archive - tvName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-") + tvName := testutil.GetRandomNameHyphenated(t) tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ Name: tvName, StorageMethod: codersdk.ProvisionerStorageMethodFile, diff --git a/testutil/names.go b/testutil/names.go index ee182ed50b68d..e53e854fae239 100644 --- a/testutil/names.go +++ b/testutil/names.go @@ -2,6 +2,7 @@ package testutil import ( "strconv" + "strings" "sync/atomic" "testing" @@ -25,6 +26,14 @@ func GetRandomName(t testing.TB) string { return incSuffix(name, n.Add(1), maxNameLen) } +// GetRandomNameHyphenated is as GetRandomName but uses a hyphen "-" instead of +// an underscore. +func GetRandomNameHyphenated(t testing.TB) string { + t.Helper() + name := namesgenerator.GetRandomName(0) + return strings.ReplaceAll(name, "_", "-") +} + func incSuffix(s string, num int64, maxLen int) string { suffix := strconv.FormatInt(num, 10) if len(s)+len(suffix) <= maxLen { From ed20bab3e0dd17ca082b82367b16c8e7f3a29705 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Thu, 10 Apr 2025 14:21:29 -0400 Subject: [PATCH 052/384] docs: move AI-agent docs out of tutorials and into a top-level section (#17231) redirects in: https://github.com/coder/coder.com/pull/873 [preview](https://coder.com/docs/@move-ai-agents-up-1/coder-ai) - [x] icon - [x] shorten ~Modify or truncate the current~ title ~to reduce its length for improved readability, conciseness, or formatting consistency.~ - [x] Best practices & adding tools via MCP - edit title, desc, headings --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- .../ai-agents => coder-ai}/README.md | 13 +- .../ai-agents => coder-ai}/agents.md | 7 +- .../ai-agents => coder-ai}/best-practices.md | 15 ++- .../ai-agents => coder-ai}/coder-dashboard.md | 7 +- .../ai-agents => coder-ai}/create-template.md | 7 +- .../ai-agents => coder-ai}/custom-agents.md | 3 +- .../ai-agents => coder-ai}/headless.md | 7 +- .../ai-agents => coder-ai}/ide-integration.md | 5 +- .../ai-agents => coder-ai}/issue-tracker.md | 11 +- .../ai-agents => coder-ai}/securing.md | 3 +- docs/images/icons/wand.svg | 3 + docs/manifest.json | 123 +++++++++--------- 12 files changed, 110 insertions(+), 94 deletions(-) rename docs/{tutorials/ai-agents => coder-ai}/README.md (81%) rename docs/{tutorials/ai-agents => coder-ai}/agents.md (95%) rename docs/{tutorials/ai-agents => coder-ai}/best-practices.md (86%) rename docs/{tutorials/ai-agents => coder-ai}/coder-dashboard.md (77%) rename docs/{tutorials/ai-agents => coder-ai}/create-template.md (89%) rename docs/{tutorials/ai-agents => coder-ai}/custom-agents.md (97%) rename docs/{tutorials/ai-agents => coder-ai}/headless.md (89%) rename docs/{tutorials/ai-agents => coder-ai}/ide-integration.md (87%) rename docs/{tutorials/ai-agents => coder-ai}/issue-tracker.md (82%) rename docs/{tutorials/ai-agents => coder-ai}/securing.md (96%) create mode 100644 docs/images/icons/wand.svg diff --git a/docs/tutorials/ai-agents/README.md b/docs/coder-ai/README.md similarity index 81% rename from docs/tutorials/ai-agents/README.md rename to docs/coder-ai/README.md index fe3ef1bb97c37..7c7227b960e58 100644 --- a/docs/tutorials/ai-agents/README.md +++ b/docs/coder-ai/README.md @@ -1,8 +1,9 @@ -# Run AI Agents in Coder (Early Access) +# Use AI Coding Agents in Coder Workspaces > [!NOTE] > -> This functionality is in early access and still evolving. +> This functionality is in early access and is evolving rapidly. +> > For now, we recommend testing it in a demo or staging environment, > rather than deploying to production. > @@ -14,19 +15,19 @@ AI Coding Agents such as [Claude Code](https://docs.anthropic.com/en/docs/agents - Protyping web applications or landing pages - Researching / onboarding to a codebase - Assisting with lightweight refactors -- Writing tests and documentation +- Writing tests and draft documentation - Small, well-defined chores With Coder, you can self-host AI agents in isolated development environments with proper context and tooling around your existing developer workflows. Whether you are a regulated enterprise or an individual developer, running AI agents at scale with Coder is much more productive and secure than running them locally. -![AI Agents in Coder](../../images/guides//ai-agents/landing.png) +![AI Agents in Coder](../images/guides/ai-agents/landing.png) ## Prerequisites Coder is free and open source for developers, with a [premium plan](https://coder.com/pricing) for enterprises. You can self-host a Coder deployment in your own cloud provider. -- A [Coder deployment](../../install/) with v2.21.0 or later -- A Coder [template](../../admin/templates/) for your project(s). +- A [Coder deployment](../install/index.md) with v2.21.0 or later +- A Coder [template](../admin/templates/index.md) for your project(s). - Access to at least one ML model (e.g. Anthropic Claude, Google Gemini, OpenAI) - Cloud Model Providers (AWS Bedrock, GCP Vertex AI, Azure OpenAI) are supported with some agents - Self-hosted models (e.g. llama3) and AI proxies (OpenRouter) are supported with some agents diff --git a/docs/tutorials/ai-agents/agents.md b/docs/coder-ai/agents.md similarity index 95% rename from docs/tutorials/ai-agents/agents.md rename to docs/coder-ai/agents.md index 2a2aa8c216107..009629cc67082 100644 --- a/docs/tutorials/ai-agents/agents.md +++ b/docs/coder-ai/agents.md @@ -2,9 +2,10 @@ > [!NOTE] > -> This page is not exhaustive and the landscape is evolving rapidly. Please -> [open an issue](https://github.com/coder/coder/issues/new) or submit a pull -> request if you'd like to see your favorite agent added or updated. +> This page is not exhaustive and the landscape is evolving rapidly. +> +> Please [open an issue](https://github.com/coder/coder/issues/new) or submit a +> pull request if you'd like to see your favorite agent added or updated. There are several types of coding agents emerging: diff --git a/docs/tutorials/ai-agents/best-practices.md b/docs/coder-ai/best-practices.md similarity index 86% rename from docs/tutorials/ai-agents/best-practices.md rename to docs/coder-ai/best-practices.md index 82df73ce21af0..3b031278c4b02 100644 --- a/docs/tutorials/ai-agents/best-practices.md +++ b/docs/coder-ai/best-practices.md @@ -1,8 +1,9 @@ -# Best Practices & Adding Tools via MCP +# Model Context Protocols (MCP) and adding AI tools > [!NOTE] > -> This functionality is in early access and still evolving. +> This functionality is in early access and is evolving rapidly. +> > For now, we recommend testing it in a demo or staging environment, > rather than deploying to production. > @@ -21,8 +22,8 @@ for development. With AI Agents, this is no exception. ## Best Practices -- Since agents are still early, it is best to use the most capable ML models you - have access to in order to evaluate their performance. +- Use the most capable ML models you have access to in order to evaluate Agent + performance. - Set a system prompt with the `AI_SYSTEM_PROMPT` environment in your template - Within your repositories, write a `.cursorrules`, `CLAUDE.md` or similar file to guide the agent's behavior. @@ -30,9 +31,11 @@ for development. With AI Agents, this is no exception. (e.g. `gh`) in your image/template. - Ensure your [template](./create-template.md) is truly pre-configured for development without manual intervention (e.g. repos are cloned, dependencies - are built, secrets are added/mocked, etc.) - > Note: [External authentication](../../admin/external-auth.md) can be helpful + are built, secrets are added/mocked, etc.). + + > Note: [External authentication](../admin/external-auth.md) can be helpful > to authenticate with third-party services such as GitHub or JFrog. + - Give your agent the proper tools via MCP to interact with your codebase and related services. - Read our recommendations on [securing agents](./securing.md) to avoid diff --git a/docs/tutorials/ai-agents/coder-dashboard.md b/docs/coder-ai/coder-dashboard.md similarity index 77% rename from docs/tutorials/ai-agents/coder-dashboard.md rename to docs/coder-ai/coder-dashboard.md index bc660191497fe..90004897c3542 100644 --- a/docs/tutorials/ai-agents/coder-dashboard.md +++ b/docs/coder-ai/coder-dashboard.md @@ -1,6 +1,7 @@ > [!NOTE] > -> This functionality is in early access and still evolving. +> This functionality is in early access and is evolving rapidly. +> > For now, we recommend testing it in a demo or staging environment, > rather than deploying to production. > @@ -17,9 +18,9 @@ Once you have an agent running and reporting activity to Coder, you can view status and switch between workspaces from the Coder dashboard. -![Coder Dashboard](../../images/guides/ai-agents/workspaces-list.png) +![Coder Dashboard](../images/guides/ai-agents/workspaces-list.png) -![Workspace Details](../../images/guides/ai-agents/workspace-details.png) +![Workspace Details](../images/guides/ai-agents/workspace-details.png) ## Next Steps diff --git a/docs/tutorials/ai-agents/create-template.md b/docs/coder-ai/create-template.md similarity index 89% rename from docs/tutorials/ai-agents/create-template.md rename to docs/coder-ai/create-template.md index 56b51505ff0d2..1b3c385f083e1 100644 --- a/docs/tutorials/ai-agents/create-template.md +++ b/docs/coder-ai/create-template.md @@ -2,7 +2,8 @@ > [!NOTE] > -> This functionality is in early access and still evolving. +> This functionality is in early access and is evolving rapidly. +> > For now, we recommend testing it in a demo or staging environment, > rather than deploying to production. > @@ -27,7 +28,7 @@ template that has all of the tools and dependencies installed. This can be done in the Coder UI: -![Duplicate template](../../images/guides/ai-agents/duplicate.png) +![Duplicate template](../images/guides/ai-agents/duplicate.png) ## 2. Add a module for supported agents @@ -48,7 +49,7 @@ report status back to the Coder control plane. The Coder dashboard should now show tasks being reported by the agent. -![AI Agents in Coder](../../images/guides//ai-agents/landing.png) +![AI Agents in Coder](../images/guides/ai-agents/landing.png) ## Next Steps diff --git a/docs/tutorials/ai-agents/custom-agents.md b/docs/coder-ai/custom-agents.md similarity index 97% rename from docs/tutorials/ai-agents/custom-agents.md rename to docs/coder-ai/custom-agents.md index 5c276eb4bdcbd..b6c67b6f4b3c9 100644 --- a/docs/tutorials/ai-agents/custom-agents.md +++ b/docs/coder-ai/custom-agents.md @@ -2,7 +2,8 @@ > [!NOTE] > -> This functionality is in early access and still evolving. +> This functionality is in early access and is evolving rapidly. +> > For now, we recommend testing it in a demo or staging environment, > rather than deploying to production. > diff --git a/docs/tutorials/ai-agents/headless.md b/docs/coder-ai/headless.md similarity index 89% rename from docs/tutorials/ai-agents/headless.md rename to docs/coder-ai/headless.md index c2c415380ac04..b88511524bde3 100644 --- a/docs/tutorials/ai-agents/headless.md +++ b/docs/coder-ai/headless.md @@ -1,6 +1,7 @@ > [!NOTE] > -> This functionality is in early access and still evolving. +> This functionality is in early access and is evolving rapidly. +> > For now, we recommend testing it in a demo or staging environment, > rather than deploying to production. > @@ -44,12 +45,12 @@ coder exp mcp configure cursor # Configure Cursor to interact with Coder ## Coder CLI Workspaces can be created, started, and stopped via the Coder CLI. See the -[CLI docs](../../reference/cli/) for more information. +[CLI docs](../reference/cli/index.md) for more information. ## REST API The Coder REST API can be used to manage workspaces and agents. See the -[API docs](../../reference/api/) for more information. +[API docs](../reference/api/index.md) for more information. ## Next Steps diff --git a/docs/tutorials/ai-agents/ide-integration.md b/docs/coder-ai/ide-integration.md similarity index 87% rename from docs/tutorials/ai-agents/ide-integration.md rename to docs/coder-ai/ide-integration.md index 678faf18a743a..0a1bb1ff51ff6 100644 --- a/docs/tutorials/ai-agents/ide-integration.md +++ b/docs/coder-ai/ide-integration.md @@ -1,6 +1,7 @@ > [!NOTE] > -> This functionality is in early access and still evolving. +> This functionality is in early access and is evolving rapidly. +> > For now, we recommend testing it in a demo or staging environment, > rather than deploying to production. > @@ -21,7 +22,7 @@ Once you have an agent running and reporting activity to Coder, you can view the status and switch between workspaces from the IDE. This can be very helpful for reviewing code, working along with the agent, and more. -![IDE Integration](../../images/guides/ai-agents/ide-integration.png) +![IDE Integration](../images/guides/ai-agents/ide-integration.png) ## Next Steps diff --git a/docs/tutorials/ai-agents/issue-tracker.md b/docs/coder-ai/issue-tracker.md similarity index 82% rename from docs/tutorials/ai-agents/issue-tracker.md rename to docs/coder-ai/issue-tracker.md index 597dd652ddfd5..680384b37f0e9 100644 --- a/docs/tutorials/ai-agents/issue-tracker.md +++ b/docs/coder-ai/issue-tracker.md @@ -2,7 +2,8 @@ > [!NOTE] > -> This functionality is in early access and still evolving. +> This functionality is in early access and is evolving rapidly. +> > For now, we recommend testing it in a demo or staging environment, > rather than deploying to production. > @@ -28,7 +29,7 @@ The [start-workspace](https://github.com/coder/start-workspace-action) GitHub action will create a Coder workspace based on a specific phrase in a comment (e.g. `@coder`). -![GitHub Issue](../../images/guides/ai-agents/github-action.png) +![GitHub Issue](../images/guides/ai-agents/github-action.png) When properly configured with an [AI template](./create-template.md), the agent will begin working on the issue. @@ -39,15 +40,15 @@ We're working on adding support for an agent automatically creating pull requests and responding to your comments. Check back soon or [join our Discord](https://discord.gg/coder) to stay updated. -![GitHub Pull Request](../../images/guides/ai-agents/github-pr.png) +![GitHub Pull Request](../images/guides/ai-agents/github-pr.png) ## Integrating with Other Issue Trackers While support for other issue trackers is under consideration, you can can use -the [REST API](../../reference/api/) or [CLI](../../reference/cli/) to integrate +the [REST API](../reference/api/index.md) or [CLI](../reference/cli/index.md) to integrate with other issue trackers or CI pipelines. -In addition, an [Open in Coder](../../admin/templates/open-in-coder.md) flow can +In addition, an [Open in Coder](../admin/templates/open-in-coder.md) flow can be used to generate a URL and/or markdown button in your issue tracker to automatically create a workspace with specific parameters. diff --git a/docs/tutorials/ai-agents/securing.md b/docs/coder-ai/securing.md similarity index 96% rename from docs/tutorials/ai-agents/securing.md rename to docs/coder-ai/securing.md index 31b628b83ebd1..91ce3b6da5249 100644 --- a/docs/tutorials/ai-agents/securing.md +++ b/docs/coder-ai/securing.md @@ -1,6 +1,7 @@ > [!NOTE] > -> This functionality is in early access and still evolving. +> This functionality is in early access and is evolving rapidly. +> > For now, we recommend testing it in a demo or staging environment, > rather than deploying to production. > diff --git a/docs/images/icons/wand.svg b/docs/images/icons/wand.svg new file mode 100644 index 0000000000000..92c499bab807c --- /dev/null +++ b/docs/images/icons/wand.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/manifest.json b/docs/manifest.json index df535a1687807..fe044da4bb441 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -667,6 +667,68 @@ } ] }, + { + "title": "Run AI Coding Agents in Coder", + "description": "Learn how to run and integrate AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", + "path": "./coder-ai/README.md", + "icon_path": "./images/icons/wand.svg", + "state": ["early access"], + "children": [ + { + "title": "Learn about coding agents", + "description": "Learn about the different AI agents and their tradeoffs", + "path": "./coder-ai/agents.md" + }, + { + "title": "Create a Coder template for agents", + "description": "Create a purpose-built template for your AI agents", + "path": "./coder-ai/create-template.md", + "state": ["early access"] + }, + { + "title": "Integrate with your issue tracker", + "description": "Assign tickets to AI agents and interact via code reviews", + "path": "./coder-ai/issue-tracker.md", + "state": ["early access"] + }, + { + "title": "Model Context Protocols (MCP) and adding AI tools", + "description": "Improve results by adding tools to your AI agents", + "path": "./coder-ai/best-practices.md", + "state": ["early access"] + }, + { + "title": "Supervise agents via Coder UI", + "description": "Interact with agents via the Coder UI", + "path": "./coder-ai/coder-dashboard.md", + "state": ["early access"] + }, + { + "title": "Supervise agents via the IDE", + "description": "Interact with agents via VS Code or Cursor", + "path": "./coder-ai/ide-integration.md", + "state": ["early access"] + }, + { + "title": "Programmatically manage agents", + "description": "Manage agents via MCP, the Coder CLI, and/or REST API", + "path": "./coder-ai/headless.md", + "state": ["early access"] + }, + { + "title": "Securing agents in Coder", + "description": "Learn how to secure agents with boundaries", + "path": "./coder-ai/securing.md", + "state": ["early access"] + }, + { + "title": "Custom agents", + "description": "Learn how to use custom agents with Coder", + "path": "./coder-ai/custom-agents.md", + "state": ["early access"] + } + ] + }, { "title": "Contributing", "description": "Learn how to contribute to Coder", @@ -710,67 +772,6 @@ "description": "Learn how to install and run Coder quickly", "path": "./tutorials/quickstart.md" }, - { - "title": "Run AI Coding Agents with Coder", - "description": "Learn how to run and secure agents in Coder", - "path": "./tutorials/ai-agents/README.md", - "state": ["early access"], - "children": [ - { - "title": "Learn about coding agents", - "description": "Learn about the different AI agents and their tradeoffs", - "path": "./tutorials/ai-agents/agents.md" - }, - { - "title": "Create a Coder template for agents", - "description": "Create a purpose-built template for your AI agents", - "path": "./tutorials/ai-agents/create-template.md", - "state": ["early access"] - }, - { - "title": "Integrate with your issue tracker", - "description": "Assign tickets to AI agents and interact via code reviews", - "path": "./tutorials/ai-agents/issue-tracker.md", - "state": ["early access"] - }, - { - "title": "Best practices \u0026 adding tools via MCP", - "description": "Improve results by adding tools to your agents", - "path": "./tutorials/ai-agents/best-practices.md", - "state": ["early access"] - }, - { - "title": "Supervise agents via Coder UI", - "description": "Interact with agents via the Coder UI", - "path": "./tutorials/ai-agents/coder-dashboard.md", - "state": ["early access"] - }, - { - "title": "Supervise agents via the IDE", - "description": "Interact with agents via VS Code or Cursor", - "path": "./tutorials/ai-agents/ide-integration.md", - "state": ["early access"] - }, - { - "title": "Programmatically manage agents", - "description": "Manage agents via MCP, the Coder CLI, and/or REST API", - "path": "./tutorials/ai-agents/headless.md", - "state": ["early access"] - }, - { - "title": "Securing agents in Coder", - "description": "Learn how to secure agents with boundaries", - "path": "./tutorials/ai-agents/securing.md", - "state": ["early access"] - }, - { - "title": "Custom agents", - "description": "Learn how to use custom agents with Coder", - "path": "./tutorials/ai-agents/custom-agents.md", - "state": ["early access"] - } - ] - }, { "title": "Write a Template from Scratch", "description": "Learn how to author Coder templates", From e5ba8b791232d67fdc052a089908dea6fe743bed Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Thu, 10 Apr 2025 14:35:29 -0400 Subject: [PATCH 053/384] docs: update aws instance recommendations (#17344) from @jatcod3r on Slack: > for the AWS recs on our [validated arch](https://coder.com/docs/admin/infrastructure/validated-architectures/1k-users) docs, should we be referencing customers to use non-T type instances? > Once you've exceeded EC2's [CPU credits](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances.html) Coder starts performing poorly. > We do suggest to [scale for peak demand](https://coder.com/docs/tutorials/best-practices/scale-coder#scaling-3), so does recommending something from the [cpu](https://aws.amazon.com/ec2/instance-types/#Compute_Optimized) or [memory optimized](https://aws.amazon.com/ec2/instance-types/#Memory_Optimized) types make sense? [preview](https://coder.com/docs/@aws-ec2-arch/admin/infrastructure/validated-architectures#aws-instance-types) --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- .../validated-architectures/1k-users.md | 15 +++++++++++---- .../validated-architectures/2k-users.md | 15 +++++++++++---- .../validated-architectures/3k-users.md | 15 +++++++++++---- .../validated-architectures/index.md | 14 ++++++++++++++ 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/docs/admin/infrastructure/validated-architectures/1k-users.md b/docs/admin/infrastructure/validated-architectures/1k-users.md index 3cb115db58702..eab7e457a94e8 100644 --- a/docs/admin/infrastructure/validated-architectures/1k-users.md +++ b/docs/admin/infrastructure/validated-architectures/1k-users.md @@ -14,7 +14,7 @@ tech startups, educational units, or small to mid-sized enterprises. | Users | Node capacity | Replicas | GCP | AWS | Azure | |-------------|---------------------|--------------------------|-----------------|------------|-------------------| -| Up to 1,000 | 2 vCPU, 8 GB memory | 1-2 nodes, 1 coderd each | `n1-standard-2` | `t3.large` | `Standard_D2s_v3` | +| Up to 1,000 | 2 vCPU, 8 GB memory | 1-2 nodes, 1 coderd each | `n1-standard-2` | `m5.large` | `Standard_D2s_v3` | **Footnotes**: @@ -25,7 +25,7 @@ tech startups, educational units, or small to mid-sized enterprises. | Users | Node capacity | Replicas | GCP | AWS | Azure | |-------------|----------------------|-------------------------------|------------------|--------------|-------------------| -| Up to 1,000 | 8 vCPU, 32 GB memory | 2 nodes, 30 provisioners each | `t2d-standard-8` | `t3.2xlarge` | `Standard_D8s_v3` | +| Up to 1,000 | 8 vCPU, 32 GB memory | 2 nodes, 30 provisioners each | `t2d-standard-8` | `c5.2xlarge` | `Standard_D8s_v3` | **Footnotes**: @@ -35,7 +35,7 @@ tech startups, educational units, or small to mid-sized enterprises. | Users | Node capacity | Replicas | GCP | AWS | Azure | |-------------|----------------------|------------------------------|------------------|--------------|-------------------| -| Up to 1,000 | 8 vCPU, 32 GB memory | 64 nodes, 16 workspaces each | `t2d-standard-8` | `t3.2xlarge` | `Standard_D8s_v3` | +| Up to 1,000 | 8 vCPU, 32 GB memory | 64 nodes, 16 workspaces each | `t2d-standard-8` | `m5.2xlarge` | `Standard_D8s_v3` | **Footnotes**: @@ -48,4 +48,11 @@ tech startups, educational units, or small to mid-sized enterprises. | Users | Node capacity | Replicas | Storage | GCP | AWS | Azure | |-------------|---------------------|----------|---------|--------------------|---------------|-------------------| -| Up to 1,000 | 2 vCPU, 8 GB memory | 1 node | 512 GB | `db-custom-2-7680` | `db.t3.large` | `Standard_D2s_v3` | +| Up to 1,000 | 2 vCPU, 8 GB memory | 1 node | 512 GB | `db-custom-2-7680` | `db.m5.large` | `Standard_D2s_v3` | + +**Footnotes for AWS instance types**: + +- For production deployments, we recommend using non-burstable instance types, + such as `m5` or `c5`, instead of burstable instances, such as `t3`. + Burstable instances can experience significant performance degradation once + CPU credits are exhausted, leading to poor user experience under sustained load. diff --git a/docs/admin/infrastructure/validated-architectures/2k-users.md b/docs/admin/infrastructure/validated-architectures/2k-users.md index f63f66fed4b6b..1769125ff0fc0 100644 --- a/docs/admin/infrastructure/validated-architectures/2k-users.md +++ b/docs/admin/infrastructure/validated-architectures/2k-users.md @@ -19,13 +19,13 @@ deployment reliability under load. | Users | Node capacity | Replicas | GCP | AWS | Azure | |-------------|----------------------|------------------------|-----------------|-------------|-------------------| -| Up to 2,000 | 4 vCPU, 16 GB memory | 2 nodes, 1 coderd each | `n1-standard-4` | `t3.xlarge` | `Standard_D4s_v3` | +| Up to 2,000 | 4 vCPU, 16 GB memory | 2 nodes, 1 coderd each | `n1-standard-4` | `m5.xlarge` | `Standard_D4s_v3` | ### Provisioner nodes | Users | Node capacity | Replicas | GCP | AWS | Azure | |-------------|----------------------|-------------------------------|------------------|--------------|-------------------| -| Up to 2,000 | 8 vCPU, 32 GB memory | 4 nodes, 30 provisioners each | `t2d-standard-8` | `t3.2xlarge` | `Standard_D8s_v3` | +| Up to 2,000 | 8 vCPU, 32 GB memory | 4 nodes, 30 provisioners each | `t2d-standard-8` | `c5.2xlarge` | `Standard_D8s_v3` | **Footnotes**: @@ -38,7 +38,7 @@ deployment reliability under load. | Users | Node capacity | Replicas | GCP | AWS | Azure | |-------------|----------------------|-------------------------------|------------------|--------------|-------------------| -| Up to 2,000 | 8 vCPU, 32 GB memory | 128 nodes, 16 workspaces each | `t2d-standard-8` | `t3.2xlarge` | `Standard_D8s_v3` | +| Up to 2,000 | 8 vCPU, 32 GB memory | 128 nodes, 16 workspaces each | `t2d-standard-8` | `m5.2xlarge` | `Standard_D8s_v3` | **Footnotes**: @@ -51,9 +51,16 @@ deployment reliability under load. | Users | Node capacity | Replicas | Storage | GCP | AWS | Azure | |-------------|----------------------|----------|---------|---------------------|----------------|-------------------| -| Up to 2,000 | 4 vCPU, 16 GB memory | 1 node | 1 TB | `db-custom-4-15360` | `db.t3.xlarge` | `Standard_D4s_v3` | +| Up to 2,000 | 4 vCPU, 16 GB memory | 1 node | 1 TB | `db-custom-4-15360` | `db.m5.xlarge` | `Standard_D4s_v3` | **Footnotes**: - Consider adding more replicas if the workspace activity is higher than 500 workspace builds per day or to achieve higher RPS. + +**Footnotes for AWS instance types**: + +- For production deployments, we recommend using non-burstable instance types, + such as `m5` or `c5`, instead of burstable instances, such as `t3`. + Burstable instances can experience significant performance degradation once + CPU credits are exhausted, leading to poor user experience under sustained load. diff --git a/docs/admin/infrastructure/validated-architectures/3k-users.md b/docs/admin/infrastructure/validated-architectures/3k-users.md index bea84db5e8b32..b742e5e21658c 100644 --- a/docs/admin/infrastructure/validated-architectures/3k-users.md +++ b/docs/admin/infrastructure/validated-architectures/3k-users.md @@ -20,13 +20,13 @@ continuously improve the reliability and performance of the platform. | Users | Node capacity | Replicas | GCP | AWS | Azure | |-------------|----------------------|-----------------------|-----------------|-------------|-------------------| -| Up to 3,000 | 8 vCPU, 32 GB memory | 4 node, 1 coderd each | `n1-standard-4` | `t3.xlarge` | `Standard_D4s_v3` | +| Up to 3,000 | 8 vCPU, 32 GB memory | 4 node, 1 coderd each | `n1-standard-4` | `m5.xlarge` | `Standard_D4s_v3` | ### Provisioner nodes | Users | Node capacity | Replicas | GCP | AWS | Azure | |-------------|----------------------|-------------------------------|------------------|--------------|-------------------| -| Up to 3,000 | 8 vCPU, 32 GB memory | 8 nodes, 30 provisioners each | `t2d-standard-8` | `t3.2xlarge` | `Standard_D8s_v3` | +| Up to 3,000 | 8 vCPU, 32 GB memory | 8 nodes, 30 provisioners each | `t2d-standard-8` | `c5.2xlarge` | `Standard_D8s_v3` | **Footnotes**: @@ -40,7 +40,7 @@ continuously improve the reliability and performance of the platform. | Users | Node capacity | Replicas | GCP | AWS | Azure | |-------------|----------------------|-------------------------------|------------------|--------------|-------------------| -| Up to 3,000 | 8 vCPU, 32 GB memory | 256 nodes, 12 workspaces each | `t2d-standard-8` | `t3.2xlarge` | `Standard_D8s_v3` | +| Up to 3,000 | 8 vCPU, 32 GB memory | 256 nodes, 12 workspaces each | `t2d-standard-8` | `m5.2xlarge` | `Standard_D8s_v3` | **Footnotes**: @@ -54,9 +54,16 @@ continuously improve the reliability and performance of the platform. | Users | Node capacity | Replicas | Storage | GCP | AWS | Azure | |-------------|----------------------|----------|---------|---------------------|-----------------|-------------------| -| Up to 3,000 | 8 vCPU, 32 GB memory | 2 nodes | 1.5 TB | `db-custom-8-30720` | `db.t3.2xlarge` | `Standard_D8s_v3` | +| Up to 3,000 | 8 vCPU, 32 GB memory | 2 nodes | 1.5 TB | `db-custom-8-30720` | `db.m5.2xlarge` | `Standard_D8s_v3` | **Footnotes**: - Consider adding more replicas if the workspace activity is higher than 1500 workspace builds per day or to achieve higher RPS. + +**Footnotes for AWS instance types**: + +- For production deployments, we recommend using non-burstable instance types, + such as `m5` or `c5`, instead of burstable instances, such as `t3`. + Burstable instances can experience significant performance degradation once + CPU credits are exhausted, leading to poor user experience under sustained load. diff --git a/docs/admin/infrastructure/validated-architectures/index.md b/docs/admin/infrastructure/validated-architectures/index.md index 2040b781ae0fa..fee01e777fbfe 100644 --- a/docs/admin/infrastructure/validated-architectures/index.md +++ b/docs/admin/infrastructure/validated-architectures/index.md @@ -220,6 +220,20 @@ For sizing recommendations, see the below reference architectures: - [Up to 3,000 users](3k-users.md) +### AWS Instance Types + +For production AWS deployments, we recommend using non-burstable instance types, +such as `m5` or `c5`, instead of burstable instances, such as `t3`. +Burstable instances can experience significant performance degradation once +CPU credits are exhausted, leading to poor user experience under sustained load. + +| Component | Recommended Instance Type | Reason | +|-------------------|---------------------------|----------------------------------------------------------| +| coderd nodes | `m5` | Balanced compute and memory for API and UI serving. | +| Provisioner nodes | `c5` | Compute-optimized performance for faster builds. | +| Workspace nodes | `m5` | Balanced performance for general development workloads. | +| Database nodes | `db.m5` | Consistent database performance for reliable operations. | + ### Networking It is likely your enterprise deploys Kubernetes clusters with various networking From c9682cb6cf1cdc78f1f242920df5a3bcd76512cf Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Thu, 10 Apr 2025 15:33:46 -0400 Subject: [PATCH 054/384] docs: hotfix rename coder-ai readme to index (#17346) [preview](https://coder.com/docs/@hotfix-ai-coder-top/) Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/coder-ai/{README.md => index.md} | 0 docs/manifest.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/coder-ai/{README.md => index.md} (100%) diff --git a/docs/coder-ai/README.md b/docs/coder-ai/index.md similarity index 100% rename from docs/coder-ai/README.md rename to docs/coder-ai/index.md diff --git a/docs/manifest.json b/docs/manifest.json index fe044da4bb441..cd07850834831 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -670,7 +670,7 @@ { "title": "Run AI Coding Agents in Coder", "description": "Learn how to run and integrate AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", - "path": "./coder-ai/README.md", + "path": "./coder-ai/index.md", "icon_path": "./images/icons/wand.svg", "state": ["early access"], "children": [ From 859dd2fc3fd144ef0ede4c6487aac90f4b3d974c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Thu, 10 Apr 2025 13:08:50 -0700 Subject: [PATCH 055/384] feat: add dynamic parameters websocket endpoint (#17165) --- archive/fs/tar.go | 5 +- coderd/apidoc/docs.go | 97 +- coderd/apidoc/swagger.json | 85 +- coderd/coderd.go | 7 + coderd/templateversions.go | 141 +- coderd/templateversions_test.go | 86 +- .../testdata/dynamicparameters/groups/main.tf | 25 + .../dynamicparameters/groups/plan.json | 92 + codersdk/client.go | 33 + codersdk/templateversions.go | 26 + codersdk/wsjson/decoder.go | 9 +- codersdk/wsjson/stream.go | 44 + docs/reference/api/schemas.md | 44 +- docs/reference/api/templates.md | 26 + go.mod | 121 +- go.sum | 1697 +++++++++++++++-- provisioner/echo/serve.go | 38 +- scripts/apitypings/main.go | 9 +- site/src/api/typesGenerated.ts | 53 + 19 files changed, 2291 insertions(+), 347 deletions(-) create mode 100644 coderd/testdata/dynamicparameters/groups/main.tf create mode 100644 coderd/testdata/dynamicparameters/groups/plan.json create mode 100644 codersdk/wsjson/stream.go diff --git a/archive/fs/tar.go b/archive/fs/tar.go index ab4027d5445ee..1a6f41937b9cb 100644 --- a/archive/fs/tar.go +++ b/archive/fs/tar.go @@ -9,9 +9,8 @@ import ( "github.com/spf13/afero/tarfs" ) +// FromTarReader creates a read-only in-memory FS func FromTarReader(r io.Reader) fs.FS { tr := tar.NewReader(r) - tfs := tarfs.New(tr) - rofs := afero.NewReadOnlyFs(tfs) - return afero.NewIOFS(rofs) + return afero.NewIOFS(tarfs.New(tr)) } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6bb177d699501..cb2f2f6c22e03 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5764,6 +5764,35 @@ const docTemplate = `{ } } }, + "/templateversions/{templateversion}/dynamic-parameters": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Templates" + ], + "summary": "Open dynamic parameters WebSocket by template version", + "operationId": "open-dynamic-parameters-websocket-by-template-version", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "templateversion", + "in": "path", + "required": true + } + ], + "responses": { + "101": { + "description": "Switching Protocols" + } + } + } + }, "/templateversions/{templateversion}/external-auth": { "get": { "security": [ @@ -11332,73 +11361,7 @@ const docTemplate = `{ } }, "codersdk.CreateTestAuditLogRequest": { - "type": "object", - "properties": { - "action": { - "enum": [ - "create", - "write", - "delete", - "start", - "stop" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.AuditAction" - } - ] - }, - "additional_fields": { - "type": "array", - "items": { - "type": "integer" - } - }, - "build_reason": { - "enum": [ - "autostart", - "autostop", - "initiator" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.BuildReason" - } - ] - }, - "organization_id": { - "type": "string", - "format": "uuid" - }, - "request_id": { - "type": "string", - "format": "uuid" - }, - "resource_id": { - "type": "string", - "format": "uuid" - }, - "resource_type": { - "enum": [ - "template", - "template_version", - "user", - "workspace", - "workspace_build", - "git_ssh_key", - "auditable_group" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.ResourceType" - } - ] - }, - "time": { - "type": "string", - "format": "date-time" - } - } + "type": "object" }, "codersdk.CreateTokenRequest": { "type": "object", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index de1d4e41c0673..90f5729654a95 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5097,6 +5097,33 @@ } } }, + "/templateversions/{templateversion}/dynamic-parameters": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Templates"], + "summary": "Open dynamic parameters WebSocket by template version", + "operationId": "open-dynamic-parameters-websocket-by-template-version", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "templateversion", + "in": "path", + "required": true + } + ], + "responses": { + "101": { + "description": "Switching Protocols" + } + } + } + }, "/templateversions/{templateversion}/external-auth": { "get": { "security": [ @@ -10100,63 +10127,7 @@ } }, "codersdk.CreateTestAuditLogRequest": { - "type": "object", - "properties": { - "action": { - "enum": ["create", "write", "delete", "start", "stop"], - "allOf": [ - { - "$ref": "#/definitions/codersdk.AuditAction" - } - ] - }, - "additional_fields": { - "type": "array", - "items": { - "type": "integer" - } - }, - "build_reason": { - "enum": ["autostart", "autostop", "initiator"], - "allOf": [ - { - "$ref": "#/definitions/codersdk.BuildReason" - } - ] - }, - "organization_id": { - "type": "string", - "format": "uuid" - }, - "request_id": { - "type": "string", - "format": "uuid" - }, - "resource_id": { - "type": "string", - "format": "uuid" - }, - "resource_type": { - "enum": [ - "template", - "template_version", - "user", - "workspace", - "workspace_build", - "git_ssh_key", - "auditable_group" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.ResourceType" - } - ] - }, - "time": { - "type": "string", - "format": "date-time" - } - } + "type": "object" }, "codersdk.CreateTokenRequest": { "type": "object", diff --git a/coderd/coderd.go b/coderd/coderd.go index 0434b9d9a17c4..43caf8b344edc 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -43,6 +43,7 @@ import ( "github.com/coder/coder/v2/coderd/cryptokeys" "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/runtimeconfig" "github.com/coder/coder/v2/coderd/webpush" @@ -557,6 +558,7 @@ func New(options *Options) *API { TemplateScheduleStore: options.TemplateScheduleStore, UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore, AccessControlStore: options.AccessControlStore, + FileCache: files.NewFromStore(options.Database), Experiments: experiments, WebpushDispatcher: options.WebPushDispatcher, healthCheckGroup: &singleflight.Group[string, *healthsdk.HealthcheckReport]{}, @@ -1096,6 +1098,10 @@ func New(options *Options) *API { // The idea is to return an empty [], so that the coder CLI won't get blocked accidentally. r.Get("/schema", templateVersionSchemaDeprecated) r.Get("/parameters", templateVersionParametersDeprecated) + r.Group(func(r chi.Router) { + r.Use(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentDynamicParameters)) + r.Get("/dynamic-parameters", api.templateVersionDynamicParameters) + }) r.Get("/rich-parameters", api.templateVersionRichParameters) r.Get("/external-auth", api.templateVersionExternalAuth) r.Get("/variables", api.templateVersionVariables) @@ -1545,6 +1551,7 @@ type API struct { // passed to dbauthz. AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore] PortSharer atomic.Pointer[portsharing.PortSharer] + FileCache files.Cache UpdatesProvider tailnet.WorkspaceUpdatesProvider diff --git a/coderd/templateversions.go b/coderd/templateversions.go index a12082e11d717..a60897ddb725a 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -35,10 +35,14 @@ import ( "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/wsjson" "github.com/coder/coder/v2/examples" "github.com/coder/coder/v2/provisioner/terraform/tfparse" "github.com/coder/coder/v2/provisionersdk" sdkproto "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/preview" + previewtypes "github.com/coder/preview/types" + "github.com/coder/websocket" ) // @Summary Get template version by ID @@ -266,6 +270,135 @@ func (api *API) patchCancelTemplateVersion(rw http.ResponseWriter, r *http.Reque }) } +// @Summary Open dynamic parameters WebSocket by template version +// @ID open-dynamic-parameters-websocket-by-template-version +// @Security CoderSessionToken +// @Tags Templates +// @Param templateversion path string true "Template version ID" format(uuid) +// @Success 101 +// @Router /templateversions/{templateversion}/dynamic-parameters [get] +func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + templateVersion := httpmw.TemplateVersionParam(r) + + // Check that the job has completed successfully + job, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching provisioner job.", + Detail: err.Error(), + }) + return + } + if !job.CompletedAt.Valid { + httpapi.Write(ctx, rw, http.StatusTooEarly, codersdk.Response{ + Message: "Template version job has not finished", + }) + return + } + + // Having the Terraform plan available for the evaluation engine is helpful + // for populating values from data blocks, but isn't strictly required. If + // we don't have a cached plan available, we just use an empty one instead. + plan := json.RawMessage("{}") + tf, err := api.Database.GetTemplateVersionTerraformValues(ctx, templateVersion.ID) + if err == nil { + plan = tf.CachedPlan + } + + input := preview.Input{ + PlanJSON: plan, + ParameterValues: map[string]string{}, + // TODO: write a db query that fetches all of the data needed to fill out + // this owner value + Owner: previewtypes.WorkspaceOwner{ + Groups: []string{"Everyone"}, + }, + } + + // nolint:gocritic // We need to fetch the templates files for the Terraform + // evaluator, and the user likely does not have permission. + fileCtx := dbauthz.AsProvisionerd(ctx) + fileID, err := api.Database.GetFileIDByTemplateVersionID(fileCtx, templateVersion.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error finding template version Terraform.", + Detail: err.Error(), + }) + return + } + + fs, err := api.FileCache.Acquire(fileCtx, fileID) + defer api.FileCache.Release(fileID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ + Message: "Internal error fetching template version Terraform.", + Detail: err.Error(), + }) + return + } + + conn, err := websocket.Accept(rw, r, nil) + if err != nil { + httpapi.Write(ctx, rw, http.StatusUpgradeRequired, codersdk.Response{ + Message: "Failed to accept WebSocket.", + Detail: err.Error(), + }) + return + } + + stream := wsjson.NewStream[codersdk.DynamicParametersRequest, codersdk.DynamicParametersResponse](conn, websocket.MessageText, websocket.MessageText, api.Logger) + + // Send an initial form state, computed without any user input. + result, diagnostics := preview.Preview(ctx, input, fs) + response := codersdk.DynamicParametersResponse{ + ID: -1, + Diagnostics: previewtypes.Diagnostics(diagnostics), + } + if result != nil { + response.Parameters = result.Parameters + } + err = stream.Send(response) + if err != nil { + stream.Drop() + return + } + + // As the user types into the form, reprocess the state using their input, + // and respond with updates. + updates := stream.Chan() + for { + select { + case <-ctx.Done(): + stream.Close(websocket.StatusGoingAway) + return + case update, ok := <-updates: + if !ok { + // The connection has been closed, so there is no one to write to + return + } + input.ParameterValues = update.Inputs + result, diagnostics := preview.Preview(ctx, input, fs) + response := codersdk.DynamicParametersResponse{ + ID: update.ID, + Diagnostics: previewtypes.Diagnostics(diagnostics), + } + if result != nil { + response.Parameters = result.Parameters + } + err = stream.Send(response) + if err != nil { + stream.Drop() + return + } + } + } +} + // @Summary Get rich parameters by template version // @ID get-rich-parameters-by-template-version // @Security CoderSessionToken @@ -287,8 +420,8 @@ func (api *API) templateVersionRichParameters(rw http.ResponseWriter, r *http.Re return } if !job.CompletedAt.Valid { - httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ - Message: "Job hasn't completed!", + httpapi.Write(ctx, rw, http.StatusTooEarly, codersdk.Response{ + Message: "Template version job has not finished", }) return } @@ -428,7 +561,7 @@ func (api *API) templateVersionVariables(rw http.ResponseWriter, r *http.Request } if !job.CompletedAt.Valid { httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{ - Message: "Job hasn't completed!", + Message: "Template version job has not finished", }) return } @@ -483,7 +616,7 @@ func (api *API) postTemplateVersionDryRun(rw http.ResponseWriter, r *http.Reques return } if !job.CompletedAt.Valid { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + httpapi.Write(ctx, rw, http.StatusTooEarly, codersdk.Response{ Message: "Template version import job hasn't completed!", }) return diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 433441fdd4cf9..4fe4550dd6806 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "net/http" + "os" "regexp" "strings" "testing" @@ -27,6 +28,7 @@ import ( "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" + "github.com/coder/websocket" ) func TestTemplateVersion(t *testing.T) { @@ -1207,7 +1209,7 @@ func TestTemplateVersionDryRun(t *testing.T) { _, err := client.CreateTemplateVersionDryRun(ctx, version.ID, codersdk.CreateTemplateVersionDryRunRequest{}) var apiErr *codersdk.Error require.ErrorAs(t, err, &apiErr) - require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + require.Equal(t, http.StatusTooEarly, apiErr.StatusCode()) }) t.Run("Cancel", func(t *testing.T) { @@ -2056,11 +2058,7 @@ func TestTemplateArchiveVersions(t *testing.T) { // Create some unused versions for i := 0; i < 2; i++ { - unused := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: echo.PlanComplete, - ProvisionApply: echo.ApplyComplete, - }, func(req *codersdk.CreateTemplateVersionRequest) { + unused := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(req *codersdk.CreateTemplateVersionRequest) { req.TemplateID = template.ID }) expArchived = append(expArchived, unused.ID) @@ -2069,11 +2067,7 @@ func TestTemplateArchiveVersions(t *testing.T) { // Create some used template versions for i := 0; i < 2; i++ { - used := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: echo.PlanComplete, - ProvisionApply: echo.ApplyComplete, - }, func(req *codersdk.CreateTemplateVersionRequest) { + used := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(req *codersdk.CreateTemplateVersionRequest) { req.TemplateID = template.ID }) coderdtest.AwaitTemplateVersionJobCompleted(t, client, used.ID) @@ -2140,3 +2134,73 @@ func TestTemplateArchiveVersions(t *testing.T) { require.NoError(t, err, "fetch all versions") require.Len(t, remaining, totalVersions-len(expArchived)-len(allFailed)+1, "remaining versions") } + +func TestTemplateVersionDynamicParameters(t *testing.T) { + t.Parallel() + + cfg := coderdtest.DeploymentValues(t) + cfg.Experiments = []string{string(codersdk.ExperimentDynamicParameters)} + ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: cfg}) + owner := coderdtest.CreateFirstUser(t, ownerClient) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + dynamicParametersTerraformSource, err := os.ReadFile("testdata/dynamicparameters/groups/main.tf") + require.NoError(t, err) + dynamicParametersTerraformPlan, err := os.ReadFile("testdata/dynamicparameters/groups/plan.json") + require.NoError(t, err) + + files := echo.WithExtraFiles(map[string][]byte{ + "main.tf": dynamicParametersTerraformSource, + }) + files.ProvisionPlan = []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Plan: dynamicParametersTerraformPlan, + }, + }, + }} + + version := coderdtest.CreateTemplateVersion(t, templateAdmin, owner.OrganizationID, files) + coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID) + _ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) + + ctx := testutil.Context(t, testutil.WaitShort) + stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID) + require.NoError(t, err) + defer stream.Close(websocket.StatusGoingAway) + + previews := stream.Chan() + + // Should automatically send a form state with all defaulted/empty values + preview := testutil.RequireRecvCtx(ctx, t, previews) + require.Empty(t, preview.Diagnostics) + require.Equal(t, "group", preview.Parameters[0].Name) + require.True(t, preview.Parameters[0].Value.Valid()) + require.Equal(t, "Everyone", preview.Parameters[0].Value.Value.AsString()) + + // Send a new value, and see it reflected + err = stream.Send(codersdk.DynamicParametersRequest{ + ID: 1, + Inputs: map[string]string{"group": "Bloob"}, + }) + require.NoError(t, err) + preview = testutil.RequireRecvCtx(ctx, t, previews) + require.Equal(t, 1, preview.ID) + require.Empty(t, preview.Diagnostics) + require.Equal(t, "group", preview.Parameters[0].Name) + require.True(t, preview.Parameters[0].Value.Valid()) + require.Equal(t, "Bloob", preview.Parameters[0].Value.Value.AsString()) + + // Back to default + err = stream.Send(codersdk.DynamicParametersRequest{ + ID: 3, + Inputs: map[string]string{}, + }) + require.NoError(t, err) + preview = testutil.RequireRecvCtx(ctx, t, previews) + require.Equal(t, 3, preview.ID) + require.Empty(t, preview.Diagnostics) + require.Equal(t, "group", preview.Parameters[0].Name) + require.True(t, preview.Parameters[0].Value.Valid()) + require.Equal(t, "Everyone", preview.Parameters[0].Value.Value.AsString()) +} diff --git a/coderd/testdata/dynamicparameters/groups/main.tf b/coderd/testdata/dynamicparameters/groups/main.tf new file mode 100644 index 0000000000000..a69b0463bb653 --- /dev/null +++ b/coderd/testdata/dynamicparameters/groups/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} + +data "coder_workspace_owner" "me" {} + +output "groups" { + value = data.coder_workspace_owner.me.groups +} + +data "coder_parameter" "group" { + name = "group" + default = try(data.coder_workspace_owner.me.groups[0], "") + dynamic "option" { + for_each = data.coder_workspace_owner.me.groups + content { + name = option.value + value = option.value + } + } +} diff --git a/coderd/testdata/dynamicparameters/groups/plan.json b/coderd/testdata/dynamicparameters/groups/plan.json new file mode 100644 index 0000000000000..8242f0dc43c58 --- /dev/null +++ b/coderd/testdata/dynamicparameters/groups/plan.json @@ -0,0 +1,92 @@ +{ + "terraform_version": "1.11.2", + "format_version": "1.2", + "checks": [], + "complete": true, + "timestamp": "2025-04-02T01:29:59Z", + "variables": {}, + "prior_state": { + "values": { + "root_module": { + "resources": [ + { + "mode": "data", + "name": "me", + "type": "coder_workspace_owner", + "address": "data.coder_workspace_owner.me", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "id": "25e81ec3-0eb9-4ee3-8b6d-738b8552f7a9", + "name": "default", + "email": "default@example.com", + "groups": [], + "full_name": "default", + "login_type": null, + "rbac_roles": [], + "session_token": "", + "ssh_public_key": "", + "ssh_private_key": "", + "oidc_access_token": "" + }, + "sensitive_values": { + "groups": [], + "rbac_roles": [], + "ssh_private_key": true + } + } + ], + "child_modules": [] + } + }, + "format_version": "1.0", + "terraform_version": "1.11.2" + }, + "configuration": { + "root_module": { + "resources": [ + { + "mode": "data", + "name": "me", + "type": "coder_workspace_owner", + "address": "data.coder_workspace_owner.me", + "schema_version": 0, + "provider_config_key": "coder" + } + ], + "variables": {}, + "module_calls": {} + }, + "provider_config": { + "coder": { + "name": "coder", + "full_name": "registry.terraform.io/coder/coder" + } + } + }, + "planned_values": { + "root_module": { + "resources": [], + "child_modules": [] + } + }, + "resource_changes": [], + "relevant_attributes": [ + { + "resource": "data.coder_workspace_owner.me", + "attribute": ["full_name"] + }, + { + "resource": "data.coder_workspace_owner.me", + "attribute": ["email"] + }, + { + "resource": "data.coder_workspace_owner.me", + "attribute": ["id"] + }, + { + "resource": "data.coder_workspace_owner.me", + "attribute": ["name"] + } + ] +} diff --git a/codersdk/client.go b/codersdk/client.go index 8a341ee742a76..8ab5a289b2cf5 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -21,6 +21,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/tracing" + "github.com/coder/websocket" "cdr.dev/slog" ) @@ -336,6 +337,38 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return resp, err } +func (c *Client) Dial(ctx context.Context, path string, opts *websocket.DialOptions) (*websocket.Conn, error) { + u, err := c.URL.Parse(path) + if err != nil { + return nil, err + } + + tokenHeader := c.SessionTokenHeader + if tokenHeader == "" { + tokenHeader = SessionTokenHeader + } + + if opts == nil { + opts = &websocket.DialOptions{} + } + if opts.HTTPHeader == nil { + opts.HTTPHeader = http.Header{} + } + if opts.HTTPHeader.Get("tokenHeader") == "" { + opts.HTTPHeader.Set(tokenHeader, c.SessionToken()) + } + + conn, resp, err := websocket.Dial(ctx, u.String(), opts) + if resp.Body != nil { + resp.Body.Close() + } + if err != nil { + return nil, err + } + + return conn, nil +} + // ExpectJSONMime is a helper function that will assert the content type // of the response is application/json. func ExpectJSONMime(res *http.Response) error { diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go index de8bb7b970957..e21991d0e98f3 100644 --- a/codersdk/templateversions.go +++ b/codersdk/templateversions.go @@ -9,6 +9,10 @@ import ( "time" "github.com/google/uuid" + + "github.com/coder/coder/v2/codersdk/wsjson" + previewtypes "github.com/coder/preview/types" + "github.com/coder/websocket" ) type TemplateVersionWarning string @@ -123,6 +127,28 @@ func (c *Client) CancelTemplateVersion(ctx context.Context, version uuid.UUID) e return nil } +type DynamicParametersRequest struct { + // ID identifies the request. The response contains the same + // ID so that the client can match it to the request. + ID int `json:"id"` + Inputs map[string]string `json:"inputs"` +} + +type DynamicParametersResponse struct { + ID int `json:"id"` + Diagnostics previewtypes.Diagnostics `json:"diagnostics"` + Parameters []previewtypes.Parameter `json:"parameters"` + // TODO: Workspace tags +} + +func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) { + conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/templateversions/%s/dynamic-parameters", version), nil) + if err != nil { + return nil, err + } + return wsjson.NewStream[DynamicParametersResponse, DynamicParametersRequest](conn, websocket.MessageText, websocket.MessageText, c.Logger()), nil +} + // TemplateVersionParameters returns parameters a template version exposes. func (c *Client) TemplateVersionRichParameters(ctx context.Context, version uuid.UUID) ([]TemplateVersionParameter, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/rich-parameters", version), nil) diff --git a/codersdk/wsjson/decoder.go b/codersdk/wsjson/decoder.go index 49f418d8b4177..9e05cb5b3585d 100644 --- a/codersdk/wsjson/decoder.go +++ b/codersdk/wsjson/decoder.go @@ -18,9 +18,12 @@ type Decoder[T any] struct { logger slog.Logger } -// Chan starts the decoder reading from the websocket and returns a channel for reading the -// resulting values. The chan T is closed if the underlying websocket is closed, or we encounter an -// error. We also close the underlying websocket if we encounter an error reading or decoding. +// Chan returns a `chan` that you can read incoming messages from. The returned +// `chan` will be closed when the WebSocket connection is closed. If there is an +// error reading from the WebSocket or decoding a value the WebSocket will be +// closed. +// +// Safety: Chan must only be called once. Successive calls will panic. func (d *Decoder[T]) Chan() <-chan T { if !d.chanCalled.CompareAndSwap(false, true) { panic("chan called more than once") diff --git a/codersdk/wsjson/stream.go b/codersdk/wsjson/stream.go new file mode 100644 index 0000000000000..8fb73adb771bd --- /dev/null +++ b/codersdk/wsjson/stream.go @@ -0,0 +1,44 @@ +package wsjson + +import ( + "cdr.dev/slog" + "github.com/coder/websocket" +) + +// Stream is a two-way messaging interface over a WebSocket connection. +type Stream[R any, W any] struct { + conn *websocket.Conn + r *Decoder[R] + w *Encoder[W] +} + +func NewStream[R any, W any](conn *websocket.Conn, readType, writeType websocket.MessageType, logger slog.Logger) *Stream[R, W] { + return &Stream[R, W]{ + conn: conn, + r: NewDecoder[R](conn, readType, logger), + // We intentionally don't call `NewEncoder` because it calls `CloseRead`. + w: &Encoder[W]{conn: conn, typ: writeType}, + } +} + +// Chan returns a `chan` that you can read incoming messages from. The returned +// `chan` will be closed when the WebSocket connection is closed. If there is an +// error reading from the WebSocket or decoding a value the WebSocket will be +// closed. +// +// Safety: Chan must only be called once. Successive calls will panic. +func (s *Stream[R, W]) Chan() <-chan R { + return s.r.Chan() +} + +func (s *Stream[R, W]) Send(v W) error { + return s.w.Encode(v) +} + +func (s *Stream[R, W]) Close(c websocket.StatusCode) error { + return s.conn.Close(c, "") +} + +func (s *Stream[R, W]) Drop() { + _ = s.conn.Close(websocket.StatusInternalError, "dropping connection") +} diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 8d38d0c4e346b..6e7a2da1a3dea 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1334,52 +1334,12 @@ This is required on creation to enable a user-flow of validating a template work ## codersdk.CreateTestAuditLogRequest ```json -{ - "action": "create", - "additional_fields": [ - 0 - ], - "build_reason": "autostart", - "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "request_id": "266ea41d-adf5-480b-af50-15b940c2b846", - "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", - "resource_type": "template", - "time": "2019-08-24T14:15:22Z" -} +{} ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|---------------------|------------------------------------------------|----------|--------------|-------------| -| `action` | [codersdk.AuditAction](#codersdkauditaction) | false | | | -| `additional_fields` | array of integer | false | | | -| `build_reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | -| `organization_id` | string | false | | | -| `request_id` | string | false | | | -| `resource_id` | string | false | | | -| `resource_type` | [codersdk.ResourceType](#codersdkresourcetype) | false | | | -| `time` | string | false | | | - -#### Enumerated Values - -| Property | Value | -|-----------------|--------------------| -| `action` | `create` | -| `action` | `write` | -| `action` | `delete` | -| `action` | `start` | -| `action` | `stop` | -| `build_reason` | `autostart` | -| `build_reason` | `autostop` | -| `build_reason` | `initiator` | -| `resource_type` | `template` | -| `resource_type` | `template_version` | -| `resource_type` | `user` | -| `resource_type` | `workspace` | -| `resource_type` | `workspace_build` | -| `resource_type` | `git_ssh_key` | -| `resource_type` | `auditable_group` | +None ## codersdk.CreateTokenRequest diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index b644affbbfc88..f48a9482fa695 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -2541,6 +2541,32 @@ Status Code **200** To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Open dynamic parameters WebSocket by template version + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/dynamic-parameters \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /templateversions/{templateversion}/dynamic-parameters` + +### Parameters + +| Name | In | Type | Required | Description | +|-------------------|------|--------------|----------|---------------------| +| `templateversion` | path | string(uuid) | true | Template version ID | + +### Responses + +| Status | Meaning | Description | Schema | +|--------|--------------------------------------------------------------------------|---------------------|--------| +| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get external auth by template version ### Code samples diff --git a/go.mod b/go.mod index 56fdd053f407e..572829b3013f6 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,14 @@ replace github.com/lib/pq => github.com/coder/pq v1.10.5-0.20240813183442-0c420c // used in conjunction with agent-exec. See https://github.com/coder/coder/pull/15817 replace github.com/charmbracelet/bubbletea => github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 +// Trivy has some issues that we're floating patches for, and will hopefully +// be upstreamed eventually. +replace github.com/aquasecurity/trivy => github.com/emyrk/trivy v0.0.0-20250320190949-47caa1ac2d53 + +// afero/tarfs has a bug that breaks our usage. A PR has been submitted upstream. +// https://github.com/spf13/afero/pull/487 +replace github.com/spf13/afero => github.com/aslilac/afero v0.0.0-20250403163713-f06e86036696 + require ( cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb cloud.google.com/go/compute/metadata v0.6.0 @@ -74,10 +82,10 @@ require ( github.com/aquasecurity/trivy-iac v0.8.0 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/awalterschulze/gographviz v2.0.3+incompatible - github.com/aws/smithy-go v1.22.2 + github.com/aws/smithy-go v1.22.3 github.com/bgentry/speakeasy v0.2.0 github.com/bramvdbogaerde/go-scp v1.5.0 - github.com/briandowns/spinner v1.18.1 + github.com/briandowns/spinner v1.23.0 github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 github.com/cenkalti/backoff/v4 v4.3.0 github.com/cespare/xxhash/v2 v2.3.0 @@ -94,8 +102,8 @@ require ( github.com/coder/quartz v0.1.2 github.com/coder/retry v1.5.1 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e - github.com/coder/websocket v1.8.12 + github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0 + 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 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf @@ -137,7 +145,7 @@ require ( github.com/hashicorp/yamux v0.1.2 github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 github.com/imulab/go-scim/pkg/v2 v2.2.0 - github.com/jedib0t/go-pretty/v6 v6.6.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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 @@ -161,7 +169,7 @@ require ( github.com/prometheus/client_golang v1.21.1 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.63.0 - github.com/quasilyte/go-ruleguard/dsl v0.3.21 + github.com/quasilyte/go-ruleguard/dsl v0.3.22 github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil/v4 v4.25.2 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 @@ -189,7 +197,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-20240808152545-0cdaa3abc0fa + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 golang.org/x/mod v0.24.0 golang.org/x/net v0.38.0 golang.org/x/oauth2 v0.29.0 @@ -216,7 +224,7 @@ require ( cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/logging v1.12.0 // indirect cloud.google.com/go/longrunning v0.6.2 // indirect - dario.cat/mergo v1.0.0 // indirect + dario.cat/mergo v1.0.1 // indirect 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 @@ -237,7 +245,7 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/ProtonMail/go-crypto v1.1.3 // indirect + github.com/ProtonMail/go-crypto v1.1.5 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/akutz/memconn v0.1.0 // indirect @@ -248,37 +256,37 @@ require ( github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aws/aws-sdk-go-v2 v1.36.0 - github.com/aws/aws-sdk-go-v2/config v1.29.1 - github.com/aws/aws-sdk-go-v2/credentials v1.17.54 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.3 + github.com/aws/aws-sdk-go-v2/config v1.29.9 + github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.1 - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bep/godartsass/v2 v2.3.2 // indirect github.com/bep/golibsass v1.2.0 // indirect - github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/chromedp/sysutil v1.1.0 // indirect github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect github.com/clbanning/mxj/v2 v2.7.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect + github.com/cloudflare/circl v1.6.0 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/coreos/go-iptables v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/docker/cli v27.4.1+incompatible // indirect - github.com/docker/docker v27.2.0+incompatible // indirect + github.com/docker/cli v27.5.0+incompatible // indirect + github.com/docker/docker v27.5.0+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd // indirect @@ -288,16 +296,16 @@ require ( github.com/elastic/go-windows v1.0.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.4.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-chi/hostrouter v0.2.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-openapi/jsonpointer v0.20.2 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/spec v0.20.6 // indirect - github.com/go-openapi/swag v0.22.8 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect @@ -311,12 +319,12 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gohugoio/hashstructure v0.3.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect 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-20230817174616-7a8ec2ada47b // indirect + github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // 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 @@ -334,7 +342,7 @@ require ( github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/go-terraform-address v0.0.0-20240523040243-ccea9d309e0c github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/hcl v1.0.1-vault-5 // indirect + github.com/hashicorp/hcl v1.0.1-vault-7 // indirect github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect @@ -343,7 +351,7 @@ require ( github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect github.com/jsimonetti/rtnetlink v1.3.5 // indirect @@ -353,9 +361,9 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect + github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect @@ -391,7 +399,7 @@ require ( github.com/pion/transport/v3 v3.0.7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/riandyrn/otelchi v0.5.1 // indirect @@ -399,7 +407,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect - github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect + github.com/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 @@ -420,8 +428,8 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tinylib/msgp v1.2.1 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.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 @@ -479,11 +487,40 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect ) -require github.com/mark3labs/mcp-go v0.17.0 +require ( + github.com/coder/preview v0.0.0-20250409162646-62939c63c71a + github.com/mark3labs/mcp-go v0.17.0 +) require ( + cel.dev/expr v0.19.1 // indirect + cloud.google.com/go v0.116.0 // indirect + cloud.google.com/go/iam v1.2.2 // indirect + cloud.google.com/go/monitoring v1.21.2 // indirect + cloud.google.com/go/storage v1.49.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect + github.com/aquasecurity/go-version v0.0.1 // indirect + github.com/aquasecurity/trivy v0.58.2 // indirect + github.com/aws/aws-sdk-go v1.55.6 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/hashicorp/go-getter v1.7.8 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/liamg/memoryfs v1.6.0 // indirect github.com/moby/sys/user v0.3.0 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/samber/lo v1.49.1 // indirect + github.com/ulikunitz/xz v0.5.12 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect ) diff --git a/go.sum b/go.sum index ca3e4d2caedf3..978d77dc95289 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,633 @@ cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb h1:4MKA8lBQLnCqj2myJCb5Lzoa65y0tABO4gHrxuMdsCQ= cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ= +cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= +cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= +cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk= cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU= +cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storage v1.49.0 h1:zenOPBOWHCnojRd9aJZAyQXBYqkJkdQS42dxL55CIMw= +cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5PiwehZI9qGU= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/trace v1.11.2 h1:4ZmaBdL8Ng/ajrgKqY5jfvzqMXbrDcBsUGXOT9aqTtI= +cloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= 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= @@ -52,18 +660,28 @@ github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 h1:fKv05 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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1/go.mod h1:0wEl7vrAD8mehJyohS9HZy+WyEOaQO2mJx86Cvh93kM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= -github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= +github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -74,25 +692,42 @@ github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7l github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A= github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw= github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/ammario/tlru v0.4.0 h1:sJ80I0swN3KOX2YxC6w8FbCqpQucWdbb+J36C05FPuU= github.com/ammario/tlru v0.4.0/go.mod h1:aYzRFu0XLo4KavE9W8Lx7tzjkX+pAApz+NgcKYIFUBQ= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/aquasecurity/go-version v0.0.1 h1:4cNl516agK0TCn5F7mmYN+xVs1E3S45LkgZk3cbaW2E= +github.com/aquasecurity/go-version v0.0.1/go.mod h1:s1UU6/v2hctXcOa3OLwfj5d9yoXHa3ahf+ipSwEvGT0= +github.com/aquasecurity/iamgo v0.0.10 h1:t/HG/MI1eSephztDc+Rzh/YfgEa+NqgYRSfr6pHdSCQ= +github.com/aquasecurity/iamgo v0.0.10/go.mod h1:GI9IQJL2a+C+V2+i3vcwnNKuIJXZ+HAfqxZytwy+cPk= +github.com/aquasecurity/jfather v0.0.8 h1:tUjPoLGdlkJU0qE7dSzd1MHk2nQFNPR0ZfF+6shaExE= +github.com/aquasecurity/jfather v0.0.8/go.mod h1:Ag+L/KuR/f8vn8okUi8Wc1d7u8yOpi2QTaGX10h71oY= github.com/aquasecurity/trivy-iac v0.8.0 h1:NKFhk/BTwQ0jIh4t74V8+6UIGUvPlaxO9HPlSMQi3fo= github.com/aquasecurity/trivy-iac v0.8.0/go.mod h1:ARiMeNqcaVWOXJmp8hmtMnNm/Jd836IOmDBUW5r4KEk= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= @@ -102,40 +737,45 @@ github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hC github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aslilac/afero v0.0.0-20250403163713-f06e86036696 h1:7hAl/81gNUjmSCqJYKe1aTIVY4myjapaSALdCko19tI= +github.com/aslilac/afero v0.0.0-20250403163713-f06e86036696/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= -github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk= -github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= -github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ= -github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk= -github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI= -github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= +github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= +github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.1 h1:yg6nrV33ljY6CppoRnnsKLqIZ5ExNdQOGRBGNfc56Yw= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.1/go.mod h1:hGdIV5nndhIclFFvI1apVfQWn9ZKqedykZ1CtLZd03E= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28/go.mod h1:kGlXVIWDfvt2Ox5zEaNglmq0hXPHgQFNMix33Tw22jA= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 h1:hgSBvRT7JEWx2+vEGI9/Ld5rZtl7M5lu8PqdvOmbRHw= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4/go.mod h1:v7NIzEFIHBiicOMaMTuEmbnzGnqW0d+6ulNALul6fYE= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I= -github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= +github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= @@ -168,13 +808,17 @@ github.com/bep/overlayfs v0.9.2 h1:qJEmFInsW12L7WW7dOTUhnMfyk/fN9OCDEO5Gr8HSDs= github.com/bep/overlayfs v0.9.2/go.mod h1:aYY9W7aXQsGcA7V9x/pzeR8LjEgIxbtisZm8Q7zPz40= github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= -github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= +github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM= github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= @@ -183,7 +827,12 @@ github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwP github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= @@ -202,12 +851,15 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAM github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs= github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0= github.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok= @@ -216,14 +868,31 @@ github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyM github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= +github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwuWwPHPYoCZ/KLAjHv5g4h2MS4f2/MTI= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4= github.com/coder/clistat v1.0.0 h1:MjiS7qQ1IobuSSgDnxcCSyBPESs44hExnh2TEqMcGnA= github.com/coder/clistat v1.0.0/go.mod h1:F+gLef+F9chVrleq808RBxdaoq52R4VLopuLdAsh8Y4= github.com/coder/flog v1.1.0 h1:kbAes1ai8fIS5OeV+QAnKBQE22ty1jRF/mcAwHpLBa4= github.com/coder/flog v1.1.0/go.mod h1:UQlQvrkJBvnRGo69Le8E24Tcl5SJleAAR7gYEHzAmdQ= +github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322 h1:m0lPZjlQ7vdVpRBPKfYIFlmgevoTkBxB10wv6l2gOaU= github.com/coder/go-httpstat v0.0.0-20230801153223-321c88088322/go.mod h1:rOLFDDVKVFiDqZFXoteXc97YXx7kFi9kYqR+2ETPkLQ= github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136 h1:0RgB61LcNs24WOxc3PBvygSNTQurm0PYPujJjLLOzs0= @@ -234,6 +903,8 @@ github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048 h1:3jzYUlGH7ZELIH4XggX github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= +github.com/coder/preview v0.0.0-20250409162646-62939c63c71a h1:1fvDm7hpNwKDQhHpStp7p1W05/33nBwptGorugNaE94= +github.com/coder/preview v0.0.0-20250409162646-62939c63c71a/go.mod h1:H9BInar+i5VALTTQ9Ulxmn94Eo2fWEhoxd0S9WakDIs= github.com/coder/quartz v0.1.2 h1:PVhc9sJimTdKd3VbygXtS4826EOCpB1fXoRlLnCrE+s= github.com/coder/quartz v0.1.2/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA= github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc= @@ -246,25 +917,33 @@ github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a h1:18TQ03KlYrkW8 github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0= github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI= -github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e h1:coy2k2X/d+bGys9wUqQn/TR/0xBibiOIX6vZzPSVGso= -github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e/go.mod h1:X28s3rz+aEM5PkBKvk3xcUrQFO2eNPjzRChUg9wb70U= -github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= -github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0 h1:NPt2+FVr+2QJoxrta5ZwyTaxocWMEKdh2WpIumffxfM= +github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0/go.mod h1:X28s3rz+aEM5PkBKvk3xcUrQFO2eNPjzRChUg9wb70U= +github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE= +github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Auuw3YOiSyLNHkdMtyCZHPFBx7syN4rk= github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0/go.mod h1:qANbdpqyAGlo2bg+4gQKPj24H1ZWa3bQU2Q5/bV5B3Y= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818 h1:bNhUTaKl3q0bFn78bBRq7iIwo72kNTvUD9Ll5TTzDDk= github.com/coder/wireguard-go v0.0.0-20240522052547-769cdd7f7818/go.mod h1:fAlLM6hUgnf4Sagxn2Uy5Us0PBgOYWz+63HwHUVGEbw= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= +github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/dave/dst v0.27.2 h1:4Y5VFTkhGLC1oddtNwuxxe36pnyLxMFXT51FOzH8Ekc= github.com/dave/dst v0.27.2/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/dave/jennifer v1.6.1 h1:T4T/67t6RAA5AIV6+NP8Uk/BIsXgDoqEowgycdQQLuk= @@ -292,14 +971,15 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= -github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= -github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= +github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U= +github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd h1:QMSNEh9uQkDjyPwu/J541GgSH+4hw+0skJDIj9HJ3mE= github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -317,6 +997,33 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1X github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-smtp v0.21.2 h1:OLDgvZKuofk4em9fT5tFG5j4jE1/hXnX75UMvcrL4AA= github.com/emersion/go-smtp v0.21.2/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/emyrk/trivy v0.0.0-20250320190949-47caa1ac2d53 h1:0bj1/UEj/7ZwQSm2EAYuYd87feUvqmlrUfR3MRzKOag= +github.com/emyrk/trivy v0.0.0-20250320190949-47caa1ac2d53/go.mod h1:QqQijstmQF9wfPij09KE96MLfbFGtfC21dG299ty+Fc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/evanw/esbuild v0.24.2 h1:PQExybVBrjHjN6/JJiShRGIXh1hWVm6NepVnhZhrt0A= @@ -334,6 +1041,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fergusstrange/embedded-postgres v1.30.0 h1:ewv1e6bBlqOIYtgGgRcEnNDpfGlmfPxB8T3PO9tV68Q= github.com/fergusstrange/embedded-postgres v1.30.0/go.mod h1:w0YvnCgf19o6tskInrOOACtnqfVlOvluz3hlNLY7tRk= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= @@ -345,8 +1054,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= -github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a h1:fwNLHrP5Rbg/mGSXCjtPdpbqv2GucVTA/KMi8wEm6mE= @@ -367,12 +1076,28 @@ github.com/go-chi/hostrouter v0.2.0 h1:GwC7TZz8+SlJN/tV/aeJgx4F+mI5+sp+5H1PelQUj github.com/go-chi/hostrouter v0.2.0/go.mod h1:pJ49vWVmtsKRKZivQx0YMYv4h0aX+Gcn6V23Np9Wf1s= github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g= github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= +github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w= github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -381,23 +1106,19 @@ github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4 github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= -github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -426,10 +1147,13 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY= @@ -455,24 +1179,67 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +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/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= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 h1:4txT5G2kqVAKMjzidIabL/8KqjIK71yj30YOeuxLn10= github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -487,24 +1254,69 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8= github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +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/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= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo= @@ -518,6 +1330,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= +github.com/hashicorp/go-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= @@ -529,6 +1343,8 @@ github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b h1:3GrpnZQBxcMj1 github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b/go.mod h1:qIFzeFcJU3OIFk/7JreWXcUjFmcCaeHTH9KoNyHYVCs= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= @@ -540,15 +1356,17 @@ github.com/hashicorp/go-terraform-address v0.0.0-20240523040243-ccea9d309e0c h1: github.com/hashicorp/go-terraform-address v0.0.0-20240523040243-ccea9d309e0c/go.mod h1:xoy1vl2+4YvqSQEkKcFjNYxTk7cll+o1f1t2wxnHIX8= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hc-install v0.9.1 h1:gkqTfE3vVbafGQo6VZXcy2v5yoz2bE0+nhZXruCuODQ= github.com/hashicorp/hc-install v0.9.1/go.mod h1:pWWvN/IrfeBK4XPeXXYkL6EjMufHkCK5DvwxeLKuBf0= -github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= -github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= @@ -579,19 +1397,25 @@ github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1 h1:jWoR2Yqg8tzM0v github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= -github.com/jedib0t/go-pretty/v6 v6.6.0 h1:wmZVuAcEkZRT+Aq1xXpE8IGat4vE5WXOMmBpbQqERXw= -github.com/jedib0t/go-pretty/v6 v6.6.0/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= +github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= @@ -604,16 +1428,26 @@ github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9 github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +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/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= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU= github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= @@ -622,6 +1456,7 @@ github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -630,6 +1465,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3 h1:Z9/bo5PSeMutpdiKYNt/TTSfGM1Ll0naj3QzYX9VxTc= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= +github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA= +github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8/go.mod h1:n/KX1BZoN1m9EwoXkn/xAV4fd3k8c++gGBsgLONaPOY= github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e h1:OP0ZMFeZkUnOzTFRfpuK3m7Kp4fNvC6qN+exwj7aI4M= github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -640,14 +1478,18 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY= -github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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= +github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= @@ -660,8 +1502,8 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO 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= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -671,9 +1513,11 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= @@ -688,6 +1532,8 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -709,8 +1555,14 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/moby v28.0.0+incompatible h1:D+F1Z56b/DS8J5pUkTG/stemqrvHBQ006hUqJxjV9P0= github.com/moby/moby v28.0.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/mocktools/go-smtp-mock/v2 v2.4.0 h1:u0ky0iyNW/LEMKAFRTsDivHyP8dHYxe/cV3FZC3rRjo= @@ -738,9 +1590,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= @@ -773,6 +1626,10 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX 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/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= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= @@ -783,6 +1640,8 @@ github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1 github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= @@ -792,27 +1651,33 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.7 h1:uv+I3nNJvlKZIQGSr8JVQLNHFU9YhhNpvC14Y6KgmSM= github.com/pkg/sftp v1.13.7/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI= -github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/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.6.0 h1:04SZ/092gONTE1XUFzYFWqgB4mKwcdkqNChLMFedwhg= github.com/prometheus-community/pro-bing v0.6.0/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 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/quasilyte/go-ruleguard/dsl v0.3.21 h1:vNkC6fC6qMLzCOGbnIHOd5ixUGgTbp3Z4fGnUgULlDA= -github.com/quasilyte/go-ruleguard/dsl v0.3.21/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +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= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/riandyrn/otelchi v0.5.1 h1:0/45omeqpP7f/cvdL16GddQBfAEmZvUyl2QzLSE6uYo= @@ -825,15 +1690,23 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg= -github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI= +github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= +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= @@ -847,12 +1720,15 @@ github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnj 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= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/sosedoff/gitkit v0.4.0 h1:opyQJ/h9xMRLsz2ca/2CRXtstePcpldiZN8DpLLF8Os= +github.com/sosedoff/gitkit v0.4.0/go.mod h1:V3EpGZ0nvCBhXerPsbDeqtyReNb48cwP9KtkUYTKT5I= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= -github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -874,6 +1750,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -909,8 +1786,12 @@ github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= -github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= +github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= +github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= +github.com/testcontainers/testcontainers-go/modules/localstack v0.35.0 h1:0EbOXcy8XQkyDUs1Y9YPUHOUByNnlGsLi5B3ln8F/RU= +github.com/testcontainers/testcontainers-go/modules/localstack v0.35.0/go.mod h1:MlHuaWQimz+15dmQ6R2S1vpYxhGFEpmRZQsL2NVWNng= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -920,16 +1801,21 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +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/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= github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -957,6 +1843,8 @@ github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pv github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -978,9 +1866,12 @@ github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCO github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= @@ -1020,6 +1911,8 @@ go.opentelemetry.io/collector/semconv v0.104.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZ go.opentelemetry.io/contrib v1.0.0/go.mod h1:EH4yDYeNoaTqn/8yCWQmfNB78VHfGX2Jt2bvnvzBlGM= go.opentelemetry.io/contrib v1.19.0 h1:rnYI7OEPMWFeM4QCqWQ3InMJ0arWMR1i0Cx9A5hcjYM= go.opentelemetry.io/contrib v1.19.0/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= @@ -1049,6 +1942,9 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstF go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -1067,6 +1963,8 @@ go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wus go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 h1:X66ZEoMN2SuaoI/dfZVYobB6E5zjZyyHUMWlCA7MgGE= go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1078,87 +1976,283 @@ golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 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/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/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= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +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/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= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +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= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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/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= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1167,13 +2261,19 @@ 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= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.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= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= @@ -1181,32 +2281,103 @@ golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 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/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= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.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/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= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= @@ -1215,6 +2386,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= @@ -1223,21 +2398,278 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs= google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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= @@ -1245,20 +2677,23 @@ gopkg.in/DataDog/dd-trace-go.v1 v1.72.1 h1:QG2HNpxe9H4WnztDYbdGQJL/5YIiiZ6xY1+wM gopkg.in/DataDog/dd-trace-go.v1 v1.72.1/go.mod h1:XqDhDqsLpThFnJc4z0FvAEItISIAUka+RHwmQ6EfN1U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= @@ -1267,34 +2702,70 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc h1:DXLLFYv/k/xr0rWcwVEvWme1GR36Oc4kNMspg38JeiE= gvisor.dev/gvisor v0.0.0-20240509041132-65b30f7869dc/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +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/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= kernel.org/pub/linux/libs/security/libcap/cap v1.2.73/go.mod h1:hbeKwKcboEsxARYmcy/AdPVN11wmT/Wnpgv4k4ftyqY= kernel.org/pub/linux/libs/security/libcap/psx v1.2.73 h1:SEAEUiPVylTD4vqqi+vtGkSnXeP2FcRO3FoZB1MklMw= kernel.org/pub/linux/libs/security/libcap/psx v1.2.73/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= -lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= -lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= -modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= -modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0= -modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= -modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= -modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8= +modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= diff --git a/provisioner/echo/serve.go b/provisioner/echo/serve.go index 174aba65c7c39..7b59efe860b59 100644 --- a/provisioner/echo/serve.go +++ b/provisioner/echo/serve.go @@ -211,6 +211,8 @@ type Responses struct { // transition responses. They are prioritized over the generic responses. ProvisionApplyMap map[proto.WorkspaceTransition][]*proto.Response ProvisionPlanMap map[proto.WorkspaceTransition][]*proto.Response + + ExtraFiles map[string][]byte } // Tar returns a tar archive of responses to provisioner operations. @@ -226,8 +228,12 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response if responses == nil { responses = &Responses{ - ParseComplete, ApplyComplete, PlanComplete, - nil, nil, + Parse: ParseComplete, + ProvisionApply: ApplyComplete, + ProvisionPlan: PlanComplete, + ProvisionApplyMap: nil, + ProvisionPlanMap: nil, + ExtraFiles: nil, } } if responses.ProvisionPlan == nil { @@ -327,6 +333,25 @@ func TarWithOptions(ctx context.Context, logger slog.Logger, responses *Response } } } + for name, content := range responses.ExtraFiles { + logger.Debug(ctx, "extra file", slog.F("name", name)) + + err := writer.WriteHeader(&tar.Header{ + Name: name, + Size: int64(len(content)), + Mode: 0o644, + }) + if err != nil { + return nil, err + } + + n, err := writer.Write(content) + if err != nil { + return nil, err + } + + logger.Debug(context.Background(), "extra file written", slog.F("name", name), slog.F("bytes_written", n)) + } // `writer.Close()` function flushes the writer buffer, and adds extra padding to create a legal tarball. err := writer.Close() if err != nil { @@ -347,3 +372,12 @@ func WithResources(resources []*proto.Resource) *Responses { }}}}, } } + +func WithExtraFiles(extraFiles map[string][]byte) *Responses { + return &Responses{ + Parse: ParseComplete, + ProvisionApply: ApplyComplete, + ProvisionPlan: PlanComplete, + ExtraFiles: extraFiles, + } +} diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index c36636510451f..5dcf6ae5dfc15 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -32,8 +32,10 @@ func main() { // Serpent has some types referenced in the codersdk. // We want the referenced types generated. referencePackages := map[string]string{ - "github.com/coder/serpent": "Serpent", - "tailscale.com/derp": "", + "github.com/coder/preview": "", + "github.com/coder/serpent": "Serpent", + "github.com/hashicorp/hcl/v2": "Hcl", + "tailscale.com/derp": "", // Conflicting name "DERPRegion" "tailscale.com/tailcfg": "Tail", "tailscale.com/net/netcheck": "Netcheck", @@ -88,7 +90,8 @@ func TypeMappings(gen *guts.GoParser) error { gen.IncludeCustomDeclaration(map[string]guts.TypeOverride{ "github.com/coder/coder/v2/codersdk.NullTime": config.OverrideNullable(config.OverrideLiteral(bindings.KeywordString)), // opt.Bool can return 'null' if unset - "tailscale.com/types/opt.Bool": config.OverrideNullable(config.OverrideLiteral(bindings.KeywordBoolean)), + "tailscale.com/types/opt.Bool": config.OverrideNullable(config.OverrideLiteral(bindings.KeywordBoolean)), + "github.com/hashicorp/hcl/v2.Expression": config.OverrideLiteral(bindings.KeywordUnknown), }) err := gen.IncludeCustom(map[string]string{ diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 09da288ceeb76..d1f38243988a3 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -706,6 +706,21 @@ export const DisplayApps: DisplayApp[] = [ "web_terminal", ]; +// From codersdk/templateversions.go +export interface DynamicParametersRequest { + readonly id: number; + readonly inputs: Record; +} + +// From codersdk/templateversions.go +export interface DynamicParametersResponse { + readonly id: number; + // this is likely an enum in an external package "github.com/coder/preview/types.Diagnostics" + readonly diagnostics: readonly (HclDiagnostic | null)[]; + // external type "github.com/coder/preview/types.Parameter", to include this type the package must be explicitly included in the parsing + readonly parameters: readonly unknown[]; +} + // From codersdk/externalauth.go export type EnhancedExternalAuthProvider = | "azure-devops" @@ -982,6 +997,44 @@ export interface HTTPCookieConfig { readonly same_site?: string; } +// From hcl/diagnostic.go +export interface HclDiagnostic { + readonly Severity: HclDiagnosticSeverity; + readonly Summary: string; + readonly Detail: string; + readonly Subject: HclRange | null; + readonly Context: HclRange | null; + readonly Expression: unknown; + readonly EvalContext: HclEvalContext | null; + // empty interface{} type, falling back to unknown + readonly Extra: unknown; +} + +// From hcl/diagnostic.go +export type HclDiagnosticSeverity = number; + +// From hcl/eval_context.go +export interface HclEvalContext { + // external type "github.com/zclconf/go-cty/cty.Value", to include this type the package must be explicitly included in the parsing + readonly Variables: Record; + // external type "github.com/zclconf/go-cty/cty/function.Function", to include this type the package must be explicitly included in the parsing + readonly Functions: Record; +} + +// From hcl/pos.go +export interface HclPos { + readonly Line: number; + readonly Column: number; + readonly Byte: number; +} + +// From hcl/pos.go +export interface HclRange { + readonly Filename: string; + readonly Start: HclPos; + readonly End: HclPos; +} + // From health/model.go export type HealthCode = | "EACS03" From 9978eb63c48fc15877aa3bbb36163cb80d18f440 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Thu, 10 Apr 2025 16:21:20 -0400 Subject: [PATCH 056/384] docs: rename coder-ai directory to avoid wildcard removal (#17348) to something that doesn't get caught in a wildcard redirect Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/{coder-ai => ai-coder}/agents.md | 0 docs/{coder-ai => ai-coder}/best-practices.md | 0 .../{coder-ai => ai-coder}/coder-dashboard.md | 0 .../{coder-ai => ai-coder}/create-template.md | 0 docs/{coder-ai => ai-coder}/custom-agents.md | 0 docs/{coder-ai => ai-coder}/headless.md | 0 .../{coder-ai => ai-coder}/ide-integration.md | 0 docs/{coder-ai => ai-coder}/index.md | 0 docs/{coder-ai => ai-coder}/issue-tracker.md | 0 docs/{coder-ai => ai-coder}/securing.md | 0 docs/manifest.json | 20 +++++++++---------- 11 files changed, 10 insertions(+), 10 deletions(-) rename docs/{coder-ai => ai-coder}/agents.md (100%) rename docs/{coder-ai => ai-coder}/best-practices.md (100%) rename docs/{coder-ai => ai-coder}/coder-dashboard.md (100%) rename docs/{coder-ai => ai-coder}/create-template.md (100%) rename docs/{coder-ai => ai-coder}/custom-agents.md (100%) rename docs/{coder-ai => ai-coder}/headless.md (100%) rename docs/{coder-ai => ai-coder}/ide-integration.md (100%) rename docs/{coder-ai => ai-coder}/index.md (100%) rename docs/{coder-ai => ai-coder}/issue-tracker.md (100%) rename docs/{coder-ai => ai-coder}/securing.md (100%) diff --git a/docs/coder-ai/agents.md b/docs/ai-coder/agents.md similarity index 100% rename from docs/coder-ai/agents.md rename to docs/ai-coder/agents.md diff --git a/docs/coder-ai/best-practices.md b/docs/ai-coder/best-practices.md similarity index 100% rename from docs/coder-ai/best-practices.md rename to docs/ai-coder/best-practices.md diff --git a/docs/coder-ai/coder-dashboard.md b/docs/ai-coder/coder-dashboard.md similarity index 100% rename from docs/coder-ai/coder-dashboard.md rename to docs/ai-coder/coder-dashboard.md diff --git a/docs/coder-ai/create-template.md b/docs/ai-coder/create-template.md similarity index 100% rename from docs/coder-ai/create-template.md rename to docs/ai-coder/create-template.md diff --git a/docs/coder-ai/custom-agents.md b/docs/ai-coder/custom-agents.md similarity index 100% rename from docs/coder-ai/custom-agents.md rename to docs/ai-coder/custom-agents.md diff --git a/docs/coder-ai/headless.md b/docs/ai-coder/headless.md similarity index 100% rename from docs/coder-ai/headless.md rename to docs/ai-coder/headless.md diff --git a/docs/coder-ai/ide-integration.md b/docs/ai-coder/ide-integration.md similarity index 100% rename from docs/coder-ai/ide-integration.md rename to docs/ai-coder/ide-integration.md diff --git a/docs/coder-ai/index.md b/docs/ai-coder/index.md similarity index 100% rename from docs/coder-ai/index.md rename to docs/ai-coder/index.md diff --git a/docs/coder-ai/issue-tracker.md b/docs/ai-coder/issue-tracker.md similarity index 100% rename from docs/coder-ai/issue-tracker.md rename to docs/ai-coder/issue-tracker.md diff --git a/docs/coder-ai/securing.md b/docs/ai-coder/securing.md similarity index 100% rename from docs/coder-ai/securing.md rename to docs/ai-coder/securing.md diff --git a/docs/manifest.json b/docs/manifest.json index cd07850834831..ec5157c354b5c 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -670,61 +670,61 @@ { "title": "Run AI Coding Agents in Coder", "description": "Learn how to run and integrate AI coding agents like GPT-Code, OpenDevin, or SWE-Agent in Coder workspaces to boost developer productivity.", - "path": "./coder-ai/index.md", + "path": "./ai-coder/index.md", "icon_path": "./images/icons/wand.svg", "state": ["early access"], "children": [ { "title": "Learn about coding agents", "description": "Learn about the different AI agents and their tradeoffs", - "path": "./coder-ai/agents.md" + "path": "./ai-coder/agents.md" }, { "title": "Create a Coder template for agents", "description": "Create a purpose-built template for your AI agents", - "path": "./coder-ai/create-template.md", + "path": "./ai-coder/create-template.md", "state": ["early access"] }, { "title": "Integrate with your issue tracker", "description": "Assign tickets to AI agents and interact via code reviews", - "path": "./coder-ai/issue-tracker.md", + "path": "./ai-coder/issue-tracker.md", "state": ["early access"] }, { "title": "Model Context Protocols (MCP) and adding AI tools", "description": "Improve results by adding tools to your AI agents", - "path": "./coder-ai/best-practices.md", + "path": "./ai-coder/best-practices.md", "state": ["early access"] }, { "title": "Supervise agents via Coder UI", "description": "Interact with agents via the Coder UI", - "path": "./coder-ai/coder-dashboard.md", + "path": "./ai-coder/coder-dashboard.md", "state": ["early access"] }, { "title": "Supervise agents via the IDE", "description": "Interact with agents via VS Code or Cursor", - "path": "./coder-ai/ide-integration.md", + "path": "./ai-coder/ide-integration.md", "state": ["early access"] }, { "title": "Programmatically manage agents", "description": "Manage agents via MCP, the Coder CLI, and/or REST API", - "path": "./coder-ai/headless.md", + "path": "./ai-coder/headless.md", "state": ["early access"] }, { "title": "Securing agents in Coder", "description": "Learn how to secure agents with boundaries", - "path": "./coder-ai/securing.md", + "path": "./ai-coder/securing.md", "state": ["early access"] }, { "title": "Custom agents", "description": "Learn how to use custom agents with Coder", - "path": "./coder-ai/custom-agents.md", + "path": "./ai-coder/custom-agents.md", "state": ["early access"] } ] From a451ea73c30c5e28221418a840ea7b244fe6e7cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:22:57 +0000 Subject: [PATCH 057/384] chore: bump github.com/mark3labs/mcp-go from 0.17.0 to 0.18.0 (#17273) 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.17.0 to 0.18.0.
    Release notes

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

    Release v0.18.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/mark3labs/mcp-go/compare/v0.17.0...v0.18.0

    Commits

    [![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.17.0&new-version=0.18.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 572829b3013f6..d91a319d3885c 100644 --- a/go.mod +++ b/go.mod @@ -489,7 +489,7 @@ require ( require ( github.com/coder/preview v0.0.0-20250409162646-62939c63c71a - github.com/mark3labs/mcp-go v0.17.0 + github.com/mark3labs/mcp-go v0.19.0 ) require ( diff --git a/go.sum b/go.sum index 978d77dc95289..148e4216742da 100644 --- a/go.sum +++ b/go.sum @@ -1496,8 +1496,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.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930= -github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= +github.com/mark3labs/mcp-go v0.19.0 h1:cYKBPFD+fge273/TV6f5+TZYBSTnxV6GCJAO08D2wvA= +github.com/mark3labs/mcp-go v0.19.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= 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 0472a88c2e47ee29440c3652622426e80f951654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Thu, 10 Apr 2025 14:19:39 -0700 Subject: [PATCH 058/384] chore: update trivy (#17347) --- go.mod | 20 ++++++++++---------- go.sum | 52 ++++++++++++++++++++++++++-------------------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index d91a319d3885c..dfd26db45fc6e 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,7 @@ replace github.com/charmbracelet/bubbletea => github.com/coder/bubbletea v1.2.2- // Trivy has some issues that we're floating patches for, and will hopefully // be upstreamed eventually. -replace github.com/aquasecurity/trivy => github.com/emyrk/trivy v0.0.0-20250320190949-47caa1ac2d53 +replace github.com/aquasecurity/trivy => github.com/coder/trivy v0.0.0-20250409153844-e6b004bc465a // afero/tarfs has a bug that breaks our usage. A PR has been submitted upstream. // https://github.com/spf13/afero/pull/487 @@ -257,8 +257,8 @@ require ( github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2 v1.36.3 - github.com/aws/aws-sdk-go-v2/config v1.29.9 - github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.13 + github.com/aws/aws-sdk-go-v2/credentials v1.17.66 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.1 github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect @@ -267,9 +267,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.18 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -285,8 +285,8 @@ require ( github.com/containerd/continuity v0.4.5 // indirect github.com/coreos/go-iptables v0.6.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/docker/cli v27.5.0+incompatible // indirect - github.com/docker/docker v27.5.0+incompatible // indirect + github.com/docker/cli v28.0.4+incompatible // indirect + github.com/docker/docker v28.0.4+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd // indirect @@ -311,7 +311,7 @@ require ( github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-test/deep v1.1.0 // indirect github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect - github.com/go-viper/mapstructure/v2 v2.1.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect @@ -482,7 +482,7 @@ require github.com/SherClockHolmes/webpush-go v1.4.0 require ( github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect + github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect ) diff --git a/go.sum b/go.sum index 148e4216742da..61fcfe8402612 100644 --- a/go.sum +++ b/go.sum @@ -748,10 +748,10 @@ github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.9 h1:Kg+fAYNaJeGXp1vmjtidss8O2uXIsXwaRqsQJKXVr+0= -github.com/aws/aws-sdk-go-v2/config v1.29.9/go.mod h1:oU3jj2O53kgOU4TXq/yipt6ryiooYjlkqqVaZk7gY/U= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62 h1:fvtQY3zFzYJ9CfixuAQ96IxDrBajbBWGqjNTCa79ocU= -github.com/aws/aws-sdk-go-v2/credentials v1.17.62/go.mod h1:ElETBxIQqcxej++Cs8GyPBbgMys5DgQPTwo7cUPDKt8= +github.com/aws/aws-sdk-go-v2/config v1.29.13 h1:RgdPqWoE8nPpIekpVpDJsBckbqT4Liiaq9f35pbTh1Y= +github.com/aws/aws-sdk-go-v2/config v1.29.13/go.mod h1:NI28qs/IOUIRhsR7GQ/JdexoqRN9tDxkIrYZq0SOF44= +github.com/aws/aws-sdk-go-v2/credentials v1.17.66 h1:aKpEKaTy6n4CEJeYI1MNj97oSDLi4xro3UzQfwf5RWE= +github.com/aws/aws-sdk-go-v2/credentials v1.17.66/go.mod h1:xQ5SusDmHb/fy55wU0QqTy0yNfLqxzec59YcsRZB+rI= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.5.1 h1:yg6nrV33ljY6CppoRnnsKLqIZ5ExNdQOGRBGNfc56Yw= @@ -768,12 +768,12 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4 h1:hgSBvRT7JEWx2+vEGI9/Ld5rZtl7M5lu8PqdvOmbRHw= github.com/aws/aws-sdk-go-v2/service/ssm v1.52.4/go.mod h1:v7NIzEFIHBiicOMaMTuEmbnzGnqW0d+6ulNALul6fYE= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 h1:8JdC7Gr9NROg1Rusk25IcZeTO59zLxsKgE0gkh5O6h0= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.1/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 h1:KwuLovgQPcdjNMfFt9OhUd9a2OwcOKhxfvF4glTzLuA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.18 h1:xz7WvTMfSStb9Y8NpCT82FXLNC3QasqBfuAFHY4Pk5g= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.18/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -919,6 +919,8 @@ github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1: 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-pre0 h1:NPt2+FVr+2QJoxrta5ZwyTaxocWMEKdh2WpIumffxfM= github.com/coder/terraform-provider-coder/v2 v2.4.0-pre0/go.mod h1:X28s3rz+aEM5PkBKvk3xcUrQFO2eNPjzRChUg9wb70U= +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= github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Auuw3YOiSyLNHkdMtyCZHPFBx7syN4rk= @@ -971,10 +973,10 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= -github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U= -github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= +github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok= +github.com/docker/docker v28.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -999,8 +1001,6 @@ github.com/emersion/go-smtp v0.21.2 h1:OLDgvZKuofk4em9fT5tFG5j4jE1/hXnX75UMvcrL4 github.com/emersion/go-smtp v0.21.2/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/emyrk/trivy v0.0.0-20250320190949-47caa1ac2d53 h1:0bj1/UEj/7ZwQSm2EAYuYd87feUvqmlrUfR3MRzKOag= -github.com/emyrk/trivy v0.0.0-20250320190949-47caa1ac2d53/go.mod h1:QqQijstmQF9wfPij09KE96MLfbFGtfC21dG299ty+Fc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -1094,8 +1094,8 @@ github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w= -github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= +github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= +github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -1135,8 +1135,8 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= -github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= -github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -1786,10 +1786,10 @@ github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= -github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= -github.com/testcontainers/testcontainers-go/modules/localstack v0.35.0 h1:0EbOXcy8XQkyDUs1Y9YPUHOUByNnlGsLi5B3ln8F/RU= -github.com/testcontainers/testcontainers-go/modules/localstack v0.35.0/go.mod h1:MlHuaWQimz+15dmQ6R2S1vpYxhGFEpmRZQsL2NVWNng= +github.com/testcontainers/testcontainers-go v0.36.0 h1:YpffyLuHtdp5EUsI5mT4sRw8GZhO/5ozyDT1xWGXt00= +github.com/testcontainers/testcontainers-go v0.36.0/go.mod h1:yk73GVJ0KUZIHUtFna6MO7QS144qYpoY8lEEtU9Hed0= +github.com/testcontainers/testcontainers-go/modules/localstack v0.36.0 h1:zVwbe46NYg2vtC26aF0ndClK5S9J7TgAliQbTLyHm+0= +github.com/testcontainers/testcontainers-go/modules/localstack v0.36.0/go.mod h1:rxyzj5nX/OUn7QK5PVxKYHJg1eeNtNzWMX2hSbNNJk0= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= @@ -2753,8 +2753,8 @@ modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= -modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8= -modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= +modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ= +modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= From e7e47537c98963932aa27de0323faf5c6bcd423d Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Fri, 11 Apr 2025 13:33:53 +1000 Subject: [PATCH 059/384] chore: fix gpg forwarding test (#17355) --- .gitignore | 3 +++ cli/ssh_test.go | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d633f94583ec9..8d29eff1048d1 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,6 @@ result # Zed .zed_server + +# dlv debug binaries for go tests +__debug_bin* diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 332fbbe219c46..453073026e16f 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -1977,7 +1977,9 @@ Expire-Date: 0 tpty.WriteLine("gpg --list-keys && echo gpg-''-listkeys-command-done") listKeysOutput := tpty.ExpectMatch("gpg--listkeys-command-done") require.Contains(t, listKeysOutput, "[ultimate] Coder Test ") - require.Contains(t, listKeysOutput, "[ultimate] Dean Sheather (work key) ") + // It's fine that this key is expired. We're just testing that the key trust + // gets synced properly. + require.Contains(t, listKeysOutput, "[ expired] Dean Sheather (work key) ") // Try to sign something. This demonstrates that the forwarding is // working as expected, since the workspace doesn't have access to the From 3c1cb5d05a81758a5567b6bec714e9922b3e4f99 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Fri, 11 Apr 2025 13:59:25 +1000 Subject: [PATCH 060/384] chore: add generic DNS record for checking if Coder Connect is running (#17298) Closes https://github.com/coder/internal/issues/466 ``` $ dig -6 @fd60:627a:a42b::53 is.coder--connect--enabled--right--now.coder AAAA ; <<>> DiG 9.10.6 <<>> -6 @fd60:627a:a42b::53 is.coder--connect--enabled--right--now.coder AAAA ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62390 ;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;is.coder--connect--enabled--right--now.coder. IN AAAA ;; ANSWER SECTION: is.coder--connect--enabled--right--now.coder. 2 IN AAAA fd60:627a:a42b::53 ;; Query time: 3 msec ;; SERVER: fd60:627a:a42b::53#53(fd60:627a:a42b::53) ;; WHEN: Wed Apr 09 16:59:18 AEST 2025 ;; MSG SIZE rcvd: 134 ``` Hostname considerations: - Workspace names, usernames, and agent names can't have double hyphens, so this name can't conflict with a real Coder Connect hostname. - Components can't start or end with hyphens according to [RFC 952](https://www.rfc-editor.org/rfc/rfc952.html) - DNS records can't have hyphens in the 3rd and 4th positions, as to not conflict with IDNs https://datatracker.ietf.org/doc/html/rfc5891 --- tailnet/conn.go | 7 +++++++ tailnet/controllers.go | 2 ++ tailnet/controllers_test.go | 37 +++++++++++++++++++++---------------- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/tailnet/conn.go b/tailnet/conn.go index 59ddefc636d13..89b3b7d483d0c 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -354,6 +354,13 @@ func NewConn(options *Options) (conn *Conn, err error) { return server, nil } +// A FQDN to be mapped to `tsaddr.CoderServiceIPv6`. This address can be used +// when you want to know if Coder Connect is running, but are not trying to +// connect to a specific known workspace. +const IsCoderConnectEnabledFQDNString = "is.coder--connect--enabled--right--now.coder." + +var IsCoderConnectEnabledFQDN, _ = dnsname.ToFQDN(IsCoderConnectEnabledFQDNString) + type ServicePrefix [6]byte var ( diff --git a/tailnet/controllers.go b/tailnet/controllers.go index bf2ec1d964f56..7a077ffabfaa0 100644 --- a/tailnet/controllers.go +++ b/tailnet/controllers.go @@ -16,6 +16,7 @@ import ( "golang.org/x/xerrors" "storj.io/drpc" "storj.io/drpc/drpcerr" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/util/dnsname" @@ -1265,6 +1266,7 @@ func (t *tunnelUpdater) updateDNSNamesLocked() map[dnsname.FQDN][]netip.Addr { } } } + names[IsCoderConnectEnabledFQDN] = []netip.Addr{tsaddr.CoderServiceIPv6()} return names } diff --git a/tailnet/controllers_test.go b/tailnet/controllers_test.go index 16f254e3240a7..3cfa47e3adca2 100644 --- a/tailnet/controllers_test.go +++ b/tailnet/controllers_test.go @@ -22,6 +22,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "storj.io/drpc" "storj.io/drpc/drpcerr" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/util/dnsname" @@ -1563,13 +1564,14 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) { // Also triggers setting DNS hosts expectedDNS := map[dnsname.FQDN][]netip.Addr{ - "w1a1.w1.me.coder.": {ws1a1IP}, - "w2a1.w2.me.coder.": {w2a1IP}, - "w2a2.w2.me.coder.": {w2a2IP}, - "w1a1.w1.testy.coder.": {ws1a1IP}, - "w2a1.w2.testy.coder.": {w2a1IP}, - "w2a2.w2.testy.coder.": {w2a2IP}, - "w1.coder.": {ws1a1IP}, + "w1a1.w1.me.coder.": {ws1a1IP}, + "w2a1.w2.me.coder.": {w2a1IP}, + "w2a2.w2.me.coder.": {w2a2IP}, + "w1a1.w1.testy.coder.": {ws1a1IP}, + "w2a1.w2.testy.coder.": {w2a1IP}, + "w2a2.w2.testy.coder.": {w2a2IP}, + "w1.coder.": {ws1a1IP}, + tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()}, } dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) @@ -1661,9 +1663,10 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { // DNS for w1a1 expectedDNS := map[dnsname.FQDN][]netip.Addr{ - "w1a1.w1.testy.coder.": {ws1a1IP}, - "w1a1.w1.me.coder.": {ws1a1IP}, - "w1.coder.": {ws1a1IP}, + "w1a1.w1.testy.coder.": {ws1a1IP}, + "w1a1.w1.me.coder.": {ws1a1IP}, + "w1.coder.": {ws1a1IP}, + tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()}, } dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) @@ -1716,9 +1719,10 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { // DNS contains only w1a2 expectedDNS = map[dnsname.FQDN][]netip.Addr{ - "w1a2.w1.testy.coder.": {ws1a2IP}, - "w1a2.w1.me.coder.": {ws1a2IP}, - "w1.coder.": {ws1a2IP}, + "w1a2.w1.testy.coder.": {ws1a2IP}, + "w1a2.w1.me.coder.": {ws1a2IP}, + "w1.coder.": {ws1a2IP}, + tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()}, } dnsCall = testutil.RequireRecvCtx(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) @@ -1798,9 +1802,10 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) { // DNS for w1a1 expectedDNS := map[dnsname.FQDN][]netip.Addr{ - "w1a1.w1.me.coder.": {ws1a1IP}, - "w1a1.w1.testy.coder.": {ws1a1IP}, - "w1.coder.": {ws1a1IP}, + "w1a1.w1.me.coder.": {ws1a1IP}, + "w1a1.w1.testy.coder.": {ws1a1IP}, + "w1.coder.": {ws1a1IP}, + tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()}, } dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) From 7bca3e237af3f84257d20b921c4d309cb0853612 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Fri, 11 Apr 2025 14:03:00 +1000 Subject: [PATCH 061/384] chore: update tailscale (#17327) Relates to https://github.com/coder/internal/issues/466 Brings in https://github.com/coder/tailscale/pull/70 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dfd26db45fc6e..8fe432f0418bf 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ replace github.com/tcnksm/go-httpstat => github.com/coder/go-httpstat v0.0.0-202 // There are a few minor changes we make to Tailscale that we're slowly upstreaming. Compare here: // https://github.com/tailscale/tailscale/compare/main...coder:tailscale:main -replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a +replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20250410041146-e62bfe0e9301 // This is replaced to include // 1. a fix for a data race: c.f. https://github.com/tailscale/wireguard-go/pull/25 diff --git a/go.sum b/go.sum index 61fcfe8402612..15a22a21a2a19 100644 --- a/go.sum +++ b/go.sum @@ -913,8 +913,8 @@ github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM= github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q= github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 h1:YoUSJ19E8AtuUFVYBpXuOD6a/zVP3rcxezNsoDseTUw= github.com/coder/ssh v0.0.0-20231128192721-70855dedb788/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ= -github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a h1:18TQ03KlYrkW8hOohTQaDnlmkY1H9pDPGbZwOnUUmm8= -github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko= +github.com/coder/tailscale v1.1.1-0.20250410041146-e62bfe0e9301 h1:RMo8EZAMYnM9+HtCBDvXbcgCf0t8Roo1ZLiy8fVuooQ= +github.com/coder/tailscale v1.1.1-0.20250410041146-e62bfe0e9301/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-pre0 h1:NPt2+FVr+2QJoxrta5ZwyTaxocWMEKdh2WpIumffxfM= From 60fbe675ed5d7d0f066e4b991abe3662fb99cb96 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 11 Apr 2025 11:41:13 +0300 Subject: [PATCH 062/384] refactor(agent/agentcontainers): implement API service (#17340) This refactor improves separation of API and containers with minimal changes to logic. Highlights: - Routes are now defined in `agentcontainers` package - Handler renamed to API - API lazy init was moved into NewAPI - Tests that don't need to be internal were made external --- agent/agentcontainers/api.go | 205 +++++++++ agent/agentcontainers/api_internal_test.go | 161 +++++++ agent/agentcontainers/api_test.go | 171 +++++++ agent/agentcontainers/containers.go | 194 -------- agent/agentcontainers/containers_dockercli.go | 6 +- .../containers_internal_test.go | 421 ------------------ agent/agentcontainers/containers_test.go | 416 +++++++++++------ agent/agentcontainers/devcontainer.go | 1 - agent/api.go | 11 +- 9 files changed, 821 insertions(+), 765 deletions(-) create mode 100644 agent/agentcontainers/api.go create mode 100644 agent/agentcontainers/api_internal_test.go create mode 100644 agent/agentcontainers/api_test.go diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go new file mode 100644 index 0000000000000..81354457d0730 --- /dev/null +++ b/agent/agentcontainers/api.go @@ -0,0 +1,205 @@ +package agentcontainers + +import ( + "context" + "errors" + "net/http" + "slices" + "time" + + "github.com/go-chi/chi/v5" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "github.com/coder/coder/v2/agent/agentexec" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/quartz" +) + +const ( + defaultGetContainersCacheDuration = 10 * time.Second + dockerCreatedAtTimeFormat = "2006-01-02 15:04:05 -0700 MST" + getContainersTimeout = 5 * time.Second +) + +// API is responsible for container-related operations in the agent. +// It provides methods to list and manage containers. +type API struct { + cacheDuration time.Duration + cl Lister + dccli DevcontainerCLI + clock quartz.Clock + + // lockCh protects the below fields. We use a channel instead of a mutex so we + // can handle cancellation properly. + lockCh chan struct{} + containers codersdk.WorkspaceAgentListContainersResponse + mtime time.Time +} + +// Option is a functional option for API. +type Option func(*API) + +// WithLister sets the agentcontainers.Lister implementation to use. +// The default implementation uses the Docker CLI to list containers. +func WithLister(cl Lister) Option { + return func(api *API) { + api.cl = cl + } +} + +func WithDevcontainerCLI(dccli DevcontainerCLI) Option { + return func(api *API) { + api.dccli = dccli + } +} + +// NewAPI returns a new API with the given options applied. +func NewAPI(logger slog.Logger, options ...Option) *API { + api := &API{ + clock: quartz.NewReal(), + cacheDuration: defaultGetContainersCacheDuration, + lockCh: make(chan struct{}, 1), + } + for _, opt := range options { + opt(api) + } + if api.cl == nil { + api.cl = &DockerCLILister{} + } + if api.dccli == nil { + api.dccli = NewDevcontainerCLI(logger, agentexec.DefaultExecer) + } + + return api +} + +// Routes returns the HTTP handler for container-related routes. +func (api *API) Routes() http.Handler { + r := chi.NewRouter() + r.Get("/", api.handleList) + r.Post("/{id}/recreate", api.handleRecreate) + return r +} + +// handleList handles the HTTP request to list containers. +func (api *API) handleList(rw http.ResponseWriter, r *http.Request) { + select { + case <-r.Context().Done(): + // Client went away. + return + default: + ct, err := api.getContainers(r.Context()) + if err != nil { + if errors.Is(err, context.Canceled) { + httpapi.Write(r.Context(), rw, http.StatusRequestTimeout, codersdk.Response{ + Message: "Could not get containers.", + Detail: "Took too long to list containers.", + }) + return + } + httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Could not get containers.", + Detail: err.Error(), + }) + return + } + + httpapi.Write(r.Context(), rw, http.StatusOK, ct) + } +} + +func copyListContainersResponse(resp codersdk.WorkspaceAgentListContainersResponse) codersdk.WorkspaceAgentListContainersResponse { + return codersdk.WorkspaceAgentListContainersResponse{ + Containers: slices.Clone(resp.Containers), + Warnings: slices.Clone(resp.Warnings), + } +} + +func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { + select { + case <-ctx.Done(): + return codersdk.WorkspaceAgentListContainersResponse{}, ctx.Err() + default: + api.lockCh <- struct{}{} + } + defer func() { + <-api.lockCh + }() + + now := api.clock.Now() + if now.Sub(api.mtime) < api.cacheDuration { + return copyListContainersResponse(api.containers), nil + } + + timeoutCtx, timeoutCancel := context.WithTimeout(ctx, getContainersTimeout) + defer timeoutCancel() + updated, err := api.cl.List(timeoutCtx) + if err != nil { + return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("get containers: %w", err) + } + api.containers = updated + api.mtime = now + + return copyListContainersResponse(api.containers), nil +} + +// handleRecreate handles the HTTP request to recreate a container. +func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := chi.URLParam(r, "id") + + if id == "" { + 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.", + }) + return + } + + containers, err := api.cl.List(ctx) + if err != nil { + httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ + Message: "Could not list containers", + Detail: err.Error(), + }) + return + } + + containerIdx := slices.IndexFunc(containers.Containers, func(c codersdk.WorkspaceAgentContainer) bool { + return c.Match(id) + }) + if containerIdx == -1 { + httpapi.Write(ctx, w, http.StatusNotFound, codersdk.Response{ + Message: "Container not found", + Detail: "Container ID or name not found in the list of containers.", + }) + return + } + + container := containers.Containers[containerIdx] + workspaceFolder := container.Labels[DevcontainerLocalFolderLabel] + configPath := container.Labels[DevcontainerConfigFileLabel] + + // Workspace folder is required to recreate a container, we don't verify + // the config path here because it's optional. + 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.", + }) + return + } + + _, err = api.dccli.Up(ctx, workspaceFolder, configPath, WithRemoveExistingContainer()) + if err != nil { + httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ + Message: "Could not recreate devcontainer", + Detail: err.Error(), + }) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/agent/agentcontainers/api_internal_test.go b/agent/agentcontainers/api_internal_test.go new file mode 100644 index 0000000000000..756526d341d68 --- /dev/null +++ b/agent/agentcontainers/api_internal_test.go @@ -0,0 +1,161 @@ +package agentcontainers + +import ( + "math/rand" + "strings" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/agent/agentcontainers/acmock" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" + "github.com/coder/quartz" +) + +func TestAPI(t *testing.T) { + t.Parallel() + + // List tests the API.getContainers method using a mock + // implementation. It specifically tests caching behavior. + t.Run("List", func(t *testing.T) { + t.Parallel() + + fakeCt := fakeContainer(t) + fakeCt2 := fakeContainer(t) + makeResponse := func(cts ...codersdk.WorkspaceAgentContainer) codersdk.WorkspaceAgentListContainersResponse { + return codersdk.WorkspaceAgentListContainersResponse{Containers: cts} + } + + // Each test case is called multiple times to ensure idempotency + for _, tc := range []struct { + name string + // data to be stored in the handler + cacheData codersdk.WorkspaceAgentListContainersResponse + // duration of cache + cacheDur time.Duration + // relative age of the cached data + cacheAge time.Duration + // function to set up expectations for the mock + setupMock func(*acmock.MockLister) + // expected result + expected codersdk.WorkspaceAgentListContainersResponse + // expected error + expectedErr string + }{ + { + name: "no cache", + setupMock: func(mcl *acmock.MockLister) { + mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt), nil).AnyTimes() + }, + expected: makeResponse(fakeCt), + }, + { + name: "no data", + cacheData: makeResponse(), + cacheAge: 2 * time.Second, + cacheDur: time.Second, + setupMock: func(mcl *acmock.MockLister) { + mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt), nil).AnyTimes() + }, + expected: makeResponse(fakeCt), + }, + { + name: "cached data", + cacheAge: time.Second, + cacheData: makeResponse(fakeCt), + cacheDur: 2 * time.Second, + expected: makeResponse(fakeCt), + }, + { + name: "lister error", + setupMock: func(mcl *acmock.MockLister) { + mcl.EXPECT().List(gomock.Any()).Return(makeResponse(), assert.AnError).AnyTimes() + }, + expectedErr: assert.AnError.Error(), + }, + { + name: "stale cache", + cacheAge: 2 * time.Second, + cacheData: makeResponse(fakeCt), + cacheDur: time.Second, + setupMock: func(mcl *acmock.MockLister) { + mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt2), nil).AnyTimes() + }, + expected: makeResponse(fakeCt2), + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + var ( + ctx = testutil.Context(t, testutil.WaitShort) + clk = quartz.NewMock(t) + ctrl = gomock.NewController(t) + mockLister = acmock.NewMockLister(ctrl) + now = time.Now().UTC() + logger = slogtest.Make(t, nil).Leveled(slog.LevelDebug) + api = NewAPI(logger, WithLister(mockLister)) + ) + api.cacheDuration = tc.cacheDur + api.clock = clk + api.containers = tc.cacheData + if tc.cacheAge != 0 { + api.mtime = now.Add(-tc.cacheAge) + } + if tc.setupMock != nil { + tc.setupMock(mockLister) + } + + clk.Set(now).MustWait(ctx) + + // Repeat the test to ensure idempotency + for i := 0; i < 2; i++ { + actual, err := api.getContainers(ctx) + if tc.expectedErr != "" { + require.Empty(t, actual, "expected no data (attempt %d)", i) + require.ErrorContains(t, err, tc.expectedErr, "expected error (attempt %d)", i) + } else { + require.NoError(t, err, "expected no error (attempt %d)", i) + require.Equal(t, tc.expected, actual, "expected containers to be equal (attempt %d)", i) + } + } + }) + } + }) +} + +func fakeContainer(t *testing.T, mut ...func(*codersdk.WorkspaceAgentContainer)) codersdk.WorkspaceAgentContainer { + t.Helper() + ct := codersdk.WorkspaceAgentContainer{ + CreatedAt: time.Now().UTC(), + ID: uuid.New().String(), + FriendlyName: testutil.GetRandomName(t), + Image: testutil.GetRandomName(t) + ":" + strings.Split(uuid.New().String(), "-")[0], + Labels: map[string]string{ + testutil.GetRandomName(t): testutil.GetRandomName(t), + }, + Running: true, + Ports: []codersdk.WorkspaceAgentContainerPort{ + { + Network: "tcp", + Port: testutil.RandomPortNoListen(t), + HostPort: testutil.RandomPortNoListen(t), + //nolint:gosec // this is a test + HostIP: []string{"127.0.0.1", "[::1]", "localhost", "0.0.0.0", "[::]", testutil.GetRandomName(t)}[rand.Intn(6)], + }, + }, + Status: testutil.MustRandString(t, 10), + Volumes: map[string]string{testutil.GetRandomName(t): testutil.GetRandomName(t)}, + } + for _, m := range mut { + m(&ct) + } + return ct +} diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go new file mode 100644 index 0000000000000..76a88f4fc1da4 --- /dev/null +++ b/agent/agentcontainers/api_test.go @@ -0,0 +1,171 @@ +package agentcontainers_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/agent/agentcontainers" + "github.com/coder/coder/v2/codersdk" +) + +// fakeLister implements the agentcontainers.Lister interface for +// testing. +type fakeLister struct { + containers codersdk.WorkspaceAgentListContainersResponse + err error +} + +func (f *fakeLister) List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { + return f.containers, f.err +} + +// fakeDevcontainerCLI implements the agentcontainers.DevcontainerCLI +// interface for testing. +type fakeDevcontainerCLI struct { + id string + err error +} + +func (f *fakeDevcontainerCLI) Up(_ context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIUpOptions) (string, error) { + return f.id, f.err +} + +func TestAPI(t *testing.T) { + t.Parallel() + + t.Run("Recreate", func(t *testing.T) { + t.Parallel() + + validContainer := codersdk.WorkspaceAgentContainer{ + ID: "container-id", + FriendlyName: "container-name", + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspace", + agentcontainers.DevcontainerConfigFileLabel: "/workspace/.devcontainer/devcontainer.json", + }, + } + + missingFolderContainer := codersdk.WorkspaceAgentContainer{ + ID: "missing-folder-container", + FriendlyName: "missing-folder-container", + Labels: map[string]string{}, + } + + tests := []struct { + name string + containerID string + lister *fakeLister + devcontainerCLI *fakeDevcontainerCLI + wantStatus int + wantBody string + }{ + { + name: "Missing ID", + containerID: "", + lister: &fakeLister{}, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusBadRequest, + wantBody: "Missing container ID or name", + }, + { + name: "List error", + containerID: "container-id", + lister: &fakeLister{ + err: xerrors.New("list error"), + }, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusInternalServerError, + wantBody: "Could not list containers", + }, + { + name: "Container not found", + containerID: "nonexistent-container", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{validContainer}, + }, + }, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusNotFound, + wantBody: "Container not found", + }, + { + name: "Missing workspace folder label", + containerID: "missing-folder-container", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{missingFolderContainer}, + }, + }, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusBadRequest, + wantBody: "Missing workspace folder label", + }, + { + name: "Devcontainer CLI error", + containerID: "container-id", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{validContainer}, + }, + }, + devcontainerCLI: &fakeDevcontainerCLI{ + err: xerrors.New("devcontainer CLI error"), + }, + wantStatus: http.StatusInternalServerError, + wantBody: "Could not recreate devcontainer", + }, + { + name: "OK", + containerID: "container-id", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{validContainer}, + }, + }, + devcontainerCLI: &fakeDevcontainerCLI{}, + wantStatus: http.StatusNoContent, + wantBody: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + + // Setup router with the handler under test. + r := chi.NewRouter() + api := agentcontainers.NewAPI( + logger, + agentcontainers.WithLister(tt.lister), + agentcontainers.WithDevcontainerCLI(tt.devcontainerCLI), + ) + r.Mount("/containers", api.Routes()) + + // Simulate HTTP request to the recreate endpoint. + req := httptest.NewRequest(http.MethodPost, "/containers/"+tt.containerID+"/recreate", nil) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + + // Check the response status code and body. + require.Equal(t, tt.wantStatus, rec.Code, "status code mismatch") + if tt.wantBody != "" { + assert.Contains(t, rec.Body.String(), tt.wantBody, "response body mismatch") + } else if tt.wantStatus == http.StatusNoContent { + assert.Empty(t, rec.Body.String(), "expected empty response body") + } + }) + } + }) +} diff --git a/agent/agentcontainers/containers.go b/agent/agentcontainers/containers.go index edd099dd842c5..5be288781d480 100644 --- a/agent/agentcontainers/containers.go +++ b/agent/agentcontainers/containers.go @@ -2,146 +2,10 @@ package agentcontainers import ( "context" - "errors" - "net/http" - "slices" - "time" - "golang.org/x/xerrors" - - "github.com/go-chi/chi/v5" - - "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/codersdk" - "github.com/coder/quartz" -) - -const ( - defaultGetContainersCacheDuration = 10 * time.Second - dockerCreatedAtTimeFormat = "2006-01-02 15:04:05 -0700 MST" - getContainersTimeout = 5 * time.Second ) -type Handler struct { - cacheDuration time.Duration - cl Lister - dccli DevcontainerCLI - clock quartz.Clock - - // lockCh protects the below fields. We use a channel instead of a mutex so we - // can handle cancellation properly. - lockCh chan struct{} - containers *codersdk.WorkspaceAgentListContainersResponse - mtime time.Time -} - -// Option is a functional option for Handler. -type Option func(*Handler) - -// WithLister sets the agentcontainers.Lister implementation to use. -// The default implementation uses the Docker CLI to list containers. -func WithLister(cl Lister) Option { - return func(ch *Handler) { - ch.cl = cl - } -} - -func WithDevcontainerCLI(dccli DevcontainerCLI) Option { - return func(ch *Handler) { - ch.dccli = dccli - } -} - -// New returns a new Handler with the given options applied. -func New(options ...Option) *Handler { - ch := &Handler{ - lockCh: make(chan struct{}, 1), - } - for _, opt := range options { - opt(ch) - } - return ch -} - -func (ch *Handler) List(rw http.ResponseWriter, r *http.Request) { - select { - case <-r.Context().Done(): - // Client went away. - return - default: - ct, err := ch.getContainers(r.Context()) - if err != nil { - if errors.Is(err, context.Canceled) { - httpapi.Write(r.Context(), rw, http.StatusRequestTimeout, codersdk.Response{ - Message: "Could not get containers.", - Detail: "Took too long to list containers.", - }) - return - } - httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Could not get containers.", - Detail: err.Error(), - }) - return - } - - httpapi.Write(r.Context(), rw, http.StatusOK, ct) - } -} - -func (ch *Handler) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { - select { - case <-ctx.Done(): - return codersdk.WorkspaceAgentListContainersResponse{}, ctx.Err() - default: - ch.lockCh <- struct{}{} - } - defer func() { - <-ch.lockCh - }() - - // make zero-value usable - if ch.cacheDuration == 0 { - ch.cacheDuration = defaultGetContainersCacheDuration - } - if ch.cl == nil { - ch.cl = &DockerCLILister{} - } - if ch.containers == nil { - ch.containers = &codersdk.WorkspaceAgentListContainersResponse{} - } - if ch.clock == nil { - ch.clock = quartz.NewReal() - } - - now := ch.clock.Now() - if now.Sub(ch.mtime) < ch.cacheDuration { - // Return a copy of the cached data to avoid accidental modification by the caller. - cpy := codersdk.WorkspaceAgentListContainersResponse{ - Containers: slices.Clone(ch.containers.Containers), - Warnings: slices.Clone(ch.containers.Warnings), - } - return cpy, nil - } - - timeoutCtx, timeoutCancel := context.WithTimeout(ctx, getContainersTimeout) - defer timeoutCancel() - updated, err := ch.cl.List(timeoutCtx) - if err != nil { - return codersdk.WorkspaceAgentListContainersResponse{}, xerrors.Errorf("get containers: %w", err) - } - ch.containers = &updated - ch.mtime = now - - // Return a copy of the cached data to avoid accidental modification by the - // caller. - cpy := codersdk.WorkspaceAgentListContainersResponse{ - Containers: slices.Clone(ch.containers.Containers), - Warnings: slices.Clone(ch.containers.Warnings), - } - return cpy, nil -} - // Lister is an interface for listing containers visible to the // workspace agent. type Lister interface { @@ -158,61 +22,3 @@ var _ Lister = NoopLister{} func (NoopLister) List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { return codersdk.WorkspaceAgentListContainersResponse{}, nil } - -func (ch *Handler) Recreate(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - id := chi.URLParam(r, "id") - - if id == "" { - 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.", - }) - return - } - - containers, err := ch.cl.List(ctx) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Could not list containers", - Detail: err.Error(), - }) - return - } - - containerIdx := slices.IndexFunc(containers.Containers, func(c codersdk.WorkspaceAgentContainer) bool { - return c.Match(id) - }) - if containerIdx == -1 { - httpapi.Write(ctx, w, http.StatusNotFound, codersdk.Response{ - Message: "Container not found", - Detail: "Container ID or name not found in the list of containers.", - }) - return - } - - container := containers.Containers[containerIdx] - workspaceFolder := container.Labels[DevcontainerLocalFolderLabel] - configPath := container.Labels[DevcontainerConfigFileLabel] - - // Workspace folder is required to recreate a container, we don't verify - // the config path here because it's optional. - 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.", - }) - return - } - - _, err = ch.dccli.Up(ctx, workspaceFolder, configPath, WithRemoveExistingContainer()) - if err != nil { - httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ - Message: "Could not recreate devcontainer", - Detail: err.Error(), - }) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/agent/agentcontainers/containers_dockercli.go b/agent/agentcontainers/containers_dockercli.go index b29f1e974bf3b..208c3ec2ea89b 100644 --- a/agent/agentcontainers/containers_dockercli.go +++ b/agent/agentcontainers/containers_dockercli.go @@ -14,14 +14,14 @@ import ( "strings" "time" + "golang.org/x/exp/maps" + "golang.org/x/xerrors" + "github.com/coder/coder/v2/agent/agentcontainers/dcspec" "github.com/coder/coder/v2/agent/agentexec" "github.com/coder/coder/v2/agent/usershell" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" - - "golang.org/x/exp/maps" - "golang.org/x/xerrors" ) // DockerCLILister is a ContainerLister that lists containers using the docker CLI diff --git a/agent/agentcontainers/containers_internal_test.go b/agent/agentcontainers/containers_internal_test.go index 6b59da407f789..eeb6a5d0374d1 100644 --- a/agent/agentcontainers/containers_internal_test.go +++ b/agent/agentcontainers/containers_internal_test.go @@ -1,163 +1,18 @@ package agentcontainers import ( - "fmt" - "math/rand" "os" "path/filepath" - "slices" - "strconv" - "strings" "testing" "time" - "go.uber.org/mock/gomock" - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/ory/dockertest/v3" - "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/coder/coder/v2/agent/agentcontainers/acmock" - "github.com/coder/coder/v2/agent/agentexec" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/pty" - "github.com/coder/coder/v2/testutil" - "github.com/coder/quartz" ) -// TestIntegrationDocker tests agentcontainers functionality using a real -// Docker container. It starts a container with a known -// label, lists the containers, and verifies that the expected container is -// returned. It also executes a sample command inside the container. -// The container is deleted after the test is complete. -// As this test creates containers, it is skipped by default. -// It can be run manually as follows: -// -// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerCLIContainerLister -// -//nolint:paralleltest // This test tends to flake when lots of containers start and stop in parallel. -func TestIntegrationDocker(t *testing.T) { - if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" { - t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") - } - - pool, err := dockertest.NewPool("") - require.NoError(t, err, "Could not connect to docker") - testLabelValue := uuid.New().String() - // Create a temporary directory to validate that we surface mounts correctly. - testTempDir := t.TempDir() - // Pick a random port to expose for testing port bindings. - testRandPort := testutil.RandomPortNoListen(t) - ct, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: "busybox", - Tag: "latest", - Cmd: []string{"sleep", "infnity"}, - Labels: map[string]string{ - "com.coder.test": testLabelValue, - "devcontainer.metadata": `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`, - }, - Mounts: []string{testTempDir + ":" + testTempDir}, - ExposedPorts: []string{fmt.Sprintf("%d/tcp", testRandPort)}, - PortBindings: map[docker.Port][]docker.PortBinding{ - docker.Port(fmt.Sprintf("%d/tcp", testRandPort)): { - { - HostIP: "0.0.0.0", - HostPort: strconv.FormatInt(int64(testRandPort), 10), - }, - }, - }, - }, func(config *docker.HostConfig) { - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }) - require.NoError(t, err, "Could not start test docker container") - t.Logf("Created container %q", ct.Container.Name) - t.Cleanup(func() { - assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name) - t.Logf("Purged container %q", ct.Container.Name) - }) - // Wait for container to start - require.Eventually(t, func() bool { - ct, ok := pool.ContainerByName(ct.Container.Name) - return ok && ct.Container.State.Running - }, testutil.WaitShort, testutil.IntervalSlow, "Container did not start in time") - - dcl := NewDocker(agentexec.DefaultExecer) - ctx := testutil.Context(t, testutil.WaitShort) - actual, err := dcl.List(ctx) - require.NoError(t, err, "Could not list containers") - require.Empty(t, actual.Warnings, "Expected no warnings") - var found bool - for _, foundContainer := range actual.Containers { - if foundContainer.ID == ct.Container.ID { - found = true - assert.Equal(t, ct.Container.Created, foundContainer.CreatedAt) - // ory/dockertest pre-pends a forward slash to the container name. - assert.Equal(t, strings.TrimPrefix(ct.Container.Name, "/"), foundContainer.FriendlyName) - // ory/dockertest returns the sha256 digest of the image. - assert.Equal(t, "busybox:latest", foundContainer.Image) - assert.Equal(t, ct.Container.Config.Labels, foundContainer.Labels) - assert.True(t, foundContainer.Running) - assert.Equal(t, "running", foundContainer.Status) - if assert.Len(t, foundContainer.Ports, 1) { - assert.Equal(t, testRandPort, foundContainer.Ports[0].Port) - assert.Equal(t, "tcp", foundContainer.Ports[0].Network) - } - if assert.Len(t, foundContainer.Volumes, 1) { - assert.Equal(t, testTempDir, foundContainer.Volumes[testTempDir]) - } - // Test that EnvInfo is able to correctly modify a command to be - // executed inside the container. - dei, err := EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, "") - require.NoError(t, err, "Expected no error from DockerEnvInfo()") - ptyWrappedCmd, ptyWrappedArgs := dei.ModifyCommand("/bin/sh", "--norc") - ptyCmd, ptyPs, err := pty.Start(agentexec.DefaultExecer.PTYCommandContext(ctx, ptyWrappedCmd, ptyWrappedArgs...)) - require.NoError(t, err, "failed to start pty command") - t.Cleanup(func() { - _ = ptyPs.Kill() - _ = ptyCmd.Close() - }) - tr := testutil.NewTerminalReader(t, ptyCmd.OutputReader()) - matchPrompt := func(line string) bool { - return strings.Contains(line, "#") - } - matchHostnameCmd := func(line string) bool { - return strings.Contains(strings.TrimSpace(line), "hostname") - } - matchHostnameOuput := func(line string) bool { - return strings.Contains(strings.TrimSpace(line), ct.Container.Config.Hostname) - } - matchEnvCmd := func(line string) bool { - return strings.Contains(strings.TrimSpace(line), "env") - } - matchEnvOutput := func(line string) bool { - return strings.Contains(line, "FOO=bar") || strings.Contains(line, "MULTILINE=foo") - } - require.NoError(t, tr.ReadUntil(ctx, matchPrompt), "failed to match prompt") - t.Logf("Matched prompt") - _, err = ptyCmd.InputWriter().Write([]byte("hostname\r\n")) - require.NoError(t, err, "failed to write to pty") - t.Logf("Wrote hostname command") - require.NoError(t, tr.ReadUntil(ctx, matchHostnameCmd), "failed to match hostname command") - t.Logf("Matched hostname command") - require.NoError(t, tr.ReadUntil(ctx, matchHostnameOuput), "failed to match hostname output") - t.Logf("Matched hostname output") - _, err = ptyCmd.InputWriter().Write([]byte("env\r\n")) - require.NoError(t, err, "failed to write to pty") - t.Logf("Wrote env command") - require.NoError(t, tr.ReadUntil(ctx, matchEnvCmd), "failed to match env command") - t.Logf("Matched env command") - require.NoError(t, tr.ReadUntil(ctx, matchEnvOutput), "failed to match env output") - t.Logf("Matched env output") - break - } - } - assert.True(t, found, "Expected to find container with label 'com.coder.test=%s'", testLabelValue) -} - func TestWrapDockerExec(t *testing.T) { t.Parallel() tests := []struct { @@ -196,120 +51,6 @@ func TestWrapDockerExec(t *testing.T) { } } -// TestContainersHandler tests the containersHandler.getContainers method using -// a mock implementation. It specifically tests caching behavior. -func TestContainersHandler(t *testing.T) { - t.Parallel() - - t.Run("list", func(t *testing.T) { - t.Parallel() - - fakeCt := fakeContainer(t) - fakeCt2 := fakeContainer(t) - makeResponse := func(cts ...codersdk.WorkspaceAgentContainer) codersdk.WorkspaceAgentListContainersResponse { - return codersdk.WorkspaceAgentListContainersResponse{Containers: cts} - } - - // Each test case is called multiple times to ensure idempotency - for _, tc := range []struct { - name string - // data to be stored in the handler - cacheData codersdk.WorkspaceAgentListContainersResponse - // duration of cache - cacheDur time.Duration - // relative age of the cached data - cacheAge time.Duration - // function to set up expectations for the mock - setupMock func(*acmock.MockLister) - // expected result - expected codersdk.WorkspaceAgentListContainersResponse - // expected error - expectedErr string - }{ - { - name: "no cache", - setupMock: func(mcl *acmock.MockLister) { - mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt), nil).AnyTimes() - }, - expected: makeResponse(fakeCt), - }, - { - name: "no data", - cacheData: makeResponse(), - cacheAge: 2 * time.Second, - cacheDur: time.Second, - setupMock: func(mcl *acmock.MockLister) { - mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt), nil).AnyTimes() - }, - expected: makeResponse(fakeCt), - }, - { - name: "cached data", - cacheAge: time.Second, - cacheData: makeResponse(fakeCt), - cacheDur: 2 * time.Second, - expected: makeResponse(fakeCt), - }, - { - name: "lister error", - setupMock: func(mcl *acmock.MockLister) { - mcl.EXPECT().List(gomock.Any()).Return(makeResponse(), assert.AnError).AnyTimes() - }, - expectedErr: assert.AnError.Error(), - }, - { - name: "stale cache", - cacheAge: 2 * time.Second, - cacheData: makeResponse(fakeCt), - cacheDur: time.Second, - setupMock: func(mcl *acmock.MockLister) { - mcl.EXPECT().List(gomock.Any()).Return(makeResponse(fakeCt2), nil).AnyTimes() - }, - expected: makeResponse(fakeCt2), - }, - } { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - var ( - ctx = testutil.Context(t, testutil.WaitShort) - clk = quartz.NewMock(t) - ctrl = gomock.NewController(t) - mockLister = acmock.NewMockLister(ctrl) - now = time.Now().UTC() - ch = Handler{ - cacheDuration: tc.cacheDur, - cl: mockLister, - clock: clk, - containers: &tc.cacheData, - lockCh: make(chan struct{}, 1), - } - ) - if tc.cacheAge != 0 { - ch.mtime = now.Add(-tc.cacheAge) - } - if tc.setupMock != nil { - tc.setupMock(mockLister) - } - - clk.Set(now).MustWait(ctx) - - // Repeat the test to ensure idempotency - for i := 0; i < 2; i++ { - actual, err := ch.getContainers(ctx) - if tc.expectedErr != "" { - require.Empty(t, actual, "expected no data (attempt %d)", i) - require.ErrorContains(t, err, tc.expectedErr, "expected error (attempt %d)", i) - } else { - require.NoError(t, err, "expected no error (attempt %d)", i) - require.Equal(t, tc.expected, actual, "expected containers to be equal (attempt %d)", i) - } - } - }) - } - }) -} - func TestConvertDockerPort(t *testing.T) { t.Parallel() @@ -675,165 +416,3 @@ func TestConvertDockerInspect(t *testing.T) { }) } } - -// TestDockerEnvInfoer tests the ability of EnvInfo to extract information from -// running containers. Containers are deleted after the test is complete. -// As this test creates containers, it is skipped by default. -// It can be run manually as follows: -// -// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerEnvInfoer -// -//nolint:paralleltest // This test tends to flake when lots of containers start and stop in parallel. -func TestDockerEnvInfoer(t *testing.T) { - if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" { - t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") - } - - pool, err := dockertest.NewPool("") - require.NoError(t, err, "Could not connect to docker") - // nolint:paralleltest // variable recapture no longer required - for idx, tt := range []struct { - image string - labels map[string]string - expectedEnv []string - containerUser string - expectedUsername string - expectedUserShell string - }{ - { - image: "busybox:latest", - labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, - - expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, - expectedUsername: "root", - expectedUserShell: "/bin/sh", - }, - { - image: "busybox:latest", - labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, - expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, - containerUser: "root", - expectedUsername: "root", - expectedUserShell: "/bin/sh", - }, - { - image: "codercom/enterprise-minimal:ubuntu", - labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, - expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, - expectedUsername: "coder", - expectedUserShell: "/bin/bash", - }, - { - image: "codercom/enterprise-minimal:ubuntu", - labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, - expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, - containerUser: "coder", - expectedUsername: "coder", - expectedUserShell: "/bin/bash", - }, - { - image: "codercom/enterprise-minimal:ubuntu", - labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, - expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, - containerUser: "root", - expectedUsername: "root", - expectedUserShell: "/bin/bash", - }, - { - image: "codercom/enterprise-minimal:ubuntu", - labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar"}},{"remoteEnv": {"MULTILINE": "foo\nbar\nbaz"}}]`}, - expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, - containerUser: "root", - expectedUsername: "root", - expectedUserShell: "/bin/bash", - }, - } { - //nolint:paralleltest // variable recapture no longer required - t.Run(fmt.Sprintf("#%d", idx), func(t *testing.T) { - // Start a container with the given image - // and environment variables - image := strings.Split(tt.image, ":")[0] - tag := strings.Split(tt.image, ":")[1] - ct, err := pool.RunWithOptions(&dockertest.RunOptions{ - Repository: image, - Tag: tag, - Cmd: []string{"sleep", "infinity"}, - Labels: tt.labels, - }, func(config *docker.HostConfig) { - config.AutoRemove = true - config.RestartPolicy = docker.RestartPolicy{Name: "no"} - }) - require.NoError(t, err, "Could not start test docker container") - t.Logf("Created container %q", ct.Container.Name) - t.Cleanup(func() { - assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name) - t.Logf("Purged container %q", ct.Container.Name) - }) - - ctx := testutil.Context(t, testutil.WaitShort) - dei, err := EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, tt.containerUser) - require.NoError(t, err, "Expected no error from DockerEnvInfo()") - - u, err := dei.User() - require.NoError(t, err, "Expected no error from CurrentUser()") - require.Equal(t, tt.expectedUsername, u.Username, "Expected username to match") - - hd, err := dei.HomeDir() - require.NoError(t, err, "Expected no error from UserHomeDir()") - require.NotEmpty(t, hd, "Expected user homedir to be non-empty") - - sh, err := dei.Shell(tt.containerUser) - require.NoError(t, err, "Expected no error from UserShell()") - require.Equal(t, tt.expectedUserShell, sh, "Expected user shell to match") - - // We don't need to test the actual environment variables here. - environ := dei.Environ() - require.NotEmpty(t, environ, "Expected environ to be non-empty") - - // Test that the environment variables are present in modified command - // output. - envCmd, envArgs := dei.ModifyCommand("env") - for _, env := range tt.expectedEnv { - require.Subset(t, envArgs, []string{"--env", env}) - } - // Run the command in the container and check the output - // HACK: we remove the --tty argument because we're not running in a tty - envArgs = slices.DeleteFunc(envArgs, func(s string) bool { return s == "--tty" }) - stdout, stderr, err := run(ctx, agentexec.DefaultExecer, envCmd, envArgs...) - require.Empty(t, stderr, "Expected no stderr output") - require.NoError(t, err, "Expected no error from running command") - for _, env := range tt.expectedEnv { - require.Contains(t, stdout, env) - } - }) - } -} - -func fakeContainer(t *testing.T, mut ...func(*codersdk.WorkspaceAgentContainer)) codersdk.WorkspaceAgentContainer { - t.Helper() - ct := codersdk.WorkspaceAgentContainer{ - CreatedAt: time.Now().UTC(), - ID: uuid.New().String(), - FriendlyName: testutil.GetRandomName(t), - Image: testutil.GetRandomName(t) + ":" + strings.Split(uuid.New().String(), "-")[0], - Labels: map[string]string{ - testutil.GetRandomName(t): testutil.GetRandomName(t), - }, - Running: true, - Ports: []codersdk.WorkspaceAgentContainerPort{ - { - Network: "tcp", - Port: testutil.RandomPortNoListen(t), - HostPort: testutil.RandomPortNoListen(t), - //nolint:gosec // this is a test - HostIP: []string{"127.0.0.1", "[::1]", "localhost", "0.0.0.0", "[::]", testutil.GetRandomName(t)}[rand.Intn(6)], - }, - }, - Status: testutil.MustRandString(t, 10), - Volumes: map[string]string{testutil.GetRandomName(t): testutil.GetRandomName(t)}, - } - for _, m := range mut { - m(&ct) - } - return ct -} diff --git a/agent/agentcontainers/containers_test.go b/agent/agentcontainers/containers_test.go index ac479de25419a..59befb2fd2be0 100644 --- a/agent/agentcontainers/containers_test.go +++ b/agent/agentcontainers/containers_test.go @@ -2,165 +2,295 @@ package agentcontainers_test import ( "context" - "net/http" - "net/http/httptest" + "fmt" + "os" + "slices" + "strconv" + "strings" "testing" - "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/xerrors" "github.com/coder/coder/v2/agent/agentcontainers" - "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/agent/agentexec" + "github.com/coder/coder/v2/pty" + "github.com/coder/coder/v2/testutil" ) -// fakeLister implements the agentcontainers.Lister interface for -// testing. -type fakeLister struct { - containers codersdk.WorkspaceAgentListContainersResponse - err error -} +// TestIntegrationDocker tests agentcontainers functionality using a real +// Docker container. It starts a container with a known +// label, lists the containers, and verifies that the expected container is +// returned. It also executes a sample command inside the container. +// The container is deleted after the test is complete. +// As this test creates containers, it is skipped by default. +// It can be run manually as follows: +// +// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerCLIContainerLister +// +//nolint:paralleltest // This test tends to flake when lots of containers start and stop in parallel. +func TestIntegrationDocker(t *testing.T) { + if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" { + t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") + } -func (f *fakeLister) List(_ context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { - return f.containers, f.err -} + pool, err := dockertest.NewPool("") + require.NoError(t, err, "Could not connect to docker") + testLabelValue := uuid.New().String() + // Create a temporary directory to validate that we surface mounts correctly. + testTempDir := t.TempDir() + // Pick a random port to expose for testing port bindings. + testRandPort := testutil.RandomPortNoListen(t) + ct, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "busybox", + Tag: "latest", + Cmd: []string{"sleep", "infnity"}, + Labels: map[string]string{ + "com.coder.test": testLabelValue, + "devcontainer.metadata": `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`, + }, + Mounts: []string{testTempDir + ":" + testTempDir}, + ExposedPorts: []string{fmt.Sprintf("%d/tcp", testRandPort)}, + PortBindings: map[docker.Port][]docker.PortBinding{ + docker.Port(fmt.Sprintf("%d/tcp", testRandPort)): { + { + HostIP: "0.0.0.0", + HostPort: strconv.FormatInt(int64(testRandPort), 10), + }, + }, + }, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + require.NoError(t, err, "Could not start test docker container") + t.Logf("Created container %q", ct.Container.Name) + t.Cleanup(func() { + assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name) + t.Logf("Purged container %q", ct.Container.Name) + }) + // Wait for container to start + require.Eventually(t, func() bool { + ct, ok := pool.ContainerByName(ct.Container.Name) + return ok && ct.Container.State.Running + }, testutil.WaitShort, testutil.IntervalSlow, "Container did not start in time") -// fakeDevcontainerCLI implements the agentcontainers.DevcontainerCLI -// interface for testing. -type fakeDevcontainerCLI struct { - id string - err error + dcl := agentcontainers.NewDocker(agentexec.DefaultExecer) + ctx := testutil.Context(t, testutil.WaitShort) + actual, err := dcl.List(ctx) + require.NoError(t, err, "Could not list containers") + require.Empty(t, actual.Warnings, "Expected no warnings") + var found bool + for _, foundContainer := range actual.Containers { + if foundContainer.ID == ct.Container.ID { + found = true + assert.Equal(t, ct.Container.Created, foundContainer.CreatedAt) + // ory/dockertest pre-pends a forward slash to the container name. + assert.Equal(t, strings.TrimPrefix(ct.Container.Name, "/"), foundContainer.FriendlyName) + // ory/dockertest returns the sha256 digest of the image. + assert.Equal(t, "busybox:latest", foundContainer.Image) + assert.Equal(t, ct.Container.Config.Labels, foundContainer.Labels) + assert.True(t, foundContainer.Running) + assert.Equal(t, "running", foundContainer.Status) + if assert.Len(t, foundContainer.Ports, 1) { + assert.Equal(t, testRandPort, foundContainer.Ports[0].Port) + assert.Equal(t, "tcp", foundContainer.Ports[0].Network) + } + if assert.Len(t, foundContainer.Volumes, 1) { + assert.Equal(t, testTempDir, foundContainer.Volumes[testTempDir]) + } + // Test that EnvInfo is able to correctly modify a command to be + // executed inside the container. + dei, err := agentcontainers.EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, "") + require.NoError(t, err, "Expected no error from DockerEnvInfo()") + ptyWrappedCmd, ptyWrappedArgs := dei.ModifyCommand("/bin/sh", "--norc") + ptyCmd, ptyPs, err := pty.Start(agentexec.DefaultExecer.PTYCommandContext(ctx, ptyWrappedCmd, ptyWrappedArgs...)) + require.NoError(t, err, "failed to start pty command") + t.Cleanup(func() { + _ = ptyPs.Kill() + _ = ptyCmd.Close() + }) + tr := testutil.NewTerminalReader(t, ptyCmd.OutputReader()) + matchPrompt := func(line string) bool { + return strings.Contains(line, "#") + } + matchHostnameCmd := func(line string) bool { + return strings.Contains(strings.TrimSpace(line), "hostname") + } + matchHostnameOuput := func(line string) bool { + return strings.Contains(strings.TrimSpace(line), ct.Container.Config.Hostname) + } + matchEnvCmd := func(line string) bool { + return strings.Contains(strings.TrimSpace(line), "env") + } + matchEnvOutput := func(line string) bool { + return strings.Contains(line, "FOO=bar") || strings.Contains(line, "MULTILINE=foo") + } + require.NoError(t, tr.ReadUntil(ctx, matchPrompt), "failed to match prompt") + t.Logf("Matched prompt") + _, err = ptyCmd.InputWriter().Write([]byte("hostname\r\n")) + require.NoError(t, err, "failed to write to pty") + t.Logf("Wrote hostname command") + require.NoError(t, tr.ReadUntil(ctx, matchHostnameCmd), "failed to match hostname command") + t.Logf("Matched hostname command") + require.NoError(t, tr.ReadUntil(ctx, matchHostnameOuput), "failed to match hostname output") + t.Logf("Matched hostname output") + _, err = ptyCmd.InputWriter().Write([]byte("env\r\n")) + require.NoError(t, err, "failed to write to pty") + t.Logf("Wrote env command") + require.NoError(t, tr.ReadUntil(ctx, matchEnvCmd), "failed to match env command") + t.Logf("Matched env command") + require.NoError(t, tr.ReadUntil(ctx, matchEnvOutput), "failed to match env output") + t.Logf("Matched env output") + break + } + } + assert.True(t, found, "Expected to find container with label 'com.coder.test=%s'", testLabelValue) } -func (f *fakeDevcontainerCLI) Up(_ context.Context, _, _ string, _ ...agentcontainers.DevcontainerCLIUpOptions) (string, error) { - return f.id, f.err -} +// TestDockerEnvInfoer tests the ability of EnvInfo to extract information from +// running containers. Containers are deleted after the test is complete. +// As this test creates containers, it is skipped by default. +// It can be run manually as follows: +// +// CODER_TEST_USE_DOCKER=1 go test ./agent/agentcontainers -run TestDockerEnvInfoer +// +//nolint:paralleltest // This test tends to flake when lots of containers start and stop in parallel. +func TestDockerEnvInfoer(t *testing.T) { + if ctud, ok := os.LookupEnv("CODER_TEST_USE_DOCKER"); !ok || ctud != "1" { + t.Skip("Set CODER_TEST_USE_DOCKER=1 to run this test") + } -func TestHandler(t *testing.T) { - t.Parallel() + pool, err := dockertest.NewPool("") + require.NoError(t, err, "Could not connect to docker") + // nolint:paralleltest // variable recapture no longer required + for idx, tt := range []struct { + image string + labels map[string]string + expectedEnv []string + containerUser string + expectedUsername string + expectedUserShell string + }{ + { + image: "busybox:latest", + labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, - t.Run("Recreate", func(t *testing.T) { - t.Parallel() + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + expectedUsername: "root", + expectedUserShell: "/bin/sh", + }, + { + image: "busybox:latest", + labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + containerUser: "root", + expectedUsername: "root", + expectedUserShell: "/bin/sh", + }, + { + image: "codercom/enterprise-minimal:ubuntu", + labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + expectedUsername: "coder", + expectedUserShell: "/bin/bash", + }, + { + image: "codercom/enterprise-minimal:ubuntu", + labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + containerUser: "coder", + expectedUsername: "coder", + expectedUserShell: "/bin/bash", + }, + { + image: "codercom/enterprise-minimal:ubuntu", + labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar", "MULTILINE": "foo\nbar\nbaz"}}]`}, + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + containerUser: "root", + expectedUsername: "root", + expectedUserShell: "/bin/bash", + }, + { + image: "codercom/enterprise-minimal:ubuntu", + labels: map[string]string{`devcontainer.metadata`: `[{"remoteEnv": {"FOO": "bar"}},{"remoteEnv": {"MULTILINE": "foo\nbar\nbaz"}}]`}, + expectedEnv: []string{"FOO=bar", "MULTILINE=foo\nbar\nbaz"}, + containerUser: "root", + expectedUsername: "root", + expectedUserShell: "/bin/bash", + }, + } { + //nolint:paralleltest // variable recapture no longer required + t.Run(fmt.Sprintf("#%d", idx), func(t *testing.T) { + // Start a container with the given image + // and environment variables + image := strings.Split(tt.image, ":")[0] + tag := strings.Split(tt.image, ":")[1] + ct, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: image, + Tag: tag, + Cmd: []string{"sleep", "infinity"}, + Labels: tt.labels, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + require.NoError(t, err, "Could not start test docker container") + t.Logf("Created container %q", ct.Container.Name) + t.Cleanup(func() { + assert.NoError(t, pool.Purge(ct), "Could not purge resource %q", ct.Container.Name) + t.Logf("Purged container %q", ct.Container.Name) + }) - validContainer := codersdk.WorkspaceAgentContainer{ - ID: "container-id", - FriendlyName: "container-name", - Labels: map[string]string{ - agentcontainers.DevcontainerLocalFolderLabel: "/workspace", - agentcontainers.DevcontainerConfigFileLabel: "/workspace/.devcontainer/devcontainer.json", - }, - } + ctx := testutil.Context(t, testutil.WaitShort) + dei, err := agentcontainers.EnvInfo(ctx, agentexec.DefaultExecer, ct.Container.ID, tt.containerUser) + require.NoError(t, err, "Expected no error from DockerEnvInfo()") - missingFolderContainer := codersdk.WorkspaceAgentContainer{ - ID: "missing-folder-container", - FriendlyName: "missing-folder-container", - Labels: map[string]string{}, - } + u, err := dei.User() + require.NoError(t, err, "Expected no error from CurrentUser()") + require.Equal(t, tt.expectedUsername, u.Username, "Expected username to match") - tests := []struct { - name string - containerID string - lister *fakeLister - devcontainerCLI *fakeDevcontainerCLI - wantStatus int - wantBody string - }{ - { - name: "Missing ID", - containerID: "", - lister: &fakeLister{}, - devcontainerCLI: &fakeDevcontainerCLI{}, - wantStatus: http.StatusBadRequest, - wantBody: "Missing container ID or name", - }, - { - name: "List error", - containerID: "container-id", - lister: &fakeLister{ - err: xerrors.New("list error"), - }, - devcontainerCLI: &fakeDevcontainerCLI{}, - wantStatus: http.StatusInternalServerError, - wantBody: "Could not list containers", - }, - { - name: "Container not found", - containerID: "nonexistent-container", - lister: &fakeLister{ - containers: codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{validContainer}, - }, - }, - devcontainerCLI: &fakeDevcontainerCLI{}, - wantStatus: http.StatusNotFound, - wantBody: "Container not found", - }, - { - name: "Missing workspace folder label", - containerID: "missing-folder-container", - lister: &fakeLister{ - containers: codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{missingFolderContainer}, - }, - }, - devcontainerCLI: &fakeDevcontainerCLI{}, - wantStatus: http.StatusBadRequest, - wantBody: "Missing workspace folder label", - }, - { - name: "Devcontainer CLI error", - containerID: "container-id", - lister: &fakeLister{ - containers: codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{validContainer}, - }, - }, - devcontainerCLI: &fakeDevcontainerCLI{ - err: xerrors.New("devcontainer CLI error"), - }, - wantStatus: http.StatusInternalServerError, - wantBody: "Could not recreate devcontainer", - }, - { - name: "OK", - containerID: "container-id", - lister: &fakeLister{ - containers: codersdk.WorkspaceAgentListContainersResponse{ - Containers: []codersdk.WorkspaceAgentContainer{validContainer}, - }, - }, - devcontainerCLI: &fakeDevcontainerCLI{}, - wantStatus: http.StatusNoContent, - wantBody: "", - }, - } + hd, err := dei.HomeDir() + require.NoError(t, err, "Expected no error from UserHomeDir()") + require.NotEmpty(t, hd, "Expected user homedir to be non-empty") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - // Setup router with the handler under test. - r := chi.NewRouter() - handler := agentcontainers.New( - agentcontainers.WithLister(tt.lister), - agentcontainers.WithDevcontainerCLI(tt.devcontainerCLI), - ) - r.Post("/containers/{id}/recreate", handler.Recreate) - - // Simulate HTTP request to the recreate endpoint. - req := httptest.NewRequest(http.MethodPost, "/containers/"+tt.containerID+"/recreate", nil) - rec := httptest.NewRecorder() - r.ServeHTTP(rec, req) - - // Check the response status code and body. - require.Equal(t, tt.wantStatus, rec.Code, "status code mismatch") - if tt.wantBody != "" { - assert.Contains(t, rec.Body.String(), tt.wantBody, "response body mismatch") - } else if tt.wantStatus == http.StatusNoContent { - assert.Empty(t, rec.Body.String(), "expected empty response body") - } - }) - } - }) + sh, err := dei.Shell(tt.containerUser) + require.NoError(t, err, "Expected no error from UserShell()") + require.Equal(t, tt.expectedUserShell, sh, "Expected user shell to match") + + // We don't need to test the actual environment variables here. + environ := dei.Environ() + require.NotEmpty(t, environ, "Expected environ to be non-empty") + + // Test that the environment variables are present in modified command + // output. + envCmd, envArgs := dei.ModifyCommand("env") + for _, env := range tt.expectedEnv { + require.Subset(t, envArgs, []string{"--env", env}) + } + // Run the command in the container and check the output + // HACK: we remove the --tty argument because we're not running in a tty + envArgs = slices.DeleteFunc(envArgs, func(s string) bool { return s == "--tty" }) + stdout, stderr, err := run(ctx, agentexec.DefaultExecer, envCmd, envArgs...) + require.Empty(t, stderr, "Expected no stderr output") + require.NoError(t, err, "Expected no error from running command") + for _, env := range tt.expectedEnv { + require.Contains(t, stdout, env) + } + }) + } +} + +func run(ctx context.Context, execer agentexec.Execer, cmd string, args ...string) (stdout, stderr string, err error) { + var stdoutBuf, stderrBuf strings.Builder + execCmd := execer.CommandContext(ctx, cmd, args...) + execCmd.Stdout = &stdoutBuf + execCmd.Stderr = &stderrBuf + err = execCmd.Run() + stdout = strings.TrimSpace(stdoutBuf.String()) + stderr = strings.TrimSpace(stderrBuf.String()) + return stdout, stderr, err } diff --git a/agent/agentcontainers/devcontainer.go b/agent/agentcontainers/devcontainer.go index f93e0722c75b9..cbf42e150d240 100644 --- a/agent/agentcontainers/devcontainer.go +++ b/agent/agentcontainers/devcontainer.go @@ -8,7 +8,6 @@ import ( "strings" "cdr.dev/slog" - "github.com/coder/coder/v2/codersdk" ) diff --git a/agent/api.go b/agent/api.go index 375338acfab18..bb357d1b87da2 100644 --- a/agent/api.go +++ b/agent/api.go @@ -36,10 +36,15 @@ func (a *agent) apiHandler() http.Handler { ignorePorts: cpy, cacheDuration: cacheDuration, } - ch := agentcontainers.New(agentcontainers.WithLister(a.lister)) + + containerAPI := agentcontainers.NewAPI( + a.logger.Named("containers"), + agentcontainers.WithLister(a.lister), + ) + promHandler := PrometheusMetricsHandler(a.prometheusRegistry, a.logger) - r.Get("/api/v0/containers", ch.List) - r.Post("/api/v0/containers/{id}/recreate", ch.Recreate) + + r.Mount("/api/v0/containers", containerAPI.Routes()) r.Get("/api/v0/listening-ports", lp.handler) r.Get("/api/v0/netcheck", a.HandleNetcheck) r.Post("/api/v0/list-directory", a.HandleLS) From 12dc086628adcd03a38034c5cdce58b190a37051 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 11 Apr 2025 13:09:51 +0400 Subject: [PATCH 063/384] feat: return hostname suffix on AgentConnectionInfo (#17334) Adds the Hostname Suffix to `AgentConnectionInfo` --- the VPN provider will use it to control the suffix for DNS hostnames. part of: #16828 --- coderd/apidoc/docs.go | 3 +++ coderd/apidoc/swagger.json | 3 +++ coderd/workspaceagents.go | 2 ++ coderd/workspaceagents_test.go | 31 +++++++++++++++++++++++++++ codersdk/workspacesdk/workspacesdk.go | 1 + docs/reference/api/agents.md | 3 ++- docs/reference/api/schemas.md | 4 +++- 7 files changed, 45 insertions(+), 2 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index cb2f2f6c22e03..ba1cf6cc30bac 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -18615,6 +18615,9 @@ const docTemplate = `{ }, "disable_direct_connections": { "type": "boolean" + }, + "hostname_suffix": { + "type": "string" } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 90f5729654a95..5a8d199e0a9d2 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -17050,6 +17050,9 @@ }, "disable_direct_connections": { "type": "boolean" + }, + "hostname_suffix": { + "type": "string" } } }, diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 1744c0c6749ca..a4f8ed297b77a 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -882,6 +882,7 @@ func (api *API) workspaceAgentConnection(rw http.ResponseWriter, r *http.Request DERPMap: api.DERPMap(), DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(), DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(), + HostnameSuffix: api.DeploymentValues.WorkspaceHostnameSuffix.Value(), }) } @@ -903,6 +904,7 @@ func (api *API) workspaceAgentConnectionGeneric(rw http.ResponseWriter, r *http. DERPMap: api.DERPMap(), DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(), DisableDirectConnections: api.DeploymentValues.DERP.Config.BlockDirect.Value(), + HostnameSuffix: api.DeploymentValues.WorkspaceHostnameSuffix.Value(), }) } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index 186c66bfd6f8e..a8fe7718f4385 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -2560,3 +2560,34 @@ func requireEqualOrBothNil[T any](t testing.TB, a, b *T) { } require.Equal(t, a, b) } + +func TestAgentConnectionInfo(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + dv := coderdtest.DeploymentValues(t) + dv.WorkspaceHostnameSuffix = "yallah" + dv.DERP.Config.BlockDirect = true + dv.DERP.Config.ForceWebSockets = true + client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{DeploymentValues: dv}) + user := coderdtest.CreateFirstUser(t, client) + r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ + OrganizationID: user.OrganizationID, + OwnerID: user.UserID, + }).WithAgent().Do() + + info, err := workspacesdk.New(client).AgentConnectionInfoGeneric(ctx) + require.NoError(t, err) + require.Equal(t, "yallah", info.HostnameSuffix) + require.True(t, info.DisableDirectConnections) + require.True(t, info.DERPForceWebSockets) + + ws, err := client.Workspace(ctx, r.Workspace.ID) + require.NoError(t, err) + agnt := ws.LatestBuild.Resources[0].Agents[0] + info, err = workspacesdk.New(client).AgentConnectionInfo(ctx, agnt.ID) + require.NoError(t, err) + require.Equal(t, "yallah", info.HostnameSuffix) + require.True(t, info.DisableDirectConnections) + require.True(t, info.DERPForceWebSockets) +} diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index ca4a3d48d7ef2..82ae7904f8046 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -143,6 +143,7 @@ type AgentConnectionInfo struct { DERPMap *tailcfg.DERPMap `json:"derp_map"` DERPForceWebSockets bool `json:"derp_force_websockets"` DisableDirectConnections bool `json:"disable_direct_connections"` + HostnameSuffix string `json:"hostname_suffix"` } func (c *Client) AgentConnectionInfoGeneric(ctx context.Context) (AgentConnectionInfo, error) { diff --git a/docs/reference/api/agents.md b/docs/reference/api/agents.md index 8faba29cf7ba5..853cb67e38bfd 100644 --- a/docs/reference/api/agents.md +++ b/docs/reference/api/agents.md @@ -698,7 +698,8 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent}/con } } }, - "disable_direct_connections": true + "disable_direct_connections": true, + "hostname_suffix": "string" } ``` diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 6e7a2da1a3dea..fb9c3b8db782f 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -11514,7 +11514,8 @@ None } } }, - "disable_direct_connections": true + "disable_direct_connections": true, + "hostname_suffix": "string" } ``` @@ -11525,6 +11526,7 @@ None | `derp_force_websockets` | boolean | false | | | | `derp_map` | [tailcfg.DERPMap](#tailcfgderpmap) | false | | | | `disable_direct_connections` | boolean | false | | | +| `hostname_suffix` | string | false | | | ## wsproxysdk.CryptoKeysResponse From 2c573dc0236d25f6a50137e9495f0659bbe52d32 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 11 Apr 2025 13:24:20 +0400 Subject: [PATCH 064/384] feat: vpn uses WorkspaceHostnameSuffix for DNS names (#17335) Use the hostname suffix to set DNS names as programmed into the DNS service and returned by the vpn `Tunnel`. part of: #16828 --- codersdk/workspacesdk/workspacesdk.go | 2 +- tailnet/conn.go | 4 +- tailnet/controllers.go | 49 +++-- tailnet/controllers_test.go | 71 ++++--- vpn/client.go | 7 +- vpn/client_test.go | 285 +++++++++++++++----------- 6 files changed, 247 insertions(+), 171 deletions(-) diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index 82ae7904f8046..df851e5ac31e9 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -143,7 +143,7 @@ type AgentConnectionInfo struct { DERPMap *tailcfg.DERPMap `json:"derp_map"` DERPForceWebSockets bool `json:"derp_force_websockets"` DisableDirectConnections bool `json:"disable_direct_connections"` - HostnameSuffix string `json:"hostname_suffix"` + HostnameSuffix string `json:"hostname_suffix,omitempty"` } func (c *Client) AgentConnectionInfoGeneric(ctx context.Context) (AgentConnectionInfo, error) { diff --git a/tailnet/conn.go b/tailnet/conn.go index 89b3b7d483d0c..0a1ee1977e98b 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -357,9 +357,7 @@ func NewConn(options *Options) (conn *Conn, err error) { // A FQDN to be mapped to `tsaddr.CoderServiceIPv6`. This address can be used // when you want to know if Coder Connect is running, but are not trying to // connect to a specific known workspace. -const IsCoderConnectEnabledFQDNString = "is.coder--connect--enabled--right--now.coder." - -var IsCoderConnectEnabledFQDN, _ = dnsname.ToFQDN(IsCoderConnectEnabledFQDNString) +const IsCoderConnectEnabledFmtString = "is.coder--connect--enabled--right--now.%s." type ServicePrefix [6]byte diff --git a/tailnet/controllers.go b/tailnet/controllers.go index 7a077ffabfaa0..b5f37311a0f71 100644 --- a/tailnet/controllers.go +++ b/tailnet/controllers.go @@ -864,11 +864,12 @@ func (r *basicResumeTokenRefresher) refresh() { } type TunnelAllWorkspaceUpdatesController struct { - coordCtrl *TunnelSrcCoordController - dnsHostSetter DNSHostsSetter - updateHandler UpdatesHandler - ownerUsername string - logger slog.Logger + coordCtrl *TunnelSrcCoordController + dnsHostSetter DNSHostsSetter + dnsNameOptions DNSNameOptions + updateHandler UpdatesHandler + ownerUsername string + logger slog.Logger mu sync.Mutex updater *tunnelUpdater @@ -883,12 +884,16 @@ type Workspace struct { agents map[uuid.UUID]*Agent } +type DNSNameOptions struct { + Suffix string +} + // updateDNSNames updates the DNS names for all agents in the workspace. // DNS hosts must be all lowercase, or the resolver won't be able to find them. // Usernames are globally unique & case-insensitive. // Workspace names are unique per-user & case-insensitive. // Agent names are unique per-workspace & case-insensitive. -func (w *Workspace) updateDNSNames() error { +func (w *Workspace) updateDNSNames(options DNSNameOptions) error { wsName := strings.ToLower(w.Name) username := strings.ToLower(w.ownerUsername) for id, a := range w.agents { @@ -896,24 +901,22 @@ func (w *Workspace) updateDNSNames() error { names := make(map[dnsname.FQDN][]netip.Addr) // TODO: technically, DNS labels cannot start with numbers, but the rules are often not // strictly enforced. - fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.%s.me.coder.", agentName, wsName)) + fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.%s.me.%s.", agentName, wsName, options.Suffix)) if err != nil { return err } names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)} - fqdn, err = dnsname.ToFQDN(fmt.Sprintf("%s.%s.%s.coder.", agentName, wsName, username)) + fqdn, err = dnsname.ToFQDN(fmt.Sprintf("%s.%s.%s.%s.", agentName, wsName, username, options.Suffix)) if err != nil { return err } names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)} if len(w.agents) == 1 { - fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%s.coder.", wsName)) + fqdn, err = dnsname.ToFQDN(fmt.Sprintf("%s.%s.", wsName, options.Suffix)) if err != nil { return err } - for _, a := range w.agents { - names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)} - } + names[fqdn] = []netip.Addr{CoderServicePrefix.AddrFromUUID(a.ID)} } a.Hosts = names w.agents[id] = a @@ -950,6 +953,7 @@ func (t *TunnelAllWorkspaceUpdatesController) New(client WorkspaceUpdatesClient) logger: t.logger, coordCtrl: t.coordCtrl, dnsHostsSetter: t.dnsHostSetter, + dnsNameOptions: t.dnsNameOptions, updateHandler: t.updateHandler, ownerUsername: t.ownerUsername, recvLoopDone: make(chan struct{}), @@ -996,6 +1000,7 @@ type tunnelUpdater struct { updateHandler UpdatesHandler ownerUsername string recvLoopDone chan struct{} + dnsNameOptions DNSNameOptions sync.Mutex workspaces map[uuid.UUID]*Workspace @@ -1250,7 +1255,7 @@ func (t *tunnelUpdater) allAgentIDsLocked() []uuid.UUID { func (t *tunnelUpdater) updateDNSNamesLocked() map[dnsname.FQDN][]netip.Addr { names := make(map[dnsname.FQDN][]netip.Addr) for _, w := range t.workspaces { - err := w.updateDNSNames() + err := w.updateDNSNames(t.dnsNameOptions) if err != nil { // This should never happen in production, because converting the FQDN only fails // if names are too long, and we put strict length limits on agent, workspace, and user @@ -1258,6 +1263,7 @@ func (t *tunnelUpdater) updateDNSNamesLocked() map[dnsname.FQDN][]netip.Addr { t.logger.Critical(context.Background(), "failed to include DNS name(s)", slog.F("workspace_id", w.ID), + slog.F("suffix", t.dnsNameOptions.Suffix), slog.Error(err)) } for _, a := range w.agents { @@ -1266,7 +1272,13 @@ func (t *tunnelUpdater) updateDNSNamesLocked() map[dnsname.FQDN][]netip.Addr { } } } - names[IsCoderConnectEnabledFQDN] = []netip.Addr{tsaddr.CoderServiceIPv6()} + isCoderConnectEnabledFQDN, err := dnsname.ToFQDN(fmt.Sprintf(IsCoderConnectEnabledFmtString, t.dnsNameOptions.Suffix)) + if err != nil { + t.logger.Critical(context.Background(), + "failed to include Coder Connect enabled DNS name", slog.F("suffix", t.dnsNameOptions.Suffix)) + } else { + names[isCoderConnectEnabledFQDN] = []netip.Addr{tsaddr.CoderServiceIPv6()} + } return names } @@ -1274,10 +1286,11 @@ type TunnelAllOption func(t *TunnelAllWorkspaceUpdatesController) // WithDNS configures the tunnelAllWorkspaceUpdatesController to set DNS names for all workspaces // and agents it learns about. -func WithDNS(d DNSHostsSetter, ownerUsername string) TunnelAllOption { +func WithDNS(d DNSHostsSetter, ownerUsername string, options DNSNameOptions) TunnelAllOption { return func(t *TunnelAllWorkspaceUpdatesController) { t.dnsHostSetter = d t.ownerUsername = ownerUsername + t.dnsNameOptions = options } } @@ -1293,7 +1306,11 @@ func WithHandler(h UpdatesHandler) TunnelAllOption { func NewTunnelAllWorkspaceUpdatesController( logger slog.Logger, c *TunnelSrcCoordController, opts ...TunnelAllOption, ) *TunnelAllWorkspaceUpdatesController { - t := &TunnelAllWorkspaceUpdatesController{logger: logger, coordCtrl: c} + t := &TunnelAllWorkspaceUpdatesController{ + logger: logger, + coordCtrl: c, + dnsNameOptions: DNSNameOptions{"coder"}, + } for _, opt := range opts { opt(t) } diff --git a/tailnet/controllers_test.go b/tailnet/controllers_test.go index 3cfa47e3adca2..089d1b1e82a29 100644 --- a/tailnet/controllers_test.go +++ b/tailnet/controllers_test.go @@ -1522,7 +1522,7 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) { fUH := newFakeUpdateHandler(ctx, t) fDNS := newFakeDNSSetter(ctx, t) coordC, updateC, updateCtrl := setupConnectedAllWorkspaceUpdatesController(ctx, t, logger, - tailnet.WithDNS(fDNS, "testy"), + tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: "mctest"}), tailnet.WithHandler(fUH), ) @@ -1562,16 +1562,19 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) { w2a1IP := netip.MustParseAddr("fd60:627a:a42b:0201::") w2a2IP := netip.MustParseAddr("fd60:627a:a42b:0202::") + expectedCoderConnectFQDN, err := dnsname.ToFQDN(fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "mctest")) + require.NoError(t, err) + // Also triggers setting DNS hosts expectedDNS := map[dnsname.FQDN][]netip.Addr{ - "w1a1.w1.me.coder.": {ws1a1IP}, - "w2a1.w2.me.coder.": {w2a1IP}, - "w2a2.w2.me.coder.": {w2a2IP}, - "w1a1.w1.testy.coder.": {ws1a1IP}, - "w2a1.w2.testy.coder.": {w2a1IP}, - "w2a2.w2.testy.coder.": {w2a2IP}, - "w1.coder.": {ws1a1IP}, - tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()}, + "w1a1.w1.me.mctest.": {ws1a1IP}, + "w2a1.w2.me.mctest.": {w2a1IP}, + "w2a2.w2.me.mctest.": {w2a2IP}, + "w1a1.w1.testy.mctest.": {ws1a1IP}, + "w2a1.w2.testy.mctest.": {w2a1IP}, + "w2a2.w2.testy.mctest.": {w2a2IP}, + "w1.mctest.": {ws1a1IP}, + expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()}, } dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) @@ -1586,23 +1589,23 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) { { ID: w1a1ID, Name: "w1a1", WorkspaceID: w1ID, Hosts: map[dnsname.FQDN][]netip.Addr{ - "w1.coder.": {ws1a1IP}, - "w1a1.w1.me.coder.": {ws1a1IP}, - "w1a1.w1.testy.coder.": {ws1a1IP}, + "w1.mctest.": {ws1a1IP}, + "w1a1.w1.me.mctest.": {ws1a1IP}, + "w1a1.w1.testy.mctest.": {ws1a1IP}, }, }, { ID: w2a1ID, Name: "w2a1", WorkspaceID: w2ID, Hosts: map[dnsname.FQDN][]netip.Addr{ - "w2a1.w2.me.coder.": {w2a1IP}, - "w2a1.w2.testy.coder.": {w2a1IP}, + "w2a1.w2.me.mctest.": {w2a1IP}, + "w2a1.w2.testy.mctest.": {w2a1IP}, }, }, { ID: w2a2ID, Name: "w2a2", WorkspaceID: w2ID, Hosts: map[dnsname.FQDN][]netip.Addr{ - "w2a2.w2.me.coder.": {w2a2IP}, - "w2a2.w2.testy.coder.": {w2a2IP}, + "w2a2.w2.me.mctest.": {w2a2IP}, + "w2a2.w2.testy.mctest.": {w2a2IP}, }, }, }, @@ -1634,7 +1637,7 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { fUH := newFakeUpdateHandler(ctx, t) fDNS := newFakeDNSSetter(ctx, t) coordC, updateC, updateCtrl := setupConnectedAllWorkspaceUpdatesController(ctx, t, logger, - tailnet.WithDNS(fDNS, "testy"), + tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: "coder"}), tailnet.WithHandler(fUH), ) @@ -1661,12 +1664,15 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { require.Equal(t, w1a1ID[:], coordCall.req.GetAddTunnel().GetId()) testutil.RequireSendCtx(ctx, t, coordCall.err, nil) + expectedCoderConnectFQDN, err := dnsname.ToFQDN(fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "coder")) + require.NoError(t, err) + // DNS for w1a1 expectedDNS := map[dnsname.FQDN][]netip.Addr{ - "w1a1.w1.testy.coder.": {ws1a1IP}, - "w1a1.w1.me.coder.": {ws1a1IP}, - "w1.coder.": {ws1a1IP}, - tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()}, + "w1a1.w1.testy.coder.": {ws1a1IP}, + "w1a1.w1.me.coder.": {ws1a1IP}, + "w1.coder.": {ws1a1IP}, + expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()}, } dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) @@ -1719,10 +1725,10 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { // DNS contains only w1a2 expectedDNS = map[dnsname.FQDN][]netip.Addr{ - "w1a2.w1.testy.coder.": {ws1a2IP}, - "w1a2.w1.me.coder.": {ws1a2IP}, - "w1.coder.": {ws1a2IP}, - tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()}, + "w1a2.w1.testy.coder.": {ws1a2IP}, + "w1a2.w1.me.coder.": {ws1a2IP}, + "w1.coder.": {ws1a2IP}, + expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()}, } dnsCall = testutil.RequireRecvCtx(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) @@ -1779,7 +1785,7 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) { fConn := &fakeCoordinatee{} tsc := tailnet.NewTunnelSrcCoordController(logger, fConn) uut := tailnet.NewTunnelAllWorkspaceUpdatesController(logger, tsc, - tailnet.WithDNS(fDNS, "testy"), + tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: "coder"}), ) updateC := newFakeWorkspaceUpdateClient(ctx, t) @@ -1800,12 +1806,15 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) { upRecvCall := testutil.RequireRecvCtx(ctx, t, updateC.recv) testutil.RequireSendCtx(ctx, t, upRecvCall.resp, initUp) + expectedCoderConnectFQDN, err := dnsname.ToFQDN(fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "coder")) + require.NoError(t, err) + // DNS for w1a1 expectedDNS := map[dnsname.FQDN][]netip.Addr{ - "w1a1.w1.me.coder.": {ws1a1IP}, - "w1a1.w1.testy.coder.": {ws1a1IP}, - "w1.coder.": {ws1a1IP}, - tailnet.IsCoderConnectEnabledFQDNString: {tsaddr.CoderServiceIPv6()}, + "w1a1.w1.me.coder.": {ws1a1IP}, + "w1a1.w1.testy.coder.": {ws1a1IP}, + "w1.coder.": {ws1a1IP}, + expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()}, } dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) @@ -1816,7 +1825,7 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) { testutil.RequireSendCtx(ctx, t, closeCall, io.EOF) // error should be our initial DNS error - err := testutil.RequireRecvCtx(ctx, t, updateCW.Wait()) + err = testutil.RequireRecvCtx(ctx, t, updateCW.Wait()) require.ErrorIs(t, err, dnsError) } diff --git a/vpn/client.go b/vpn/client.go index 882197165e9ea..85e0d45c3d6f8 100644 --- a/vpn/client.go +++ b/vpn/client.go @@ -107,6 +107,11 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string if err != nil { return nil, xerrors.Errorf("get connection info: %w", err) } + // default to DNS suffix of "coder" if the server hasn't set it (might be too old). + dnsNameOptions := tailnet.DNSNameOptions{Suffix: "coder"} + if connInfo.HostnameSuffix != "" { + dnsNameOptions.Suffix = connInfo.HostnameSuffix + } headers.Set(codersdk.SessionTokenHeader, token) dialer := workspacesdk.NewWebsocketDialer(options.Logger, rpcURL, &websocket.DialOptions{ @@ -148,7 +153,7 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string updatesCtrl := tailnet.NewTunnelAllWorkspaceUpdatesController( options.Logger, coordCtrl, - tailnet.WithDNS(conn, me.Username), + tailnet.WithDNS(conn, me.Username, dnsNameOptions), tailnet.WithHandler(options.UpdateHandler), ) controller.WorkspaceUpdatesCtrl = updatesCtrl diff --git a/vpn/client_test.go b/vpn/client_test.go index a1166eeaabe70..41602d1ffa79f 100644 --- a/vpn/client_test.go +++ b/vpn/client_test.go @@ -3,11 +3,14 @@ package vpn_test import ( "net/http" "net/http/httptest" + "net/netip" "net/url" "sync/atomic" "testing" "time" + "tailscale.com/util/dnsname" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,136 +32,180 @@ import ( func TestClient_WorkspaceUpdates(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) - logger := testutil.Logger(t) - userID := uuid.UUID{1} wsID := uuid.UUID{2} peerID := uuid.UUID{3} - - fCoord := tailnettest.NewFakeCoordinator() - var coord tailnet.Coordinator = fCoord - coordPtr := atomic.Pointer[tailnet.Coordinator]{} - coordPtr.Store(&coord) - ctrl := gomock.NewController(t) - mProvider := tailnettest.NewMockWorkspaceUpdatesProvider(ctrl) - - mSub := tailnettest.NewMockSubscription(ctrl) - outUpdateCh := make(chan *proto.WorkspaceUpdate, 1) - inUpdateCh := make(chan tailnet.WorkspaceUpdate, 1) - mProvider.EXPECT().Subscribe(gomock.Any(), userID).Times(1).Return(mSub, nil) - mSub.EXPECT().Updates().MinTimes(1).Return(outUpdateCh) - mSub.EXPECT().Close().Times(1).Return(nil) - - svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ - Logger: logger, - CoordPtr: &coordPtr, - DERPMapUpdateFrequency: time.Hour, - DERPMapFn: func() *tailcfg.DERPMap { return &tailcfg.DERPMap{} }, - WorkspaceUpdatesProvider: mProvider, - ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), - }) - require.NoError(t, err) - - user := make(chan struct{}) - connInfo := make(chan struct{}) - serveErrCh := make(chan error) - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/api/v2/users/me": - httpapi.Write(ctx, w, http.StatusOK, codersdk.User{ - ReducedUser: codersdk.ReducedUser{ - MinimalUser: codersdk.MinimalUser{ - ID: userID, + agentID := uuid.UUID{4} + + testCases := []struct { + name string + agentConnectionInfo workspacesdk.AgentConnectionInfo + hostnames []string + }{ + { + name: "empty", + agentConnectionInfo: workspacesdk.AgentConnectionInfo{}, + hostnames: []string{"wrk.coder.", "agnt.wrk.me.coder.", "agnt.wrk.rootbeer.coder."}, + }, + { + name: "suffix", + agentConnectionInfo: workspacesdk.AgentConnectionInfo{HostnameSuffix: "float"}, + hostnames: []string{"wrk.float.", "agnt.wrk.me.float.", "agnt.wrk.rootbeer.float."}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + logger := testutil.Logger(t) + + fCoord := tailnettest.NewFakeCoordinator() + var coord tailnet.Coordinator = fCoord + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + ctrl := gomock.NewController(t) + mProvider := tailnettest.NewMockWorkspaceUpdatesProvider(ctrl) + + mSub := tailnettest.NewMockSubscription(ctrl) + outUpdateCh := make(chan *proto.WorkspaceUpdate, 1) + inUpdateCh := make(chan tailnet.WorkspaceUpdate, 1) + mProvider.EXPECT().Subscribe(gomock.Any(), userID).Times(1).Return(mSub, nil) + mSub.EXPECT().Updates().MinTimes(1).Return(outUpdateCh) + mSub.EXPECT().Close().Times(1).Return(nil) + + svc, err := tailnet.NewClientService(tailnet.ClientServiceOptions{ + Logger: logger, + CoordPtr: &coordPtr, + DERPMapUpdateFrequency: time.Hour, + DERPMapFn: func() *tailcfg.DERPMap { return &tailcfg.DERPMap{} }, + WorkspaceUpdatesProvider: mProvider, + ResumeTokenProvider: tailnet.NewInsecureTestResumeTokenProvider(), + }) + require.NoError(t, err) + + user := make(chan struct{}) + connInfo := make(chan struct{}) + serveErrCh := make(chan error) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v2/users/me": + httpapi.Write(ctx, w, http.StatusOK, codersdk.User{ + ReducedUser: codersdk.ReducedUser{ + MinimalUser: codersdk.MinimalUser{ + ID: userID, + Username: "rootbeer", + }, + }, + }) + user <- struct{}{} + + case "/api/v2/workspaceagents/connection": + httpapi.Write(ctx, w, http.StatusOK, tc.agentConnectionInfo) + connInfo <- struct{}{} + + case "/api/v2/tailnet": + // need 2.3 for WorkspaceUpdates RPC + cVer := r.URL.Query().Get("version") + assert.Equal(t, "2.3", cVer) + + sws, err := websocket.Accept(w, r, nil) + if !assert.NoError(t, err) { + return + } + wsCtx, nc := codersdk.WebsocketNetConn(ctx, sws, websocket.MessageBinary) + serveErrCh <- svc.ServeConnV2(wsCtx, nc, tailnet.StreamID{ + Name: "client", + ID: peerID, + // Auth can be nil as we use a mock update provider + Auth: tailnet.ClientUserCoordinateeAuth{ + Auth: nil, + }, + }) + default: + http.NotFound(w, r) + } + })) + t.Cleanup(server.Close) + + svrURL, err := url.Parse(server.URL) + require.NoError(t, err) + connErrCh := make(chan error) + connCh := make(chan vpn.Conn) + go func() { + conn, err := vpn.NewClient().NewConn(ctx, svrURL, "fakeToken", &vpn.Options{ + UpdateHandler: updateHandler(func(wu tailnet.WorkspaceUpdate) error { + inUpdateCh <- wu + return nil + }), + DNSConfigurator: &noopConfigurator{}, + }) + connErrCh <- err + connCh <- conn + }() + testutil.RequireRecvCtx(ctx, t, user) + testutil.RequireRecvCtx(ctx, t, connInfo) + err = testutil.RequireRecvCtx(ctx, t, connErrCh) + require.NoError(t, err) + conn := testutil.RequireRecvCtx(ctx, t, connCh) + + // Send a workspace update + update := &proto.WorkspaceUpdate{ + UpsertedWorkspaces: []*proto.Workspace{ + { + Id: wsID[:], + Name: "wrk", }, }, - }) - user <- struct{}{} - - case "/api/v2/workspaceagents/connection": - httpapi.Write(ctx, w, http.StatusOK, workspacesdk.AgentConnectionInfo{ - DisableDirectConnections: false, - }) - connInfo <- struct{}{} + UpsertedAgents: []*proto.Agent{ + { + Id: agentID[:], + Name: "agnt", + WorkspaceId: wsID[:], + }, + }, + } + testutil.RequireSendCtx(ctx, t, outUpdateCh, update) - case "/api/v2/tailnet": - // need 2.3 for WorkspaceUpdates RPC - cVer := r.URL.Query().Get("version") - assert.Equal(t, "2.3", cVer) + // It'll be received by the update handler + recvUpdate := testutil.RequireRecvCtx(ctx, t, inUpdateCh) + require.Len(t, recvUpdate.UpsertedWorkspaces, 1) + require.Equal(t, wsID, recvUpdate.UpsertedWorkspaces[0].ID) + require.Len(t, recvUpdate.UpsertedAgents, 1) - sws, err := websocket.Accept(w, r, nil) - if !assert.NoError(t, err) { - return + expectedHosts := map[dnsname.FQDN][]netip.Addr{} + for _, name := range tc.hostnames { + expectedHosts[dnsname.FQDN(name)] = []netip.Addr{tailnet.CoderServicePrefix.AddrFromUUID(agentID)} } - wsCtx, nc := codersdk.WebsocketNetConn(ctx, sws, websocket.MessageBinary) - serveErrCh <- svc.ServeConnV2(wsCtx, nc, tailnet.StreamID{ - Name: "client", - ID: peerID, - // Auth can be nil as we use a mock update provider - Auth: tailnet.ClientUserCoordinateeAuth{ - Auth: nil, + + // And be reflected on the Conn's state + state, err := conn.CurrentWorkspaceState() + require.NoError(t, err) + require.Equal(t, tailnet.WorkspaceUpdate{ + UpsertedWorkspaces: []*tailnet.Workspace{ + { + ID: wsID, + Name: "wrk", + }, }, - }) - default: - http.NotFound(w, r) - } - })) - t.Cleanup(server.Close) - - svrURL, err := url.Parse(server.URL) - require.NoError(t, err) - connErrCh := make(chan error) - connCh := make(chan vpn.Conn) - go func() { - conn, err := vpn.NewClient().NewConn(ctx, svrURL, "fakeToken", &vpn.Options{ - UpdateHandler: updateHandler(func(wu tailnet.WorkspaceUpdate) error { - inUpdateCh <- wu - return nil - }), - DNSConfigurator: &noopConfigurator{}, + UpsertedAgents: []*tailnet.Agent{ + { + ID: agentID, + Name: "agnt", + WorkspaceID: wsID, + Hosts: expectedHosts, + }, + }, + DeletedWorkspaces: []*tailnet.Workspace{}, + DeletedAgents: []*tailnet.Agent{}, + }, state) + + // Close the conn + conn.Close() + err = testutil.RequireRecvCtx(ctx, t, serveErrCh) + require.NoError(t, err) }) - connErrCh <- err - connCh <- conn - }() - testutil.RequireRecvCtx(ctx, t, user) - testutil.RequireRecvCtx(ctx, t, connInfo) - err = testutil.RequireRecvCtx(ctx, t, connErrCh) - require.NoError(t, err) - conn := testutil.RequireRecvCtx(ctx, t, connCh) - - // Send a workspace update - update := &proto.WorkspaceUpdate{ - UpsertedWorkspaces: []*proto.Workspace{ - { - Id: wsID[:], - }, - }, } - testutil.RequireSendCtx(ctx, t, outUpdateCh, update) - - // It'll be received by the update handler - recvUpdate := testutil.RequireRecvCtx(ctx, t, inUpdateCh) - require.Len(t, recvUpdate.UpsertedWorkspaces, 1) - require.Equal(t, wsID, recvUpdate.UpsertedWorkspaces[0].ID) - - // And be reflected on the Conn's state - state, err := conn.CurrentWorkspaceState() - require.NoError(t, err) - require.Equal(t, tailnet.WorkspaceUpdate{ - UpsertedWorkspaces: []*tailnet.Workspace{ - { - ID: wsID, - }, - }, - UpsertedAgents: []*tailnet.Agent{}, - DeletedWorkspaces: []*tailnet.Workspace{}, - DeletedAgents: []*tailnet.Agent{}, - }, state) - - // Close the conn - conn.Close() - err = testutil.RequireRecvCtx(ctx, t, serveErrCh) - require.NoError(t, err) } type updateHandler func(tailnet.WorkspaceUpdate) error From 12355506377080ed2dfa091636da6f4fb4c4a503 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 11 Apr 2025 10:24:45 +0100 Subject: [PATCH 065/384] feat(codersdk): add toolsdk and replace existing mcp server tool impl (#17343) - Refactors existing `mcp` package to use `kylecarbs/aisdk-go` and moves to `codersdk/toolsdk` package. - Updates existing MCP server implementation to use `codersdk/toolsdk` Co-authored-by: Kyle Carberry --- cli/exp_mcp.go | 91 ++- cli/exp_mcp_test.go | 7 +- coderd/database/dbfake/dbfake.go | 51 +- codersdk/toolsdk/toolsdk.go | 1244 ++++++++++++++++++++++++++++++ codersdk/toolsdk/toolsdk_test.go | 367 +++++++++ go.mod | 35 +- go.sum | 78 +- mcp/mcp.go | 600 -------------- mcp/mcp_test.go | 397 ---------- 9 files changed, 1774 insertions(+), 1096 deletions(-) create mode 100644 codersdk/toolsdk/toolsdk.go create mode 100644 codersdk/toolsdk/toolsdk_test.go delete mode 100644 mcp/mcp.go delete mode 100644 mcp/mcp_test.go diff --git a/cli/exp_mcp.go b/cli/exp_mcp.go index 2726f2a3d53cc..8b8c96ab41863 100644 --- a/cli/exp_mcp.go +++ b/cli/exp_mcp.go @@ -6,19 +6,19 @@ import ( "errors" "os" "path/filepath" + "slices" "strings" + "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/spf13/afero" "golang.org/x/xerrors" - "cdr.dev/slog" - "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" - codermcp "github.com/coder/coder/v2/mcp" + "github.com/coder/coder/v2/codersdk/toolsdk" "github.com/coder/serpent" ) @@ -365,6 +365,8 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct ctx, cancel := context.WithCancel(inv.Context()) defer cancel() + fs := afero.NewOsFs() + me, err := client.User(ctx, codersdk.Me) if err != nil { cliui.Errorf(inv.Stderr, "Failed to log in to the Coder deployment.") @@ -397,40 +399,36 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct server.WithInstructions(instructions), ) - // Create a separate logger for the tools. - toolLogger := slog.Make(sloghuman.Sink(invStderr)) - - toolDeps := codermcp.ToolDeps{ - Client: client, - Logger: &toolLogger, - AppStatusSlug: appStatusSlug, - AgentClient: agentsdk.New(client.URL), - } - + // Create a new context for the tools with all relevant information. + clientCtx := toolsdk.WithClient(ctx, client) // Get the workspace agent token from the environment. - agentToken, ok := os.LookupEnv("CODER_AGENT_TOKEN") - if ok && agentToken != "" { - toolDeps.AgentClient.SetSessionToken(agentToken) + if agentToken, err := getAgentToken(fs); err == nil && agentToken != "" { + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(agentToken) + clientCtx = toolsdk.WithAgentClient(clientCtx, agentClient) } else { cliui.Warnf(inv.Stderr, "CODER_AGENT_TOKEN is not set, task reporting will not be available") } - if appStatusSlug == "" { + if appStatusSlug != "" { cliui.Warnf(inv.Stderr, "CODER_MCP_APP_STATUS_SLUG is not set, task reporting will not be available.") + } else { + clientCtx = toolsdk.WithWorkspaceAppStatusSlug(clientCtx, appStatusSlug) } // Register tools based on the allowlist (if specified) - reg := codermcp.AllTools() - if len(allowedTools) > 0 { - reg = reg.WithOnlyAllowed(allowedTools...) + for _, tool := range toolsdk.All { + if len(allowedTools) == 0 || slices.ContainsFunc(allowedTools, func(t string) bool { + return t == tool.Tool.Name + }) { + mcpSrv.AddTools(mcpFromSDK(tool)) + } } - reg.Register(mcpSrv, toolDeps) - srv := server.NewStdioServer(mcpSrv) done := make(chan error) go func() { defer close(done) - srvErr := srv.Listen(ctx, invStdin, invStdout) + srvErr := srv.Listen(clientCtx, invStdin, invStdout) done <- srvErr }() @@ -527,8 +525,8 @@ func configureClaude(fs afero.Fs, cfg ClaudeConfig) error { if !ok { mcpServers = make(map[string]any) } - for name, mcp := range cfg.MCPServers { - mcpServers[name] = mcp + for name, cfgmcp := range cfg.MCPServers { + mcpServers[name] = cfgmcp } project["mcpServers"] = mcpServers // Prevents Claude from asking the user to complete the project onboarding. @@ -674,7 +672,7 @@ func indexOf(s, substr string) int { func getAgentToken(fs afero.Fs) (string, error) { token, ok := os.LookupEnv("CODER_AGENT_TOKEN") - if ok { + if ok && token != "" { return token, nil } tokenFile, ok := os.LookupEnv("CODER_AGENT_TOKEN_FILE") @@ -687,3 +685,44 @@ func getAgentToken(fs afero.Fs) (string, error) { } return string(bs), nil } + +// mcpFromSDK adapts a toolsdk.Tool to go-mcp's server.ServerTool. +// It assumes that the tool responds with a valid JSON object. +func mcpFromSDK(sdkTool toolsdk.Tool[any]) server.ServerTool { + return server.ServerTool{ + Tool: mcp.Tool{ + Name: sdkTool.Tool.Name, + Description: sdkTool.Description, + InputSchema: mcp.ToolInputSchema{ + Type: "object", // Default of mcp.NewTool() + Properties: sdkTool.Schema.Properties, + Required: sdkTool.Schema.Required, + }, + }, + Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + result, err := sdkTool.Handler(ctx, request.Params.Arguments) + if err != nil { + return nil, err + } + var sb strings.Builder + if err := json.NewEncoder(&sb).Encode(result); err == nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(sb.String()), + }, + }, nil + } + // If the result is not JSON, return it as a string. + // This is a fallback for tools that return non-JSON data. + resultStr, ok := result.(string) + if !ok { + return nil, xerrors.Errorf("tool call result is neither valid JSON or a string, got: %T", result) + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(resultStr), + }, + }, nil + }, + } +} diff --git a/cli/exp_mcp_test.go b/cli/exp_mcp_test.go index 20ced5761f42c..0151021579814 100644 --- a/cli/exp_mcp_test.go +++ b/cli/exp_mcp_test.go @@ -39,12 +39,13 @@ func TestExpMcpServer(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) // Given: we run the exp mcp command with allowed tools set - inv, root := clitest.New(t, "exp", "mcp", "server", "--allowed-tools=coder_whoami,coder_list_templates") + inv, root := clitest.New(t, "exp", "mcp", "server", "--allowed-tools=coder_get_authenticated_user") inv = inv.WithContext(cancelCtx) pty := ptytest.New(t) inv.Stdin = pty.Input() inv.Stdout = pty.Output() + // nolint: gocritic // not the focus of this test clitest.SetupConfig(t, client, root) cmdDone := make(chan struct{}) @@ -73,13 +74,13 @@ func TestExpMcpServer(t *testing.T) { } err := json.Unmarshal([]byte(output), &toolsResponse) require.NoError(t, err) - require.Len(t, toolsResponse.Result.Tools, 2, "should have exactly 2 tools") + require.Len(t, toolsResponse.Result.Tools, 1, "should have exactly 1 tool") foundTools := make([]string, 0, 2) for _, tool := range toolsResponse.Result.Tools { foundTools = append(foundTools, tool.Name) } slices.Sort(foundTools) - require.Equal(t, []string{"coder_list_templates", "coder_whoami"}, foundTools) + require.Equal(t, []string{"coder_get_authenticated_user"}, foundTools) }) t.Run("OK", func(t *testing.T) { diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 197502ebac42c..abadd78f07b36 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -287,23 +287,25 @@ type TemplateVersionResponse struct { } type TemplateVersionBuilder struct { - t testing.TB - db database.Store - seed database.TemplateVersion - fileID uuid.UUID - ps pubsub.Pubsub - resources []*sdkproto.Resource - params []database.TemplateVersionParameter - promote bool + t testing.TB + db database.Store + seed database.TemplateVersion + fileID uuid.UUID + ps pubsub.Pubsub + resources []*sdkproto.Resource + params []database.TemplateVersionParameter + promote bool + autoCreateTemplate bool } // TemplateVersion generates a template version and optionally a parent // template if no template ID is set on the seed. func TemplateVersion(t testing.TB, db database.Store) TemplateVersionBuilder { return TemplateVersionBuilder{ - t: t, - db: db, - promote: true, + t: t, + db: db, + promote: true, + autoCreateTemplate: true, } } @@ -337,6 +339,13 @@ func (t TemplateVersionBuilder) Params(ps ...database.TemplateVersionParameter) return t } +func (t TemplateVersionBuilder) SkipCreateTemplate() TemplateVersionBuilder { + // nolint: revive // returns modified struct + t.autoCreateTemplate = false + t.promote = false + return t +} + func (t TemplateVersionBuilder) Do() TemplateVersionResponse { t.t.Helper() @@ -347,7 +356,7 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse { t.fileID = takeFirst(t.fileID, uuid.New()) var resp TemplateVersionResponse - if t.seed.TemplateID.UUID == uuid.Nil { + if t.seed.TemplateID.UUID == uuid.Nil && t.autoCreateTemplate { resp.Template = dbgen.Template(t.t, t.db, database.Template{ ActiveVersionID: t.seed.ID, OrganizationID: t.seed.OrganizationID, @@ -360,16 +369,14 @@ func (t TemplateVersionBuilder) Do() TemplateVersionResponse { } version := dbgen.TemplateVersion(t.t, t.db, t.seed) - - // Always make this version the active version. We can easily - // add a conditional to the builder to opt out of this when - // necessary. - err := t.db.UpdateTemplateActiveVersionByID(ownerCtx, database.UpdateTemplateActiveVersionByIDParams{ - ID: t.seed.TemplateID.UUID, - ActiveVersionID: t.seed.ID, - UpdatedAt: dbtime.Now(), - }) - require.NoError(t.t, err) + if t.promote { + err := t.db.UpdateTemplateActiveVersionByID(ownerCtx, database.UpdateTemplateActiveVersionByIDParams{ + ID: t.seed.TemplateID.UUID, + ActiveVersionID: t.seed.ID, + UpdatedAt: dbtime.Now(), + }) + require.NoError(t.t, err) + } payload, err := json.Marshal(provisionerdserver.TemplateVersionImportJob{ TemplateVersionID: t.seed.ID, diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go new file mode 100644 index 0000000000000..835c37a65180e --- /dev/null +++ b/codersdk/toolsdk/toolsdk.go @@ -0,0 +1,1244 @@ +package toolsdk + +import ( + "archive/tar" + "context" + "io" + + "github.com/google/uuid" + "github.com/kylecarbs/aisdk-go" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" +) + +// HandlerFunc is a function that handles a tool call. +type HandlerFunc[T any] func(ctx context.Context, args map[string]any) (T, error) + +type Tool[T any] struct { + aisdk.Tool + Handler HandlerFunc[T] +} + +// Generic returns a Tool[any] that can be used to call the tool. +func (t Tool[T]) Generic() Tool[any] { + return Tool[any]{ + Tool: t.Tool, + Handler: func(ctx context.Context, args map[string]any) (any, error) { + return t.Handler(ctx, args) + }, + } +} + +var ( + // All is a list of all tools that can be used in the Coder CLI. + // When you add a new tool, be sure to include it here! + All = []Tool[any]{ + CreateTemplateVersion.Generic(), + CreateTemplate.Generic(), + CreateWorkspace.Generic(), + CreateWorkspaceBuild.Generic(), + DeleteTemplate.Generic(), + GetAuthenticatedUser.Generic(), + GetTemplateVersionLogs.Generic(), + GetWorkspace.Generic(), + GetWorkspaceAgentLogs.Generic(), + GetWorkspaceBuildLogs.Generic(), + ListWorkspaces.Generic(), + ListTemplates.Generic(), + ListTemplateVersionParameters.Generic(), + ReportTask.Generic(), + UploadTarFile.Generic(), + UpdateTemplateActiveVersion.Generic(), + } + + ReportTask = Tool[string]{ + Tool: aisdk.Tool{ + Name: "coder_report_task", + Description: "Report progress on a user task in Coder.", + Schema: aisdk.Schema{ + Properties: map[string]any{ + "summary": map[string]any{ + "type": "string", + "description": "A concise summary of your current progress on the task. This must be less than 160 characters in length.", + }, + "link": map[string]any{ + "type": "string", + "description": "A link to a relevant resource, such as a PR or issue.", + }, + "emoji": map[string]any{ + "type": "string", + "description": "An emoji that visually represents your current progress. Choose an emoji that helps the user understand your current status at a glance.", + }, + "state": map[string]any{ + "type": "string", + "description": "The state of your task. This can be one of the following: working, complete, or failure. Select the state that best represents your current progress.", + "enum": []string{ + string(codersdk.WorkspaceAppStatusStateWorking), + string(codersdk.WorkspaceAppStatusStateComplete), + string(codersdk.WorkspaceAppStatusStateFailure), + }, + }, + }, + Required: []string{"summary", "link", "emoji", "state"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) (string, error) { + agentClient, err := agentClientFromContext(ctx) + if err != nil { + return "", xerrors.New("tool unavailable as CODER_AGENT_TOKEN or CODER_AGENT_TOKEN_FILE not set") + } + appSlug, ok := workspaceAppStatusSlugFromContext(ctx) + if !ok { + return "", xerrors.New("workspace app status slug not found in context") + } + summary, ok := args["summary"].(string) + if !ok { + return "", xerrors.New("summary must be a string") + } + if len(summary) > 160 { + return "", xerrors.New("summary must be less than 160 characters") + } + link, ok := args["link"].(string) + if !ok { + return "", xerrors.New("link must be a string") + } + emoji, ok := args["emoji"].(string) + if !ok { + return "", xerrors.New("emoji must be a string") + } + state, ok := args["state"].(string) + if !ok { + return "", xerrors.New("state must be a string") + } + + if err := agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{ + AppSlug: appSlug, + Message: summary, + URI: link, + Icon: emoji, + NeedsUserAttention: false, // deprecated, to be removed later + State: codersdk.WorkspaceAppStatusState(state), + }); err != nil { + return "", err + } + return "Thanks for reporting!", nil + }, + } + + GetWorkspace = Tool[codersdk.Workspace]{ + Tool: aisdk.Tool{ + Name: "coder_get_workspace", + Description: `Get a workspace by ID. + +This returns more data than list_workspaces to reduce token usage.`, + Schema: aisdk.Schema{ + Properties: map[string]any{ + "workspace_id": map[string]any{ + "type": "string", + }, + }, + Required: []string{"workspace_id"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) (codersdk.Workspace, error) { + client, err := clientFromContext(ctx) + if err != nil { + return codersdk.Workspace{}, err + } + workspaceID, err := uuidFromArgs(args, "workspace_id") + if err != nil { + return codersdk.Workspace{}, err + } + return client.Workspace(ctx, workspaceID) + }, + } + + CreateWorkspace = Tool[codersdk.Workspace]{ + Tool: aisdk.Tool{ + Name: "coder_create_workspace", + Description: `Create a new workspace in Coder. + +If a user is asking to "test a template", they are typically referring +to creating a workspace from a template to ensure the infrastructure +is provisioned correctly and the agent can connect to the control plane. +`, + Schema: aisdk.Schema{ + Properties: map[string]any{ + "user": map[string]any{ + "type": "string", + "description": "Username or ID of the user to create the workspace for. Use the `me` keyword to create a workspace for the authenticated user.", + }, + "template_version_id": map[string]any{ + "type": "string", + "description": "ID of the template version to create the workspace from.", + }, + "name": map[string]any{ + "type": "string", + "description": "Name of the workspace to create.", + }, + "rich_parameters": map[string]any{ + "type": "object", + "description": "Key/value pairs of rich parameters to pass to the template version to create the workspace.", + }, + }, + Required: []string{"user", "template_version_id", "name", "rich_parameters"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) (codersdk.Workspace, error) { + client, err := clientFromContext(ctx) + if err != nil { + return codersdk.Workspace{}, err + } + templateVersionID, err := uuidFromArgs(args, "template_version_id") + if err != nil { + return codersdk.Workspace{}, err + } + name, ok := args["name"].(string) + if !ok { + return codersdk.Workspace{}, xerrors.New("workspace name must be a string") + } + workspace, err := client.CreateUserWorkspace(ctx, "me", codersdk.CreateWorkspaceRequest{ + TemplateVersionID: templateVersionID, + Name: name, + }) + if err != nil { + return codersdk.Workspace{}, err + } + return workspace, nil + }, + } + + ListWorkspaces = Tool[[]MinimalWorkspace]{ + Tool: aisdk.Tool{ + Name: "coder_list_workspaces", + Description: "Lists workspaces for the authenticated user.", + Schema: aisdk.Schema{ + Properties: map[string]any{ + "owner": map[string]any{ + "type": "string", + "description": "The owner of the workspaces to list. Use \"me\" to list workspaces for the authenticated user. If you do not specify an owner, \"me\" will be assumed by default.", + }, + }, + }, + }, + Handler: func(ctx context.Context, args map[string]any) ([]MinimalWorkspace, error) { + client, err := clientFromContext(ctx) + if err != nil { + return nil, err + } + owner, ok := args["owner"].(string) + if !ok { + owner = codersdk.Me + } + workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ + Owner: owner, + }) + if err != nil { + return nil, err + } + minimalWorkspaces := make([]MinimalWorkspace, len(workspaces.Workspaces)) + for i, workspace := range workspaces.Workspaces { + minimalWorkspaces[i] = MinimalWorkspace{ + ID: workspace.ID.String(), + Name: workspace.Name, + TemplateID: workspace.TemplateID.String(), + TemplateName: workspace.TemplateName, + TemplateDisplayName: workspace.TemplateDisplayName, + TemplateIcon: workspace.TemplateIcon, + TemplateActiveVersionID: workspace.TemplateActiveVersionID, + Outdated: workspace.Outdated, + } + } + return minimalWorkspaces, nil + }, + } + + ListTemplates = Tool[[]MinimalTemplate]{ + Tool: aisdk.Tool{ + Name: "coder_list_templates", + Description: "Lists templates for the authenticated user.", + }, + Handler: func(ctx context.Context, _ map[string]any) ([]MinimalTemplate, error) { + client, err := clientFromContext(ctx) + if err != nil { + return nil, err + } + templates, err := client.Templates(ctx, codersdk.TemplateFilter{}) + if err != nil { + return nil, err + } + minimalTemplates := make([]MinimalTemplate, len(templates)) + for i, template := range templates { + minimalTemplates[i] = MinimalTemplate{ + DisplayName: template.DisplayName, + ID: template.ID.String(), + Name: template.Name, + Description: template.Description, + ActiveVersionID: template.ActiveVersionID, + ActiveUserCount: template.ActiveUserCount, + } + } + return minimalTemplates, nil + }, + } + + ListTemplateVersionParameters = Tool[[]codersdk.TemplateVersionParameter]{ + Tool: aisdk.Tool{ + Name: "coder_template_version_parameters", + Description: "Get the parameters for a template version. You can refer to these as workspace parameters to the user, as they are typically important for creating a workspace.", + Schema: aisdk.Schema{ + Properties: map[string]any{ + "template_version_id": map[string]any{ + "type": "string", + }, + }, + Required: []string{"template_version_id"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) ([]codersdk.TemplateVersionParameter, error) { + client, err := clientFromContext(ctx) + if err != nil { + return nil, err + } + templateVersionID, err := uuidFromArgs(args, "template_version_id") + if err != nil { + return nil, err + } + parameters, err := client.TemplateVersionRichParameters(ctx, templateVersionID) + if err != nil { + return nil, err + } + return parameters, nil + }, + } + + GetAuthenticatedUser = Tool[codersdk.User]{ + Tool: aisdk.Tool{ + Name: "coder_get_authenticated_user", + Description: "Get the currently authenticated user, similar to the `whoami` command.", + }, + Handler: func(ctx context.Context, _ map[string]any) (codersdk.User, error) { + client, err := clientFromContext(ctx) + if err != nil { + return codersdk.User{}, err + } + return client.User(ctx, "me") + }, + } + + CreateWorkspaceBuild = Tool[codersdk.WorkspaceBuild]{ + Tool: aisdk.Tool{ + Name: "coder_create_workspace_build", + Description: "Create a new workspace build for an existing workspace. Use this to start, stop, or delete.", + Schema: aisdk.Schema{ + Properties: map[string]any{ + "workspace_id": map[string]any{ + "type": "string", + }, + "transition": map[string]any{ + "type": "string", + "description": "The transition to perform. Must be one of: start, stop, delete", + }, + }, + Required: []string{"workspace_id", "transition"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) (codersdk.WorkspaceBuild, error) { + client, err := clientFromContext(ctx) + if err != nil { + return codersdk.WorkspaceBuild{}, err + } + workspaceID, err := uuidFromArgs(args, "workspace_id") + if err != nil { + return codersdk.WorkspaceBuild{}, err + } + rawTransition, ok := args["transition"].(string) + if !ok { + return codersdk.WorkspaceBuild{}, xerrors.New("transition must be a string") + } + return client.CreateWorkspaceBuild(ctx, workspaceID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransition(rawTransition), + }) + }, + } + + CreateTemplateVersion = Tool[codersdk.TemplateVersion]{ + Tool: aisdk.Tool{ + Name: "coder_create_template_version", + Description: `Create a new template version. This is a precursor to creating a template, or you can update an existing template. + +Templates are Terraform defining a development environment. The provisioned infrastructure must run +an Agent that connects to the Coder Control Plane to provide a rich experience. + +Here are some strict rules for creating a template version: +- YOU MUST NOT use "variable" or "output" blocks in the Terraform code. +- YOU MUST ALWAYS check template version logs after creation to ensure the template was imported successfully. + +When a template version is created, a Terraform Plan occurs that ensures the infrastructure +_could_ be provisioned, but actual provisioning occurs when a workspace is created. + + +The Coder Terraform Provider can be imported like: + +` + "```" + `hcl +terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} +` + "```" + ` + +A destroy does not occur when a user stops a workspace, but rather the transition changes: + +` + "```" + `hcl +data "coder_workspace" "me" {} +` + "```" + ` + +This data source provides the following fields: +- id: The UUID of the workspace. +- name: The name of the workspace. +- transition: Either "start" or "stop". +- start_count: A computed count based on the transition field. If "start", this will be 1. + +Access workspace owner information with: + +` + "```" + `hcl +data "coder_workspace_owner" "me" {} +` + "```" + ` + +This data source provides the following fields: +- id: The UUID of the workspace owner. +- name: The name of the workspace owner. +- full_name: The full name of the workspace owner. +- email: The email of the workspace owner. +- session_token: A token that can be used to authenticate the workspace owner. It is regenerated every time the workspace is started. +- oidc_access_token: A valid OpenID Connect access token of the workspace owner. This is only available if the workspace owner authenticated with OpenID Connect. If a valid token cannot be obtained, this value will be an empty string. + +Parameters are defined in the template version. They are rendered in the UI on the workspace creation page: + +` + "```" + `hcl +resource "coder_parameter" "region" { + name = "region" + type = "string" + default = "us-east-1" +} +` + "```" + ` + +This resource accepts the following properties: +- name: The name of the parameter. +- default: The default value of the parameter. +- type: The type of the parameter. Must be one of: "string", "number", "bool", or "list(string)". +- display_name: The displayed name of the parameter as it will appear in the UI. +- description: The description of the parameter as it will appear in the UI. +- ephemeral: The value of an ephemeral parameter will not be preserved between consecutive workspace builds. +- form_type: The type of this parameter. Must be one of: [radio, slider, input, dropdown, checkbox, switch, multi-select, tag-select, textarea, error]. +- icon: A URL to an icon to display in the UI. +- mutable: Whether this value can be changed after workspace creation. This can be destructive for values like region, so use with caution! +- option: Each option block defines a value for a user to select from. (see below for nested schema) + Required: + - name: The name of the option. + - value: The value of the option. + Optional: + - description: The description of the option as it will appear in the UI. + - icon: A URL to an icon to display in the UI. + +A Workspace Agent runs on provisioned infrastructure to provide access to the workspace: + +` + "```" + `hcl +resource "coder_agent" "dev" { + arch = "amd64" + os = "linux" +} +` + "```" + ` + +This resource accepts the following properties: +- arch: The architecture of the agent. Must be one of: "amd64", "arm64", or "armv7". +- os: The operating system of the agent. Must be one of: "linux", "windows", or "darwin". +- auth: The authentication method for the agent. Must be one of: "token", "google-instance-identity", "aws-instance-identity", or "azure-instance-identity". It is insecure to pass the agent token via exposed variables to Virtual Machines. Instance Identity enables provisioned VMs to authenticate by instance ID on start. +- dir: The starting directory when a user creates a shell session. Defaults to "$HOME". +- env: A map of environment variables to set for the agent. +- startup_script: A script to run after the agent starts. This script MUST exit eventually to signal that startup has completed. Use "&" or "screen" to run processes in the background. + +This resource provides the following fields: +- id: The UUID of the agent. +- init_script: The script to run on provisioned infrastructure to fetch and start the agent. +- token: Set the environment variable CODER_AGENT_TOKEN to this value to authenticate the agent. + +The agent MUST be installed and started using the init_script. + +Expose terminal or HTTP applications running in a workspace with: + +` + "```" + `hcl +resource "coder_app" "dev" { + agent_id = coder_agent.dev.id + slug = "my-app-name" + display_name = "My App" + icon = "https://my-app.com/icon.svg" + url = "http://127.0.0.1:3000" +} +` + "```" + ` + +This resource accepts the following properties: +- agent_id: The ID of the agent to attach the app to. +- slug: The slug of the app. +- display_name: The displayed name of the app as it will appear in the UI. +- icon: A URL to an icon to display in the UI. +- url: An external url if external=true or a URL to be proxied to from inside the workspace. This should be of the form http://localhost:PORT[/SUBPATH]. Either command or url may be specified, but not both. +- command: A command to run in a terminal opening this app. In the web, this will open in a new tab. In the CLI, this will SSH and execute the command. Either command or url may be specified, but not both. +- external: Whether this app is an external app. If true, the url will be opened in a new tab. + + +The Coder Server may not be authenticated with the infrastructure provider a user requests. In this scenario, +the user will need to provide credentials to the Coder Server before the workspace can be provisioned. + +Here are examples of provisioning the Coder Agent on specific infrastructure providers: + + +// The agent is configured with "aws-instance-identity" auth. +terraform { + required_providers { + cloudinit = { + source = "hashicorp/cloudinit" + } + aws = { + source = "hashicorp/aws" + } + } +} + +data "cloudinit_config" "user_data" { + gzip = false + base64_encode = false + boundary = "//" + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + + // Here is the content of the cloud-config.yaml.tftpl file: + // #cloud-config + // cloud_final_modules: + // - [scripts-user, always] + // hostname: ${hostname} + // users: + // - name: ${linux_user} + // sudo: ALL=(ALL) NOPASSWD:ALL + // shell: /bin/bash + content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { + hostname = local.hostname + linux_user = local.linux_user + }) + } + + part { + filename = "userdata.sh" + content_type = "text/x-shellscript" + + // Here is the content of the userdata.sh.tftpl file: + // #!/bin/bash + // sudo -u '${linux_user}' sh -c '${init_script}' + content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", { + linux_user = local.linux_user + + init_script = try(coder_agent.dev[0].init_script, "") + }) + } +} + +resource "aws_instance" "dev" { + ami = data.aws_ami.ubuntu.id + availability_zone = "${data.coder_parameter.region.value}a" + instance_type = data.coder_parameter.instance_type.value + + user_data = data.cloudinit_config.user_data.rendered + tags = { + Name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}" + } + lifecycle { + ignore_changes = [ami] + } +} + + + +// The agent is configured with "google-instance-identity" auth. +terraform { + required_providers { + google = { + source = "hashicorp/google" + } + } +} + +resource "google_compute_instance" "dev" { + zone = module.gcp_region.value + count = data.coder_workspace.me.start_count + name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-root" + machine_type = "e2-medium" + network_interface { + network = "default" + access_config { + // Ephemeral public IP + } + } + boot_disk { + auto_delete = false + source = google_compute_disk.root.name + } + service_account { + email = data.google_compute_default_service_account.default.email + scopes = ["cloud-platform"] + } + # The startup script runs as root with no $HOME environment set up, so instead of directly + # running the agent init script, create a user (with a homedir, default shell and sudo + # permissions) and execute the init script as that user. + metadata_startup_script = </dev/null 2>&1; then + useradd -m -s /bin/bash "${local.linux_user}" + echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user +fi + +exec sudo -u "${local.linux_user}" sh -c '${coder_agent.main.init_script}' +EOMETA +} + + + +// The agent is configured with "azure-instance-identity" auth. +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + } + cloudinit = { + source = "hashicorp/cloudinit" + } + } +} + +data "cloudinit_config" "user_data" { + gzip = false + base64_encode = true + + boundary = "//" + + part { + filename = "cloud-config.yaml" + content_type = "text/cloud-config" + + // Here is the content of the cloud-config.yaml.tftpl file: + // #cloud-config + // cloud_final_modules: + // - [scripts-user, always] + // bootcmd: + // # work around https://github.com/hashicorp/terraform-provider-azurerm/issues/6117 + // - until [ -e /dev/disk/azure/scsi1/lun10 ]; do sleep 1; done + // device_aliases: + // homedir: /dev/disk/azure/scsi1/lun10 + // disk_setup: + // homedir: + // table_type: gpt + // layout: true + // fs_setup: + // - label: coder_home + // filesystem: ext4 + // device: homedir.1 + // mounts: + // - ["LABEL=coder_home", "/home/${username}"] + // hostname: ${hostname} + // users: + // - name: ${username} + // sudo: ["ALL=(ALL) NOPASSWD:ALL"] + // groups: sudo + // shell: /bin/bash + // packages: + // - git + // write_files: + // - path: /opt/coder/init + // permissions: "0755" + // encoding: b64 + // content: ${init_script} + // - path: /etc/systemd/system/coder-agent.service + // permissions: "0644" + // content: | + // [Unit] + // Description=Coder Agent + // After=network-online.target + // Wants=network-online.target + + // [Service] + // User=${username} + // ExecStart=/opt/coder/init + // Restart=always + // RestartSec=10 + // TimeoutStopSec=90 + // KillMode=process + + // OOMScoreAdjust=-900 + // SyslogIdentifier=coder-agent + + // [Install] + // WantedBy=multi-user.target + // runcmd: + // - chown ${username}:${username} /home/${username} + // - systemctl enable coder-agent + // - systemctl start coder-agent + content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", { + username = "coder" # Ensure this user/group does not exist in your VM image + init_script = base64encode(coder_agent.main.init_script) + hostname = lower(data.coder_workspace.me.name) + }) + } +} + +resource "azurerm_linux_virtual_machine" "main" { + count = data.coder_workspace.me.start_count + name = "vm" + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + size = data.coder_parameter.instance_type.value + // cloud-init overwrites this, so the value here doesn't matter + admin_username = "adminuser" + admin_ssh_key { + public_key = tls_private_key.dummy.public_key_openssh + username = "adminuser" + } + + network_interface_ids = [ + azurerm_network_interface.main.id, + ] + computer_name = lower(data.coder_workspace.me.name) + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + source_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-focal" + sku = "20_04-lts-gen2" + version = "latest" + } + user_data = data.cloudinit_config.user_data.rendered +} + + + +terraform { + required_providers { + coder = { + source = "kreuzwerker/docker" + } + } +} + +// The agent is configured with "token" auth. + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = "codercom/enterprise-base:ubuntu" + # Uses lower() to avoid Docker restriction on container names. + name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.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", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")] + env = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"] + host { + host = "host.docker.internal" + ip = "host-gateway" + } + volumes { + container_path = "/home/coder" + volume_name = docker_volume.home_volume.name + read_only = false + } +} + + + +// The agent is configured with "token" auth. + +resource "kubernetes_deployment" "main" { + count = data.coder_workspace.me.start_count + depends_on = [ + kubernetes_persistent_volume_claim.home + ] + wait_for_rollout = false + metadata { + name = "coder-${data.coder_workspace.me.id}" + } + + spec { + replicas = 1 + strategy { + type = "Recreate" + } + + template { + spec { + security_context { + run_as_user = 1000 + fs_group = 1000 + run_as_non_root = true + } + + container { + name = "dev" + image = "codercom/enterprise-base:ubuntu" + image_pull_policy = "Always" + command = ["sh", "-c", coder_agent.main.init_script] + security_context { + run_as_user = "1000" + } + env { + name = "CODER_AGENT_TOKEN" + value = coder_agent.main.token + } + } + } + } + } +} + + +The file_id provided is a reference to a tar file you have uploaded containing the Terraform. +`, + Schema: aisdk.Schema{ + Properties: map[string]any{ + "template_id": map[string]any{ + "type": "string", + }, + "file_id": map[string]any{ + "type": "string", + }, + }, + Required: []string{"file_id"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) (codersdk.TemplateVersion, error) { + client, err := clientFromContext(ctx) + if err != nil { + return codersdk.TemplateVersion{}, err + } + me, err := client.User(ctx, "me") + if err != nil { + return codersdk.TemplateVersion{}, err + } + fileID, err := uuidFromArgs(args, "file_id") + if err != nil { + return codersdk.TemplateVersion{}, err + } + var templateID uuid.UUID + if args["template_id"] != nil { + templateID, err = uuidFromArgs(args, "template_id") + if err != nil { + return codersdk.TemplateVersion{}, err + } + } + templateVersion, err := client.CreateTemplateVersion(ctx, me.OrganizationIDs[0], codersdk.CreateTemplateVersionRequest{ + Message: "Created by AI", + StorageMethod: codersdk.ProvisionerStorageMethodFile, + FileID: fileID, + Provisioner: codersdk.ProvisionerTypeTerraform, + TemplateID: templateID, + }) + if err != nil { + return codersdk.TemplateVersion{}, err + } + return templateVersion, nil + }, + } + + GetWorkspaceAgentLogs = Tool[[]string]{ + Tool: aisdk.Tool{ + Name: "coder_get_workspace_agent_logs", + Description: `Get the logs of a workspace agent. + +More logs may appear after this call. It does not wait for the agent to finish.`, + Schema: aisdk.Schema{ + Properties: map[string]any{ + "workspace_agent_id": map[string]any{ + "type": "string", + }, + }, + Required: []string{"workspace_agent_id"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) ([]string, error) { + client, err := clientFromContext(ctx) + if err != nil { + return nil, err + } + workspaceAgentID, err := uuidFromArgs(args, "workspace_agent_id") + if err != nil { + return nil, err + } + logs, closer, err := client.WorkspaceAgentLogsAfter(ctx, workspaceAgentID, 0, false) + if err != nil { + return nil, err + } + defer closer.Close() + var acc []string + for logChunk := range logs { + for _, log := range logChunk { + acc = append(acc, log.Output) + } + } + return acc, nil + }, + } + + GetWorkspaceBuildLogs = Tool[[]string]{ + Tool: aisdk.Tool{ + Name: "coder_get_workspace_build_logs", + Description: `Get the logs of a workspace build. + +Useful for checking whether a workspace builds successfully or not.`, + Schema: aisdk.Schema{ + Properties: map[string]any{ + "workspace_build_id": map[string]any{ + "type": "string", + }, + }, + Required: []string{"workspace_build_id"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) ([]string, error) { + client, err := clientFromContext(ctx) + if err != nil { + return nil, err + } + workspaceBuildID, err := uuidFromArgs(args, "workspace_build_id") + if err != nil { + return nil, err + } + logs, closer, err := client.WorkspaceBuildLogsAfter(ctx, workspaceBuildID, 0) + if err != nil { + return nil, err + } + defer closer.Close() + var acc []string + for log := range logs { + acc = append(acc, log.Output) + } + return acc, nil + }, + } + + GetTemplateVersionLogs = Tool[[]string]{ + Tool: aisdk.Tool{ + Name: "coder_get_template_version_logs", + Description: "Get the logs of a template version. This is useful to check whether a template version successfully imports or not.", + Schema: aisdk.Schema{ + Properties: map[string]any{ + "template_version_id": map[string]any{ + "type": "string", + }, + }, + Required: []string{"template_version_id"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) ([]string, error) { + client, err := clientFromContext(ctx) + if err != nil { + return nil, err + } + templateVersionID, err := uuidFromArgs(args, "template_version_id") + if err != nil { + return nil, err + } + + logs, closer, err := client.TemplateVersionLogsAfter(ctx, templateVersionID, 0) + if err != nil { + return nil, err + } + defer closer.Close() + var acc []string + for log := range logs { + acc = append(acc, log.Output) + } + return acc, nil + }, + } + + UpdateTemplateActiveVersion = Tool[string]{ + Tool: aisdk.Tool{ + Name: "coder_update_template_active_version", + Description: "Update the active version of a template. This is helpful when iterating on templates.", + Schema: aisdk.Schema{ + Properties: map[string]any{ + "template_id": map[string]any{ + "type": "string", + }, + "template_version_id": map[string]any{ + "type": "string", + }, + }, + Required: []string{"template_id", "template_version_id"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) (string, error) { + client, err := clientFromContext(ctx) + if err != nil { + return "", err + } + templateID, err := uuidFromArgs(args, "template_id") + if err != nil { + return "", err + } + templateVersionID, err := uuidFromArgs(args, "template_version_id") + if err != nil { + return "", err + } + err = client.UpdateActiveTemplateVersion(ctx, templateID, codersdk.UpdateActiveTemplateVersion{ + ID: templateVersionID, + }) + if err != nil { + return "", err + } + return "Successfully updated active version!", nil + }, + } + + UploadTarFile = Tool[codersdk.UploadResponse]{ + Tool: aisdk.Tool{ + Name: "coder_upload_tar_file", + Description: `Create and upload a tar file by key/value mapping of file names to file contents. Use this to create template versions. Reference the tool description of "create_template_version" to understand template requirements.`, + Schema: aisdk.Schema{ + Properties: map[string]any{ + "mime_type": map[string]any{ + "type": "string", + }, + "files": map[string]any{ + "type": "object", + "description": "A map of file names to file contents.", + }, + }, + Required: []string{"mime_type", "files"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) (codersdk.UploadResponse, error) { + client, err := clientFromContext(ctx) + if err != nil { + return codersdk.UploadResponse{}, err + } + + files, ok := args["files"].(map[string]any) + if !ok { + return codersdk.UploadResponse{}, xerrors.New("files must be a map") + } + + pipeReader, pipeWriter := io.Pipe() + go func() { + defer pipeWriter.Close() + tarWriter := tar.NewWriter(pipeWriter) + for name, content := range files { + contentStr, ok := content.(string) + if !ok { + _ = pipeWriter.CloseWithError(xerrors.New("file content must be a string")) + return + } + header := &tar.Header{ + Name: name, + Size: int64(len(contentStr)), + Mode: 0o644, + } + if err := tarWriter.WriteHeader(header); err != nil { + _ = pipeWriter.CloseWithError(err) + return + } + if _, err := tarWriter.Write([]byte(contentStr)); err != nil { + _ = pipeWriter.CloseWithError(err) + return + } + } + if err := tarWriter.Close(); err != nil { + _ = pipeWriter.CloseWithError(err) + } + }() + + resp, err := client.Upload(ctx, codersdk.ContentTypeTar, pipeReader) + if err != nil { + return codersdk.UploadResponse{}, err + } + return resp, nil + }, + } + + CreateTemplate = Tool[codersdk.Template]{ + Tool: aisdk.Tool{ + Name: "coder_create_template", + Description: "Create a new template in Coder. First, you must create a template version.", + Schema: aisdk.Schema{ + Properties: map[string]any{ + "name": map[string]any{ + "type": "string", + }, + "display_name": map[string]any{ + "type": "string", + }, + "description": map[string]any{ + "type": "string", + }, + "icon": map[string]any{ + "type": "string", + "description": "A URL to an icon to use.", + }, + "version_id": map[string]any{ + "type": "string", + "description": "The ID of the version to use.", + }, + }, + Required: []string{"name", "display_name", "description", "version_id"}, + }, + }, + Handler: func(ctx context.Context, args map[string]any) (codersdk.Template, error) { + client, err := clientFromContext(ctx) + if err != nil { + return codersdk.Template{}, err + } + me, err := client.User(ctx, "me") + if err != nil { + return codersdk.Template{}, err + } + versionID, err := uuidFromArgs(args, "version_id") + if err != nil { + return codersdk.Template{}, err + } + name, ok := args["name"].(string) + if !ok { + return codersdk.Template{}, xerrors.New("name must be a string") + } + displayName, ok := args["display_name"].(string) + if !ok { + return codersdk.Template{}, xerrors.New("display_name must be a string") + } + description, ok := args["description"].(string) + if !ok { + return codersdk.Template{}, xerrors.New("description must be a string") + } + + template, err := client.CreateTemplate(ctx, me.OrganizationIDs[0], codersdk.CreateTemplateRequest{ + Name: name, + DisplayName: displayName, + Description: description, + VersionID: versionID, + }) + if err != nil { + return codersdk.Template{}, err + } + return template, nil + }, + } + + DeleteTemplate = Tool[string]{ + Tool: aisdk.Tool{ + Name: "coder_delete_template", + Description: "Delete a template. This is irreversible.", + Schema: aisdk.Schema{ + Properties: map[string]any{ + "template_id": map[string]any{ + "type": "string", + }, + }, + }, + }, + Handler: func(ctx context.Context, args map[string]any) (string, error) { + client, err := clientFromContext(ctx) + if err != nil { + return "", err + } + + templateID, err := uuidFromArgs(args, "template_id") + if err != nil { + return "", err + } + err = client.DeleteTemplate(ctx, templateID) + if err != nil { + return "", err + } + return "Successfully deleted template!", nil + }, + } +) + +type MinimalWorkspace struct { + ID string `json:"id"` + Name string `json:"name"` + TemplateID string `json:"template_id"` + TemplateName string `json:"template_name"` + TemplateDisplayName string `json:"template_display_name"` + TemplateIcon string `json:"template_icon"` + TemplateActiveVersionID uuid.UUID `json:"template_active_version_id"` + Outdated bool `json:"outdated"` +} + +type MinimalTemplate struct { + DisplayName string `json:"display_name"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ActiveVersionID uuid.UUID `json:"active_version_id"` + ActiveUserCount int `json:"active_user_count"` +} + +func clientFromContext(ctx context.Context) (*codersdk.Client, error) { + client, ok := ctx.Value(clientContextKey{}).(*codersdk.Client) + if !ok { + return nil, xerrors.New("client required in context") + } + return client, nil +} + +type clientContextKey struct{} + +func WithClient(ctx context.Context, client *codersdk.Client) context.Context { + return context.WithValue(ctx, clientContextKey{}, client) +} + +type agentClientContextKey struct{} + +func WithAgentClient(ctx context.Context, client *agentsdk.Client) context.Context { + return context.WithValue(ctx, agentClientContextKey{}, client) +} + +func agentClientFromContext(ctx context.Context) (*agentsdk.Client, error) { + client, ok := ctx.Value(agentClientContextKey{}).(*agentsdk.Client) + if !ok { + return nil, xerrors.New("agent client required in context") + } + return client, nil +} + +type workspaceAppStatusSlugContextKey struct{} + +func WithWorkspaceAppStatusSlug(ctx context.Context, slug string) context.Context { + return context.WithValue(ctx, workspaceAppStatusSlugContextKey{}, slug) +} + +func workspaceAppStatusSlugFromContext(ctx context.Context) (string, bool) { + slug, ok := ctx.Value(workspaceAppStatusSlugContextKey{}).(string) + if !ok || slug == "" { + return "", false + } + return slug, true +} + +func uuidFromArgs(args map[string]any, key string) (uuid.UUID, error) { + raw, ok := args[key].(string) + if !ok { + return uuid.Nil, xerrors.Errorf("%s must be a string", key) + } + id, err := uuid.Parse(raw) + if err != nil { + return uuid.Nil, xerrors.Errorf("failed to parse %s: %w", key, err) + } + return id, nil +} diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go new file mode 100644 index 0000000000000..ee48a6dd8c780 --- /dev/null +++ b/codersdk/toolsdk/toolsdk_test.go @@ -0,0 +1,367 @@ +package toolsdk_test + +import ( + "context" + "os" + "sort" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbfake" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/codersdk/toolsdk" + "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/coder/v2/testutil" +) + +// These tests are dependent on the state of the coder server. +// Running them in parallel is prone to racy behavior. +// nolint:tparallel,paralleltest +func TestTools(t *testing.T) { + // Given: a running coderd instance + setupCtx := testutil.Context(t, testutil.WaitShort) + client, store := coderdtest.NewWithDatabase(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + // Given: a member user with which to test the tools. + memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + // Given: a workspace with an agent. + // nolint:gocritic // This is in a test package and does not end up in the build + r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ + OrganizationID: owner.OrganizationID, + OwnerID: member.ID, + }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { + agents[0].Apps = []*proto.App{ + { + Slug: "some-agent-app", + }, + } + return agents + }).Do() + + // Given: a client configured with the agent token. + agentClient := agentsdk.New(client.URL) + agentClient.SetSessionToken(r.AgentToken) + // Get the agent ID from the API. Overriding it in dbfake doesn't work. + ws, err := client.Workspace(setupCtx, r.Workspace.ID) + require.NoError(t, err) + require.NotEmpty(t, ws.LatestBuild.Resources) + require.NotEmpty(t, ws.LatestBuild.Resources[0].Agents) + agentID := ws.LatestBuild.Resources[0].Agents[0].ID + + // Given: the workspace agent has written logs. + agentClient.PatchLogs(setupCtx, agentsdk.PatchLogs{ + Logs: []agentsdk.Log{ + { + CreatedAt: time.Now(), + Level: codersdk.LogLevelInfo, + Output: "test log message", + }, + }, + }) + + t.Run("ReportTask", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithAgentClient(ctx, agentClient) + ctx = toolsdk.WithWorkspaceAppStatusSlug(ctx, "some-agent-app") + _, err := testTool(ctx, t, toolsdk.ReportTask, map[string]any{ + "summary": "test summary", + "state": "complete", + "link": "https://example.com", + "emoji": "✅", + }) + require.NoError(t, err) + }) + + t.Run("ListTemplates", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + // Get the templates directly for comparison + expected, err := memberClient.Templates(context.Background(), codersdk.TemplateFilter{}) + require.NoError(t, err) + + result, err := testTool(ctx, t, toolsdk.ListTemplates, map[string]any{}) + + require.NoError(t, err) + require.Len(t, result, len(expected)) + + // Sort the results by name to ensure the order is consistent + sort.Slice(expected, func(a, b int) bool { + return expected[a].Name < expected[b].Name + }) + sort.Slice(result, func(a, b int) bool { + return result[a].Name < result[b].Name + }) + for i, template := range result { + require.Equal(t, expected[i].ID.String(), template.ID) + } + }) + + t.Run("Whoami", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + result, err := testTool(ctx, t, toolsdk.GetAuthenticatedUser, map[string]any{}) + + require.NoError(t, err) + require.Equal(t, member.ID, result.ID) + require.Equal(t, member.Username, result.Username) + }) + + t.Run("ListWorkspaces", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + result, err := testTool(ctx, t, toolsdk.ListWorkspaces, map[string]any{ + "owner": "me", + }) + + require.NoError(t, err) + require.Len(t, result, 1, "expected 1 workspace") + workspace := result[0] + require.Equal(t, r.Workspace.ID.String(), workspace.ID, "expected the workspace to match the one we created") + }) + + t.Run("GetWorkspace", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + result, err := testTool(ctx, t, toolsdk.GetWorkspace, map[string]any{ + "workspace_id": r.Workspace.ID.String(), + }) + + require.NoError(t, err) + require.Equal(t, r.Workspace.ID, result.ID, "expected the workspace ID to match") + }) + + t.Run("CreateWorkspaceBuild", func(t *testing.T) { + t.Run("Stop", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + result, err := testTool(ctx, t, toolsdk.CreateWorkspaceBuild, map[string]any{ + "workspace_id": r.Workspace.ID.String(), + "transition": "stop", + }) + + require.NoError(t, err) + require.Equal(t, codersdk.WorkspaceTransitionStop, result.Transition) + require.Equal(t, r.Workspace.ID, result.WorkspaceID) + + // Important: cancel the build. We don't run any provisioners, so this + // will remain in the 'pending' state indefinitely. + require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID)) + }) + + t.Run("Start", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + result, err := testTool(ctx, t, toolsdk.CreateWorkspaceBuild, map[string]any{ + "workspace_id": r.Workspace.ID.String(), + "transition": "start", + }) + + require.NoError(t, err) + require.Equal(t, codersdk.WorkspaceTransitionStart, result.Transition) + require.Equal(t, r.Workspace.ID, result.WorkspaceID) + + // Important: cancel the build. We don't run any provisioners, so this + // will remain in the 'pending' state indefinitely. + require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID)) + }) + }) + + t.Run("ListTemplateVersionParameters", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + params, err := testTool(ctx, t, toolsdk.ListTemplateVersionParameters, map[string]any{ + "template_version_id": r.TemplateVersion.ID.String(), + }) + + require.NoError(t, err) + require.Empty(t, params) + }) + + t.Run("GetWorkspaceAgentLogs", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, client) + + logs, err := testTool(ctx, t, toolsdk.GetWorkspaceAgentLogs, map[string]any{ + "workspace_agent_id": agentID.String(), + }) + + require.NoError(t, err) + require.NotEmpty(t, logs) + }) + + t.Run("GetWorkspaceBuildLogs", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + logs, err := testTool(ctx, t, toolsdk.GetWorkspaceBuildLogs, map[string]any{ + "workspace_build_id": r.Build.ID.String(), + }) + + require.NoError(t, err) + _ = logs // The build may not have any logs yet, so we just check that the function returns successfully + }) + + t.Run("GetTemplateVersionLogs", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + logs, err := testTool(ctx, t, toolsdk.GetTemplateVersionLogs, map[string]any{ + "template_version_id": r.TemplateVersion.ID.String(), + }) + + require.NoError(t, err) + _ = logs // Just ensuring the call succeeds + }) + + t.Run("UpdateTemplateActiveVersion", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, client) // Use owner client for permission + + result, err := testTool(ctx, t, toolsdk.UpdateTemplateActiveVersion, map[string]any{ + "template_id": r.Template.ID.String(), + "template_version_id": r.TemplateVersion.ID.String(), + }) + + require.NoError(t, err) + require.Contains(t, result, "Successfully updated") + }) + + t.Run("DeleteTemplate", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, client) + + _, err := testTool(ctx, t, toolsdk.DeleteTemplate, map[string]any{ + "template_id": r.Template.ID.String(), + }) + + // This will fail with because there already exists a workspace. + require.ErrorContains(t, err, "All workspaces must be deleted before a template can be removed") + }) + + t.Run("UploadTarFile", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, client) + + files := map[string]any{ + "main.tf": "resource \"null_resource\" \"example\" {}", + } + + result, err := testTool(ctx, t, toolsdk.UploadTarFile, map[string]any{ + "mime_type": string(codersdk.ContentTypeTar), + "files": files, + }) + + require.NoError(t, err) + require.NotEmpty(t, result.ID) + }) + + t.Run("CreateTemplateVersion", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, client) + + // nolint:gocritic // This is in a test package and does not end up in the build + file := dbgen.File(t, store, database.File{}) + + tv, err := testTool(ctx, t, toolsdk.CreateTemplateVersion, map[string]any{ + "file_id": file.ID.String(), + }) + require.NoError(t, err) + require.NotEmpty(t, tv) + }) + + t.Run("CreateTemplate", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, client) + + // Create a new template version for use here. + tv := dbfake.TemplateVersion(t, store). + // nolint:gocritic // This is in a test package and does not end up in the build + Seed(database.TemplateVersion{OrganizationID: owner.OrganizationID, CreatedBy: owner.UserID}). + SkipCreateTemplate().Do() + + // We're going to re-use the pre-existing template version + _, err := testTool(ctx, t, toolsdk.CreateTemplate, map[string]any{ + "name": testutil.GetRandomNameHyphenated(t), + "display_name": "Test Template", + "description": "This is a test template", + "version_id": tv.TemplateVersion.ID.String(), + }) + + require.NoError(t, err) + }) + + t.Run("CreateWorkspace", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + // We need a template version ID to create a workspace + res, err := testTool(ctx, t, toolsdk.CreateWorkspace, map[string]any{ + "user": "me", + "template_version_id": r.TemplateVersion.ID.String(), + "name": testutil.GetRandomNameHyphenated(t), + "rich_parameters": map[string]any{}, + }) + + // The creation might fail for various reasons, but the important thing is + // to mark it as tested + require.NoError(t, err) + require.NotEmpty(t, res.ID, "expected a workspace ID") + }) +} + +// TestedTools keeps track of which tools have been tested. +var testedTools sync.Map + +// testTool is a helper function to test a tool and mark it as tested. +func testTool[T any](ctx context.Context, t *testing.T, tool toolsdk.Tool[T], args map[string]any) (T, error) { + t.Helper() + testedTools.Store(tool.Tool.Name, true) + result, err := tool.Handler(ctx, args) + return result, err +} + +// TestMain runs after all tests to ensure that all tools in this package have +// been tested once. +func TestMain(m *testing.M) { + // Initialize testedTools + for _, tool := range toolsdk.All { + testedTools.Store(tool.Tool.Name, false) + } + + code := m.Run() + + // Ensure all tools have been tested + var untested []string + for _, tool := range toolsdk.All { + if tested, ok := testedTools.Load(tool.Tool.Name); !ok || !tested.(bool) { + untested = append(untested, tool.Tool.Name) + } + } + + if len(untested) > 0 && code == 0 { + println("The following tools were not tested:") + for _, tool := range untested { + println(" - " + tool) + } + println("Please ensure that all tools are tested using testTool().") + println("If you just added a new tool, please add a test for it.") + println("NOTE: if you just ran an individual test, this is expected.") + os.Exit(1) + } + + os.Exit(code) +} diff --git a/go.mod b/go.mod index 8fe432f0418bf..dc4d94ec02408 100644 --- a/go.mod +++ b/go.mod @@ -222,8 +222,8 @@ require ( require ( cloud.google.com/go/auth v0.15.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/logging v1.12.0 // indirect - cloud.google.com/go/longrunning v0.6.2 // indirect + cloud.google.com/go/logging v1.13.0 // indirect + cloud.google.com/go/longrunning v0.6.4 // indirect dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect @@ -465,9 +465,9 @@ require ( golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect + google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect howett.net/plist v1.0.0 // indirect @@ -489,38 +489,43 @@ require ( require ( github.com/coder/preview v0.0.0-20250409162646-62939c63c71a - github.com/mark3labs/mcp-go v0.19.0 + github.com/kylecarbs/aisdk-go v0.0.5 + github.com/mark3labs/mcp-go v0.17.0 ) require ( - cel.dev/expr v0.19.1 // indirect - cloud.google.com/go v0.116.0 // indirect - cloud.google.com/go/iam v1.2.2 // indirect - cloud.google.com/go/monitoring v1.21.2 // indirect - cloud.google.com/go/storage v1.49.0 // indirect + cel.dev/expr v0.19.2 // indirect + cloud.google.com/go v0.120.0 // indirect + 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/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // 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 + github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/aquasecurity/trivy v0.58.2 // indirect github.com/aws/aws-sdk-go v1.55.6 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 // indirect + github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // 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 github.com/hashicorp/go-getter v1.7.8 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/liamg/memoryfs v1.6.0 // indirect github.com/moby/sys/user v0.3.0 // indirect + github.com/openai/openai-go v0.1.0-beta.6 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/samber/lo v1.49.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect + google.golang.org/genai v0.7.0 // indirect k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect ) diff --git a/go.sum b/go.sum index 15a22a21a2a19..65c8a706e52e3 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb h1:4MKA8lBQLnCqj2myJCb5Lzoa65y0tABO4gHrxuMdsCQ= cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ= -cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= -cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= +cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -38,8 +38,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= -cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= +cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= @@ -319,8 +319,8 @@ cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGE cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA= -cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY= +cloud.google.com/go/iam v1.4.0 h1:ZNfy/TYfn2uh/ukvhp783WhnbVluqf/tzOaqVUPlIPA= +cloud.google.com/go/iam v1.4.0/go.mod h1:gMBgqPaERlriaOV0CUl//XUzDhSfXevn4OEUbg6VRs4= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= @@ -350,13 +350,13 @@ cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6 cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk= -cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM= +cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= +cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc= -cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI= +cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg= +cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs= cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= @@ -380,8 +380,8 @@ cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhI cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= -cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU= -cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU= +cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= +cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= @@ -544,8 +544,8 @@ cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeL cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storage v1.49.0 h1:zenOPBOWHCnojRd9aJZAyQXBYqkJkdQS42dxL55CIMw= -cloud.google.com/go/storage v1.49.0/go.mod h1:k1eHhhpLvrPjVGfo0mOUPEJ4Y2+a/Hv5PiwehZI9qGU= +cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs= +cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= @@ -565,8 +565,8 @@ cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/trace v1.11.2 h1:4ZmaBdL8Ng/ajrgKqY5jfvzqMXbrDcBsUGXOT9aqTtI= -cloud.google.com/go/trace v1.11.2/go.mod h1:bn7OwXd4pd5rFuAnTrzBuoZ4ax2XQeG3qNgYmfCy0Io= +cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE= +cloud.google.com/go/trace v1.11.3/go.mod h1:pt7zCYiDSQjC9Y2oqCsh9jF4GStB/hmjrYLsxRR27q8= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= @@ -662,12 +662,12 @@ github.com/DataDog/sketches-go v1.4.5 h1:ki7VfeNz7IcNafq7yI/j5U/YCkO3LJiMDtXz9OM github.com/DataDog/sketches-go v1.4.5/go.mod h1:7Y8GN8Jf66DLyDhc94zuWA3uHEt/7ttt8jHOBWWrSOg= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 h1:3c8yed4lgqTt+oTQ+JNMDo+F4xprBf+O/il4ZC0nRLw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 h1:UQ0AhxogsIRZDkElkblfnwjc3IaltCm2HUMvezQaL7s= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1/go.mod h1:jyqM3eLpJ3IbIFDTKVz2rF9T/xWGW0rIriGwnz8l9Tk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1 h1:oTX4vsorBZo/Zdum6OKPA4o7544hm6smoRv1QjpTwGo= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.48.1/go.mod h1:0wEl7vrAD8mehJyohS9HZy+WyEOaQO2mJx86Cvh93kM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 h1:8nn+rsCvTq9axyEh382S0PFLBeaFwNsT43IrPWzctRU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1/go.mod h1:viRWSEhtMZqz1rhwmOVKkWl6SwmVowfL9O2YR5gI2PE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0 h1:nNMpRpnkWDAaqcpxMJvxa/Ud98gjbYwayJY4/9bdjiU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.50.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 h1:ig/FpDD2JofP/NExKQUbn7uOSZzJAQqogfqluZK4ed4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= @@ -713,6 +713,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 h1:b5t1ZJMvV/l99y4jbz7kRFdUp3BSDkI8EhSlHczivtw= +github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= @@ -884,8 +886,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3 h1:boJj011Hh+874zpIySeApCX4GeOjPl9qhRF3QuIZq+Q= -github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwuWwPHPYoCZ/KLAjHv5g4h2MS4f2/MTI= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4= github.com/coder/clistat v1.0.0 h1:MjiS7qQ1IobuSSgDnxcCSyBPESs44hExnh2TEqMcGnA= @@ -1314,6 +1316,8 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= @@ -1463,9 +1467,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylecarbs/aisdk-go v0.0.5 h1:e4HE/SMBUUZn7AS/luiIYbEtHbbtUBzJS95R6qHDYVE= +github.com/kylecarbs/aisdk-go v0.0.5/go.mod h1:3nAhClwRNo6ZfU44GrBZ8O2fCCrxJdaHb9JIz+P3LR8= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3 h1:Z9/bo5PSeMutpdiKYNt/TTSfGM1Ll0naj3QzYX9VxTc= github.com/kylecarbs/chroma/v2 v2.0.0-20240401211003-9e036e0631f3/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk= -github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b h1:1Y1X6aR78kMEQE1iCjQodB3lA7VO4jB88Wf8ZrzXSsA= github.com/kylecarbs/opencensus-go v0.23.1-0.20220307014935-4d0325a68f8b/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= github.com/kylecarbs/readline v0.0.0-20220211054233-0d62993714c8/go.mod h1:n/KX1BZoN1m9EwoXkn/xAV4fd3k8c++gGBsgLONaPOY= github.com/kylecarbs/spinner v1.18.2-0.20220329160715-20702b5af89e h1:OP0ZMFeZkUnOzTFRfpuK3m7Kp4fNvC6qN+exwj7aI4M= @@ -1496,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.19.0 h1:cYKBPFD+fge273/TV6f5+TZYBSTnxV6GCJAO08D2wvA= -github.com/mark3labs/mcp-go v0.19.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= +github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930= +github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= 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= @@ -1604,6 +1609,8 @@ 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/openai/openai-go v0.1.0-beta.6 h1:JquYDpprfrGnlKvQQg+apy9dQ8R9mIrm+wNvAPp6jCQ= +github.com/openai/openai-go v0.1.0-beta.6/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -1792,6 +1799,7 @@ github.com/testcontainers/testcontainers-go/modules/localstack v0.36.0 h1:zVwbe4 github.com/testcontainers/testcontainers-go/modules/localstack v0.36.0/go.mod h1:rxyzj5nX/OUn7QK5PVxKYHJg1eeNtNzWMX2hSbNNJk0= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -1799,6 +1807,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 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= @@ -2474,6 +2484,8 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genai v0.7.0 h1:TINBYXnP+K+D8b16LfVyb6XR3kdtieXy6nJsGoEXcBc= +google.golang.org/genai v0.7.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -2603,12 +2615,12 @@ google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= -google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc= -google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/mcp/mcp.go b/mcp/mcp.go deleted file mode 100644 index 0dd01ccdc5fdd..0000000000000 --- a/mcp/mcp.go +++ /dev/null @@ -1,600 +0,0 @@ -package codermcp - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "io" - "slices" - "strings" - "time" - - "github.com/google/uuid" - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - "golang.org/x/xerrors" - - "cdr.dev/slog" - "github.com/coder/coder/v2/coderd/util/ptr" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/coder/coder/v2/codersdk/workspacesdk" -) - -// allTools is the list of all available tools. When adding a new tool, -// make sure to update this list. -var allTools = ToolRegistry{ - { - Tool: mcp.NewTool("coder_report_task", - mcp.WithDescription(`Report progress on a user task in Coder. -Use this tool to keep the user informed about your progress with their request. -For long-running operations, call this periodically to provide status updates. -This is especially useful when performing multi-step operations like workspace creation or deployment.`), - mcp.WithString("summary", mcp.Description(`A concise summary of your current progress on the task. - -Good Summaries: -- "Taking a look at the login page..." -- "Found a bug! Fixing it now..." -- "Investigating the GitHub Issue..." -- "Waiting for workspace to start (1/3 resources ready)" -- "Downloading template files from repository"`), mcp.Required()), - mcp.WithString("link", mcp.Description(`A relevant URL related to your work, such as: -- GitHub issue link -- Pull request URL -- Documentation reference -- Workspace URL -Use complete URLs (including https://) when possible.`), mcp.Required()), - mcp.WithString("emoji", mcp.Description(`A relevant emoji that visually represents the current status: -- 🔍 for investigating/searching -- 🚀 for deploying/starting -- 🐛 for debugging -- ✅ for completion -- ⏳ for waiting -Choose an emoji that helps the user understand the current phase at a glance.`), mcp.Required()), - mcp.WithBoolean("done", mcp.Description(`Whether the overall task the user requested is complete. -Set to true only when the entire requested operation is finished successfully. -For multi-step processes, use false until all steps are complete.`), mcp.Required()), - mcp.WithBoolean("need_user_attention", mcp.Description(`Whether the user needs to take action on the task. -Set to true if the task is in a failed state or if the user needs to take action to continue.`), mcp.Required()), - ), - MakeHandler: handleCoderReportTask, - }, - { - Tool: mcp.NewTool("coder_whoami", - mcp.WithDescription(`Get information about the currently logged-in Coder user. -Returns JSON with the user's profile including fields: id, username, email, created_at, status, roles, etc. -Use this to identify the current user context before performing workspace operations. -This tool is useful for verifying permissions and checking the user's identity. - -Common errors: -- Authentication failure: The session may have expired -- Server unavailable: The Coder deployment may be unreachable`), - ), - MakeHandler: handleCoderWhoami, - }, - { - Tool: mcp.NewTool("coder_list_templates", - mcp.WithDescription(`List all templates available on the Coder deployment. -Returns JSON with detailed information about each template, including: -- Template name, ID, and description -- Creation/modification timestamps -- Version information -- Associated organization - -Use this tool to discover available templates before creating workspaces. -Templates define the infrastructure and configuration for workspaces. - -Common errors: -- Authentication failure: Check user permissions -- No templates available: The deployment may not have any templates configured`), - ), - MakeHandler: handleCoderListTemplates, - }, - { - Tool: mcp.NewTool("coder_list_workspaces", - mcp.WithDescription(`List workspaces available on the Coder deployment. -Returns JSON with workspace metadata including status, resources, and configurations. -Use this before other workspace operations to find valid workspace names/IDs. -Results are paginated - use offset and limit parameters for large deployments. - -Common errors: -- Authentication failure: Check user permissions -- Invalid owner parameter: Ensure the owner exists`), - mcp.WithString(`owner`, mcp.Description(`The username of the workspace owner to filter by. -Defaults to "me" which represents the currently authenticated user. -Use this to view workspaces belonging to other users (requires appropriate permissions). -Special value: "me" - List workspaces owned by the authenticated user.`), mcp.DefaultString(codersdk.Me)), - mcp.WithNumber(`offset`, mcp.Description(`Pagination offset - the starting index for listing workspaces. -Used with the 'limit' parameter to implement pagination. -For example, to get the second page of results with 10 items per page, use offset=10. -Defaults to 0 (first page).`), mcp.DefaultNumber(0)), - mcp.WithNumber(`limit`, mcp.Description(`Maximum number of workspaces to return in a single request. -Used with the 'offset' parameter to implement pagination. -Higher values return more results but may increase response time. -Valid range: 1-100. Defaults to 10.`), mcp.DefaultNumber(10)), - ), - MakeHandler: handleCoderListWorkspaces, - }, - { - Tool: mcp.NewTool("coder_get_workspace", - mcp.WithDescription(`Get detailed information about a specific Coder workspace. -Returns comprehensive JSON with the workspace's configuration, status, and resources. -Use this to check workspace status before performing operations like exec or start/stop. -The response includes the latest build status, agent connectivity, and resource details. - -Common errors: -- Workspace not found: Check the workspace name or ID -- Permission denied: The user may not have access to this workspace`), - mcp.WithString("workspace", mcp.Description(`The workspace ID (UUID) or name to retrieve. -Can be specified as either: -- Full UUID: e.g., "8a0b9c7d-1e2f-3a4b-5c6d-7e8f9a0b1c2d" -- Workspace name: e.g., "dev", "python-project" -Use coder_list_workspaces first if you're not sure about available workspace names.`), mcp.Required()), - ), - MakeHandler: handleCoderGetWorkspace, - }, - { - Tool: mcp.NewTool("coder_workspace_exec", - mcp.WithDescription(`Execute a shell command in a remote Coder workspace. -Runs the specified command and returns the complete output (stdout/stderr). -Use this for file operations, running build commands, or checking workspace state. -The workspace must be running with a connected agent for this to succeed. - -Before using this tool: -1. Verify the workspace is running using coder_get_workspace -2. Start the workspace if needed using coder_start_workspace - -Common errors: -- Workspace not running: Start the workspace first -- Command not allowed: Check security restrictions -- Agent not connected: The workspace may still be starting up`), - mcp.WithString("workspace", mcp.Description(`The workspace ID (UUID) or name where the command will execute. -Can be specified as either: -- Full UUID: e.g., "8a0b9c7d-1e2f-3a4b-5c6d-7e8f9a0b1c2d" -- Workspace name: e.g., "dev", "python-project" -The workspace must be running with a connected agent. -Use coder_get_workspace first to check the workspace status.`), mcp.Required()), - mcp.WithString("command", mcp.Description(`The shell command to execute in the workspace. -Commands are executed in the default shell of the workspace. - -Examples: -- "ls -la" - List files with details -- "cd /path/to/directory && command" - Execute in specific directory -- "cat ~/.bashrc" - View a file's contents -- "python -m pip list" - List installed Python packages - -Note: Very long-running commands may time out.`), mcp.Required()), - ), - MakeHandler: handleCoderWorkspaceExec, - }, - { - Tool: mcp.NewTool("coder_workspace_transition", - mcp.WithDescription(`Start or stop a running Coder workspace. -If stopping, initiates the workspace stop transition. -Only works on workspaces that are currently running or failed. - -If starting, initiates the workspace start transition. -Only works on workspaces that are currently stopped or failed. - -Stopping or starting a workspace is an asynchronous operation - it may take several minutes to complete. - -After calling this tool: -1. Use coder_report_task to inform the user that the workspace is stopping or starting -2. Use coder_get_workspace periodically to check for completion - -Common errors: -- Workspace already started/starting/stopped/stopping: No action needed -- Cancellation failed: There may be issues with the underlying infrastructure -- User doesn't own workspace: Permission issues`), - mcp.WithString("workspace", mcp.Description(`The workspace ID (UUID) or name to start or stop. -Can be specified as either: -- Full UUID: e.g., "8a0b9c7d-1e2f-3a4b-5c6d-7e8f9a0b1c2d" -- Workspace name: e.g., "dev", "python-project" -The workspace must be in a running state to be stopped, or in a stopped or failed state to be started. -Use coder_get_workspace first to check the current workspace status.`), mcp.Required()), - mcp.WithString("transition", mcp.Description(`The transition to apply to the workspace. -Can be either "start" or "stop".`)), - ), - MakeHandler: handleCoderWorkspaceTransition, - }, -} - -// ToolDeps contains all dependencies needed by tool handlers -type ToolDeps struct { - Client *codersdk.Client - AgentClient *agentsdk.Client - Logger *slog.Logger - AppStatusSlug string -} - -// ToolHandler associates a tool with its handler creation function -type ToolHandler struct { - Tool mcp.Tool - MakeHandler func(ToolDeps) server.ToolHandlerFunc -} - -// ToolRegistry is a map of available tools with their handler creation -// functions -type ToolRegistry []ToolHandler - -// WithOnlyAllowed returns a new ToolRegistry containing only the tools -// specified in the allowed list. -func (r ToolRegistry) WithOnlyAllowed(allowed ...string) ToolRegistry { - if len(allowed) == 0 { - return []ToolHandler{} - } - - filtered := make(ToolRegistry, 0, len(r)) - - // The overhead of a map lookup is likely higher than a linear scan - // for a small number of tools. - for _, entry := range r { - if slices.Contains(allowed, entry.Tool.Name) { - filtered = append(filtered, entry) - } - } - return filtered -} - -// Register registers all tools in the registry with the given tool adder -// and dependencies. -func (r ToolRegistry) Register(srv *server.MCPServer, deps ToolDeps) { - for _, entry := range r { - srv.AddTool(entry.Tool, entry.MakeHandler(deps)) - } -} - -// AllTools returns all available tools. -func AllTools() ToolRegistry { - // return a copy of allTools to avoid mutating the original - return slices.Clone(allTools) -} - -type handleCoderReportTaskArgs struct { - Summary string `json:"summary"` - Link string `json:"link"` - Emoji string `json:"emoji"` - Done bool `json:"done"` - NeedUserAttention bool `json:"need_user_attention"` -} - -// Example payload: -// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_report_task", "arguments": {"summary": "I need help with the login page.", "link": "https://github.com/coder/coder/pull/1234", "emoji": "🔍", "done": false, "need_user_attention": true}}} -func handleCoderReportTask(deps ToolDeps) server.ToolHandlerFunc { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if deps.AgentClient == nil { - return nil, xerrors.New("developer error: agent client is required") - } - - if deps.AppStatusSlug == "" { - return nil, xerrors.New("No app status slug provided, set CODER_MCP_APP_STATUS_SLUG when running the MCP server to report tasks.") - } - - // Convert the request parameters to a json.RawMessage so we can unmarshal - // them into the correct struct. - args, err := unmarshalArgs[handleCoderReportTaskArgs](request.Params.Arguments) - if err != nil { - return nil, xerrors.Errorf("failed to unmarshal arguments: %w", err) - } - - deps.Logger.Info(ctx, "report task tool called", - slog.F("summary", args.Summary), - slog.F("link", args.Link), - slog.F("emoji", args.Emoji), - slog.F("done", args.Done), - slog.F("need_user_attention", args.NeedUserAttention), - ) - - newStatus := agentsdk.PatchAppStatus{ - AppSlug: deps.AppStatusSlug, - Message: args.Summary, - URI: args.Link, - Icon: args.Emoji, - NeedsUserAttention: args.NeedUserAttention, - State: codersdk.WorkspaceAppStatusStateWorking, - } - - if args.Done { - newStatus.State = codersdk.WorkspaceAppStatusStateComplete - } - if args.NeedUserAttention { - newStatus.State = codersdk.WorkspaceAppStatusStateFailure - } - - if err := deps.AgentClient.PatchAppStatus(ctx, newStatus); err != nil { - return nil, xerrors.Errorf("failed to patch app status: %w", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent("Thanks for reporting!"), - }, - }, nil - } -} - -// Example payload: -// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_whoami", "arguments": {}}} -func handleCoderWhoami(deps ToolDeps) server.ToolHandlerFunc { - return func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if deps.Client == nil { - return nil, xerrors.New("developer error: client is required") - } - me, err := deps.Client.User(ctx, codersdk.Me) - if err != nil { - return nil, xerrors.Errorf("Failed to fetch the current user: %s", err.Error()) - } - - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(me); err != nil { - return nil, xerrors.Errorf("Failed to encode the current user: %s", err.Error()) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(strings.TrimSpace(buf.String())), - }, - }, nil - } -} - -type handleCoderListWorkspacesArgs struct { - Owner string `json:"owner"` - Offset int `json:"offset"` - Limit int `json:"limit"` -} - -// Example payload: -// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_list_workspaces", "arguments": {"owner": "me", "offset": 0, "limit": 10}}} -func handleCoderListWorkspaces(deps ToolDeps) server.ToolHandlerFunc { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if deps.Client == nil { - return nil, xerrors.New("developer error: client is required") - } - args, err := unmarshalArgs[handleCoderListWorkspacesArgs](request.Params.Arguments) - if err != nil { - return nil, xerrors.Errorf("failed to unmarshal arguments: %w", err) - } - - workspaces, err := deps.Client.Workspaces(ctx, codersdk.WorkspaceFilter{ - Owner: args.Owner, - Offset: args.Offset, - Limit: args.Limit, - }) - if err != nil { - return nil, xerrors.Errorf("failed to fetch workspaces: %w", err) - } - - // Encode it as JSON. TODO: It might be nicer for the agent to have a tabulated response. - data, err := json.Marshal(workspaces) - if err != nil { - return nil, xerrors.Errorf("failed to encode workspaces: %s", err.Error()) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(string(data)), - }, - }, nil - } -} - -type handleCoderGetWorkspaceArgs struct { - Workspace string `json:"workspace"` -} - -// Example payload: -// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_get_workspace", "arguments": {"workspace": "dev"}}} -func handleCoderGetWorkspace(deps ToolDeps) server.ToolHandlerFunc { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if deps.Client == nil { - return nil, xerrors.New("developer error: client is required") - } - args, err := unmarshalArgs[handleCoderGetWorkspaceArgs](request.Params.Arguments) - if err != nil { - return nil, xerrors.Errorf("failed to unmarshal arguments: %w", err) - } - - workspace, err := getWorkspaceByIDOrOwnerName(ctx, deps.Client, args.Workspace) - if err != nil { - return nil, xerrors.Errorf("failed to fetch workspace: %w", err) - } - - workspaceJSON, err := json.Marshal(workspace) - if err != nil { - return nil, xerrors.Errorf("failed to encode workspace: %w", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(string(workspaceJSON)), - }, - }, nil - } -} - -type handleCoderWorkspaceExecArgs struct { - Workspace string `json:"workspace"` - Command string `json:"command"` -} - -// Example payload: -// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_workspace_exec", "arguments": {"workspace": "dev", "command": "ps -ef"}}} -func handleCoderWorkspaceExec(deps ToolDeps) server.ToolHandlerFunc { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if deps.Client == nil { - return nil, xerrors.New("developer error: client is required") - } - args, err := unmarshalArgs[handleCoderWorkspaceExecArgs](request.Params.Arguments) - if err != nil { - return nil, xerrors.Errorf("failed to unmarshal arguments: %w", err) - } - - // Attempt to fetch the workspace. We may get a UUID or a name, so try to - // handle both. - ws, err := getWorkspaceByIDOrOwnerName(ctx, deps.Client, args.Workspace) - if err != nil { - return nil, xerrors.Errorf("failed to fetch workspace: %w", err) - } - - // Ensure the workspace is started. - // Select the first agent of the workspace. - var agt *codersdk.WorkspaceAgent - for _, r := range ws.LatestBuild.Resources { - for _, a := range r.Agents { - if a.Status != codersdk.WorkspaceAgentConnected { - continue - } - agt = ptr.Ref(a) - break - } - } - if agt == nil { - return nil, xerrors.Errorf("no connected agents for workspace %s", ws.ID) - } - - startedAt := time.Now() - conn, err := workspacesdk.New(deps.Client).AgentReconnectingPTY(ctx, workspacesdk.WorkspaceAgentReconnectingPTYOpts{ - AgentID: agt.ID, - Reconnect: uuid.New(), - Width: 80, - Height: 24, - Command: args.Command, - BackendType: "buffered", // the screen backend is annoying to use here. - }) - if err != nil { - return nil, xerrors.Errorf("failed to open reconnecting PTY: %w", err) - } - defer conn.Close() - connectedAt := time.Now() - - var buf bytes.Buffer - if _, err := io.Copy(&buf, conn); err != nil { - // EOF is expected when the connection is closed. - // We can ignore this error. - if !errors.Is(err, io.EOF) { - return nil, xerrors.Errorf("failed to read from reconnecting PTY: %w", err) - } - } - completedAt := time.Now() - connectionTime := connectedAt.Sub(startedAt) - executionTime := completedAt.Sub(connectedAt) - - resp := map[string]string{ - "connection_time": connectionTime.String(), - "execution_time": executionTime.String(), - "output": buf.String(), - } - respJSON, err := json.Marshal(resp) - if err != nil { - return nil, xerrors.Errorf("failed to encode workspace build: %w", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(string(respJSON)), - }, - }, nil - } -} - -// Example payload: -// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_list_templates", "arguments": {}}} -func handleCoderListTemplates(deps ToolDeps) server.ToolHandlerFunc { - return func(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if deps.Client == nil { - return nil, xerrors.New("developer error: client is required") - } - templates, err := deps.Client.Templates(ctx, codersdk.TemplateFilter{}) - if err != nil { - return nil, xerrors.Errorf("failed to fetch templates: %w", err) - } - - templateJSON, err := json.Marshal(templates) - if err != nil { - return nil, xerrors.Errorf("failed to encode templates: %w", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(string(templateJSON)), - }, - }, nil - } -} - -type handleCoderWorkspaceTransitionArgs struct { - Workspace string `json:"workspace"` - Transition string `json:"transition"` -} - -// Example payload: -// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": -// "coder_workspace_transition", "arguments": {"workspace": "dev", "transition": "stop"}}} -func handleCoderWorkspaceTransition(deps ToolDeps) server.ToolHandlerFunc { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if deps.Client == nil { - return nil, xerrors.New("developer error: client is required") - } - args, err := unmarshalArgs[handleCoderWorkspaceTransitionArgs](request.Params.Arguments) - if err != nil { - return nil, xerrors.Errorf("failed to unmarshal arguments: %w", err) - } - - workspace, err := getWorkspaceByIDOrOwnerName(ctx, deps.Client, args.Workspace) - if err != nil { - return nil, xerrors.Errorf("failed to fetch workspace: %w", err) - } - - wsTransition := codersdk.WorkspaceTransition(args.Transition) - switch wsTransition { - case codersdk.WorkspaceTransitionStart: - case codersdk.WorkspaceTransitionStop: - default: - return nil, xerrors.New("invalid transition") - } - - // We're not going to check the workspace status here as it is checked on the - // server side. - wb, err := deps.Client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ - Transition: wsTransition, - }) - if err != nil { - return nil, xerrors.Errorf("failed to stop workspace: %w", err) - } - - resp := map[string]any{"status": wb.Status, "transition": wb.Transition} - respJSON, err := json.Marshal(resp) - if err != nil { - return nil, xerrors.Errorf("failed to encode workspace build: %w", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(string(respJSON)), - }, - }, nil - } -} - -func getWorkspaceByIDOrOwnerName(ctx context.Context, client *codersdk.Client, identifier string) (codersdk.Workspace, error) { - if wsid, err := uuid.Parse(identifier); err == nil { - return client.Workspace(ctx, wsid) - } - return client.WorkspaceByOwnerAndName(ctx, codersdk.Me, identifier, codersdk.WorkspaceOptions{}) -} - -// unmarshalArgs is a helper function to convert the map[string]any we get from -// the MCP server into a typed struct. It does this by marshaling and unmarshalling -// the arguments. -func unmarshalArgs[T any](args map[string]interface{}) (t T, err error) { - argsJSON, err := json.Marshal(args) - if err != nil { - return t, xerrors.Errorf("failed to marshal arguments: %w", err) - } - if err := json.Unmarshal(argsJSON, &t); err != nil { - return t, xerrors.Errorf("failed to unmarshal arguments: %w", err) - } - return t, nil -} diff --git a/mcp/mcp_test.go b/mcp/mcp_test.go deleted file mode 100644 index f40dc03bae908..0000000000000 --- a/mcp/mcp_test.go +++ /dev/null @@ -1,397 +0,0 @@ -package codermcp_test - -import ( - "context" - "encoding/json" - "io" - "runtime" - "testing" - - "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" - "github.com/stretchr/testify/require" - - "cdr.dev/slog/sloggers/slogtest" - "github.com/coder/coder/v2/agent/agenttest" - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbfake" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/agentsdk" - codermcp "github.com/coder/coder/v2/mcp" - "github.com/coder/coder/v2/provisionersdk/proto" - "github.com/coder/coder/v2/pty/ptytest" - "github.com/coder/coder/v2/testutil" -) - -// These tests are dependent on the state of the coder server. -// Running them in parallel is prone to racy behavior. -// nolint:tparallel,paralleltest -func TestCoderTools(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("skipping on non-linux due to pty issues") - } - ctx := testutil.Context(t, testutil.WaitLong) - // Given: a coder server, workspace, and agent. - client, store := coderdtest.NewWithDatabase(t, nil) - owner := coderdtest.CreateFirstUser(t, client) - // Given: a member user with which to test the tools. - memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) - // Given: a workspace with an agent. - r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - }).WithAgent(func(agents []*proto.Agent) []*proto.Agent { - agents[0].Apps = []*proto.App{ - { - Slug: "some-agent-app", - }, - } - return agents - }).Do() - - // Note: we want to test the list_workspaces tool before starting the - // workspace agent. Starting the workspace agent will modify the workspace - // state, which will affect the results of the list_workspaces tool. - listWorkspacesDone := make(chan struct{}) - agentStarted := make(chan struct{}) - go func() { - defer close(agentStarted) - <-listWorkspacesDone - agt := agenttest.New(t, client.URL, r.AgentToken) - t.Cleanup(func() { - _ = agt.Close() - }) - _ = coderdtest.NewWorkspaceAgentWaiter(t, client, r.Workspace.ID).Wait() - }() - - // Given: a MCP server listening on a pty. - pty := ptytest.New(t) - mcpSrv, closeSrv := startTestMCPServer(ctx, t, pty.Input(), pty.Output()) - t.Cleanup(func() { - _ = closeSrv() - }) - - // Register tools using our registry - logger := slogtest.Make(t, nil) - agentClient := agentsdk.New(memberClient.URL) - codermcp.AllTools().Register(mcpSrv, codermcp.ToolDeps{ - Client: memberClient, - Logger: &logger, - AppStatusSlug: "some-agent-app", - AgentClient: agentClient, - }) - - t.Run("coder_list_templates", func(t *testing.T) { - // When: the coder_list_templates tool is called - ctr := makeJSONRPCRequest(t, "tools/call", "coder_list_templates", map[string]any{}) - - pty.WriteLine(ctr) - _ = pty.ReadLine(ctx) // skip the echo - - // Then: the response is a list of expected visible to the user. - expected, err := memberClient.Templates(ctx, codersdk.TemplateFilter{}) - require.NoError(t, err) - actual := unmarshalFromCallToolResult[[]codersdk.Template](t, pty.ReadLine(ctx)) - require.Len(t, actual, 1) - require.Equal(t, expected[0].ID, actual[0].ID) - }) - - t.Run("coder_report_task", func(t *testing.T) { - // Given: the MCP server has an agent token. - oldAgentToken := agentClient.SDK.SessionToken() - agentClient.SetSessionToken(r.AgentToken) - t.Cleanup(func() { - agentClient.SDK.SetSessionToken(oldAgentToken) - }) - // When: the coder_report_task tool is called - ctr := makeJSONRPCRequest(t, "tools/call", "coder_report_task", map[string]any{ - "summary": "Test summary", - "link": "https://example.com", - "emoji": "🔍", - "done": false, - "need_user_attention": true, - }) - - pty.WriteLine(ctr) - _ = pty.ReadLine(ctx) // skip the echo - - // Then: positive feedback is given to the reporting agent. - actual := pty.ReadLine(ctx) - require.Contains(t, actual, "Thanks for reporting!") - - // Then: the response is a success message. - ws, err := memberClient.Workspace(ctx, r.Workspace.ID) - require.NoError(t, err, "failed to get workspace") - agt, err := memberClient.WorkspaceAgent(ctx, ws.LatestBuild.Resources[0].Agents[0].ID) - require.NoError(t, err, "failed to get workspace agent") - require.NotEmpty(t, agt.Apps, "workspace agent should have an app") - require.NotEmpty(t, agt.Apps[0].Statuses, "workspace agent app should have a status") - st := agt.Apps[0].Statuses[0] - // require.Equal(t, ws.ID, st.WorkspaceID, "workspace app status should have the correct workspace id") - require.Equal(t, agt.ID, st.AgentID, "workspace app status should have the correct agent id") - require.Equal(t, agt.Apps[0].ID, st.AppID, "workspace app status should have the correct app id") - require.Equal(t, codersdk.WorkspaceAppStatusStateFailure, st.State, "workspace app status should be in the failure state") - require.Equal(t, "Test summary", st.Message, "workspace app status should have the correct message") - require.Equal(t, "https://example.com", st.URI, "workspace app status should have the correct uri") - require.Equal(t, "🔍", st.Icon, "workspace app status should have the correct icon") - require.True(t, st.NeedsUserAttention, "workspace app status should need user attention") - }) - - t.Run("coder_whoami", func(t *testing.T) { - // When: the coder_whoami tool is called - ctr := makeJSONRPCRequest(t, "tools/call", "coder_whoami", map[string]any{}) - - pty.WriteLine(ctr) - _ = pty.ReadLine(ctx) // skip the echo - - // Then: the response is a valid JSON respresentation of the calling user. - expected, err := memberClient.User(ctx, codersdk.Me) - require.NoError(t, err) - actual := unmarshalFromCallToolResult[codersdk.User](t, pty.ReadLine(ctx)) - require.Equal(t, expected.ID, actual.ID) - }) - - t.Run("coder_list_workspaces", func(t *testing.T) { - defer close(listWorkspacesDone) - // When: the coder_list_workspaces tool is called - ctr := makeJSONRPCRequest(t, "tools/call", "coder_list_workspaces", map[string]any{ - "coder_url": client.URL.String(), - "coder_session_token": client.SessionToken(), - }) - - pty.WriteLine(ctr) - _ = pty.ReadLine(ctx) // skip the echo - - // Then: the response is a valid JSON respresentation of the calling user's workspaces. - actual := unmarshalFromCallToolResult[codersdk.WorkspacesResponse](t, pty.ReadLine(ctx)) - require.Len(t, actual.Workspaces, 1, "expected 1 workspace") - require.Equal(t, r.Workspace.ID, actual.Workspaces[0].ID, "expected the workspace to be the one we created in setup") - }) - - t.Run("coder_get_workspace", func(t *testing.T) { - // Given: the workspace agent is connected. - // The act of starting the agent will modify the workspace state. - <-agentStarted - // When: the coder_get_workspace tool is called - ctr := makeJSONRPCRequest(t, "tools/call", "coder_get_workspace", map[string]any{ - "workspace": r.Workspace.ID.String(), - }) - - pty.WriteLine(ctr) - _ = pty.ReadLine(ctx) // skip the echo - - expected, err := memberClient.Workspace(ctx, r.Workspace.ID) - require.NoError(t, err) - - // Then: the response is a valid JSON respresentation of the workspace. - actual := unmarshalFromCallToolResult[codersdk.Workspace](t, pty.ReadLine(ctx)) - require.Equal(t, expected.ID, actual.ID) - }) - - // NOTE: this test runs after the list_workspaces tool is called. - t.Run("coder_workspace_exec", func(t *testing.T) { - // Given: the workspace agent is connected - <-agentStarted - - // When: the coder_workspace_exec tools is called with a command - randString := testutil.GetRandomName(t) - ctr := makeJSONRPCRequest(t, "tools/call", "coder_workspace_exec", map[string]any{ - "workspace": r.Workspace.ID.String(), - "command": "echo " + randString, - "coder_url": client.URL.String(), - "coder_session_token": client.SessionToken(), - }) - - pty.WriteLine(ctr) - _ = pty.ReadLine(ctx) // skip the echo - - // Then: the response is the output of the command. - actual := pty.ReadLine(ctx) - require.Contains(t, actual, randString) - }) - - // NOTE: this test runs after the list_workspaces tool is called. - t.Run("tool_restrictions", func(t *testing.T) { - // Given: the workspace agent is connected - <-agentStarted - - // Given: a restricted MCP server with only allowed tools and commands - restrictedPty := ptytest.New(t) - allowedTools := []string{"coder_workspace_exec"} - restrictedMCPSrv, closeRestrictedSrv := startTestMCPServer(ctx, t, restrictedPty.Input(), restrictedPty.Output()) - t.Cleanup(func() { - _ = closeRestrictedSrv() - }) - codermcp.AllTools(). - WithOnlyAllowed(allowedTools...). - Register(restrictedMCPSrv, codermcp.ToolDeps{ - Client: memberClient, - Logger: &logger, - }) - - // When: the tools/list command is called - toolsListCmd := makeJSONRPCRequest(t, "tools/list", "", nil) - restrictedPty.WriteLine(toolsListCmd) - _ = restrictedPty.ReadLine(ctx) // skip the echo - - // Then: the response is a list of only the allowed tools. - toolsListResponse := restrictedPty.ReadLine(ctx) - require.Contains(t, toolsListResponse, "coder_workspace_exec") - require.NotContains(t, toolsListResponse, "coder_whoami") - - // When: a disallowed tool is called - disallowedToolCmd := makeJSONRPCRequest(t, "tools/call", "coder_whoami", map[string]any{}) - restrictedPty.WriteLine(disallowedToolCmd) - _ = restrictedPty.ReadLine(ctx) // skip the echo - - // Then: the response is an error indicating the tool is not available. - disallowedToolResponse := restrictedPty.ReadLine(ctx) - require.Contains(t, disallowedToolResponse, "error") - require.Contains(t, disallowedToolResponse, "not found") - }) - - t.Run("coder_workspace_transition_stop", func(t *testing.T) { - // Given: a separate workspace in the running state - stopWs := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - }).WithAgent().Do() - - // When: the coder_workspace_transition tool is called with a stop transition - ctr := makeJSONRPCRequest(t, "tools/call", "coder_workspace_transition", map[string]any{ - "workspace": stopWs.Workspace.ID.String(), - "transition": "stop", - }) - - pty.WriteLine(ctr) - _ = pty.ReadLine(ctx) // skip the echo - - // Then: the response is as expected. - expected := makeJSONRPCTextResponse(t, `{"status":"pending","transition":"stop"}`) // no provisionerd yet - actual := pty.ReadLine(ctx) - testutil.RequireJSONEq(t, expected, actual) - }) - - t.Run("coder_workspace_transition_start", func(t *testing.T) { - // Given: a separate workspace in the stopped state - stopWs := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{ - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - }).Seed(database.WorkspaceBuild{ - Transition: database.WorkspaceTransitionStop, - }).Do() - - // When: the coder_workspace_transition tool is called with a start transition - ctr := makeJSONRPCRequest(t, "tools/call", "coder_workspace_transition", map[string]any{ - "workspace": stopWs.Workspace.ID.String(), - "transition": "start", - }) - - pty.WriteLine(ctr) - _ = pty.ReadLine(ctx) // skip the echo - - // Then: the response is as expected - expected := makeJSONRPCTextResponse(t, `{"status":"pending","transition":"start"}`) // no provisionerd yet - actual := pty.ReadLine(ctx) - testutil.RequireJSONEq(t, expected, actual) - }) -} - -// makeJSONRPCRequest is a helper function that makes a JSON RPC request. -func makeJSONRPCRequest(t *testing.T, method, name string, args map[string]any) string { - t.Helper() - req := mcp.JSONRPCRequest{ - ID: "1", - JSONRPC: "2.0", - Request: mcp.Request{Method: method}, - Params: struct { // Unfortunately, there is no type for this yet. - Name string `json:"name"` - Arguments map[string]any `json:"arguments,omitempty"` - Meta *struct { - ProgressToken mcp.ProgressToken `json:"progressToken,omitempty"` - } `json:"_meta,omitempty"` - }{ - Name: name, - Arguments: args, - }, - } - bs, err := json.Marshal(req) - require.NoError(t, err, "failed to marshal JSON RPC request") - return string(bs) -} - -// makeJSONRPCTextResponse is a helper function that makes a JSON RPC text response -func makeJSONRPCTextResponse(t *testing.T, text string) string { - t.Helper() - - resp := mcp.JSONRPCResponse{ - ID: "1", - JSONRPC: "2.0", - Result: mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(text), - }, - }, - } - bs, err := json.Marshal(resp) - require.NoError(t, err, "failed to marshal JSON RPC response") - return string(bs) -} - -func unmarshalFromCallToolResult[T any](t *testing.T, raw string) T { - t.Helper() - - var resp map[string]any - require.NoError(t, json.Unmarshal([]byte(raw), &resp), "failed to unmarshal JSON RPC response") - res, ok := resp["result"].(map[string]any) - require.True(t, ok, "expected a result field in the response") - ct, ok := res["content"].([]any) - require.True(t, ok, "expected a content field in the result") - require.Len(t, ct, 1, "expected a single content item in the result") - ct0, ok := ct[0].(map[string]any) - require.True(t, ok, "expected a content item in the result") - txt, ok := ct0["text"].(string) - require.True(t, ok, "expected a text field in the content item") - var actual T - require.NoError(t, json.Unmarshal([]byte(txt), &actual), "failed to unmarshal content") - return actual -} - -// startTestMCPServer is a helper function that starts a MCP server listening on -// a pty. It is the responsibility of the caller to close the server. -func startTestMCPServer(ctx context.Context, t testing.TB, stdin io.Reader, stdout io.Writer) (*server.MCPServer, func() error) { - t.Helper() - - mcpSrv := server.NewMCPServer( - "Test Server", - "0.0.0", - server.WithInstructions(""), - server.WithLogging(), - ) - - stdioSrv := server.NewStdioServer(mcpSrv) - - cancelCtx, cancel := context.WithCancel(ctx) - closeCh := make(chan struct{}) - done := make(chan error) - go func() { - defer close(done) - srvErr := stdioSrv.Listen(cancelCtx, stdin, stdout) - done <- srvErr - }() - - go func() { - select { - case <-closeCh: - cancel() - case <-done: - cancel() - } - }() - - return mcpSrv, func() error { - close(closeCh) - return <-done - } -} From 69aa36516944ed96de9e1f0ee63713c205364740 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 11 Apr 2025 14:46:32 +0400 Subject: [PATCH 066/384] fix: remove provisioner/terraform/testdata/resources/version.txt (#17357) Removes `provisioner/terraform/testdata/resources/version.txt` Pretty sure Claude hallucinated it into existence in #17035 based on the similar `provisioner/terraform/testdata/version.txt` --- provisioner/terraform/testdata/resources/version.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 provisioner/terraform/testdata/resources/version.txt diff --git a/provisioner/terraform/testdata/resources/version.txt b/provisioner/terraform/testdata/resources/version.txt deleted file mode 100644 index 3d0e62313ced1..0000000000000 --- a/provisioner/terraform/testdata/resources/version.txt +++ /dev/null @@ -1 +0,0 @@ -1.11.4 From 9e2af3e1274cc0c7f4512e8b3aa5f82cc7e7b93a Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 11 Apr 2025 15:00:48 +0400 Subject: [PATCH 067/384] feat: add configurable DNS match domain for tailnet connections (#17336) Use the hostname suffix to set the DNS match domain when creating a Tailnet as part of the vpn `Tunnel`. part of: #16828 --- tailnet/configmaps.go | 24 ++++++++++----- tailnet/configmaps_internal_test.go | 45 +++++++++++++++-------------- tailnet/conn.go | 12 ++++++++ tailnet/controllers.go | 2 +- tailnet/controllers_test.go | 10 ++++--- vpn/client.go | 6 +++- 6 files changed, 65 insertions(+), 34 deletions(-) diff --git a/tailnet/configmaps.go b/tailnet/configmaps.go index 605fe559bffac..26b6801130c9e 100644 --- a/tailnet/configmaps.go +++ b/tailnet/configmaps.go @@ -36,7 +36,10 @@ const lostTimeout = 15 * time.Minute // CoderDNSSuffix is the default DNS suffix that we append to Coder DNS // records. -const CoderDNSSuffix = "coder." +const ( + CoderDNSSuffix = "coder" + CoderDNSSuffixFQDN = dnsname.FQDN(CoderDNSSuffix + ".") +) // engineConfigurable is the subset of wgengine.Engine that we use for configuration. // @@ -69,20 +72,26 @@ type configMaps struct { filterDirty bool closing bool - engine engineConfigurable - static netmap.NetworkMap + engine engineConfigurable + static netmap.NetworkMap + hosts map[dnsname.FQDN][]netip.Addr peers map[uuid.UUID]*peerLifecycle addresses []netip.Prefix derpMap *tailcfg.DERPMap logger slog.Logger blockEndpoints bool + matchDomain dnsname.FQDN // for testing clock quartz.Clock } -func newConfigMaps(logger slog.Logger, engine engineConfigurable, nodeID tailcfg.NodeID, nodeKey key.NodePrivate, discoKey key.DiscoPublic) *configMaps { +func newConfigMaps( + logger slog.Logger, engine engineConfigurable, + nodeID tailcfg.NodeID, nodeKey key.NodePrivate, discoKey key.DiscoPublic, + matchDomain dnsname.FQDN, +) *configMaps { pubKey := nodeKey.Public() c := &configMaps{ phased: phased{Cond: *(sync.NewCond(&sync.Mutex{}))}, @@ -125,8 +134,9 @@ func newConfigMaps(logger slog.Logger, engine engineConfigurable, nodeID tailcfg Caps: []filter.CapMatch{}, }}, }, - peers: make(map[uuid.UUID]*peerLifecycle), - clock: quartz.NewReal(), + peers: make(map[uuid.UUID]*peerLifecycle), + matchDomain: matchDomain, + clock: quartz.NewReal(), } go c.configLoop() return c @@ -338,7 +348,7 @@ func (c *configMaps) reconfig(nm *netmap.NetworkMap, hosts map[dnsname.FQDN][]ne dnsCfg.Hosts = hosts dnsCfg.OnlyIPv6 = true dnsCfg.Routes = map[dnsname.FQDN][]*dnstype.Resolver{ - CoderDNSSuffix: nil, + c.matchDomain: nil, } } cfg, err := nmcfg.WGCfg(nm, Logger(c.logger.Named("net.wgconfig")), netmap.AllowSingleHosts, "") diff --git a/tailnet/configmaps_internal_test.go b/tailnet/configmaps_internal_test.go index 69244faf00aad..1727d4b5e27cd 100644 --- a/tailnet/configmaps_internal_test.go +++ b/tailnet/configmaps_internal_test.go @@ -34,7 +34,7 @@ func TestConfigMaps_setAddresses_different(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() addrs := []netip.Prefix{netip.MustParsePrefix("192.168.0.200/32")} @@ -93,7 +93,7 @@ func TestConfigMaps_setAddresses_same(t *testing.T) { nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() addrs := []netip.Prefix{netip.MustParsePrefix("192.168.0.200/32")} - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() // Given: addresses already set @@ -123,7 +123,7 @@ func TestConfigMaps_updatePeers_new(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() p1ID := uuid.UUID{1} @@ -193,7 +193,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_neverConfigures(t *testing. nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() mClock := quartz.NewMock(t) uut.clock = mClock @@ -237,7 +237,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_outOfOrder(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() mClock := quartz.NewMock(t) uut.clock = mClock @@ -308,7 +308,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() mClock := quartz.NewMock(t) uut.clock = mClock @@ -379,7 +379,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_timeout(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() mClock := quartz.NewMock(t) uut.clock = mClock @@ -437,7 +437,7 @@ func TestConfigMaps_updatePeers_same(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() // Then: we don't configure @@ -496,7 +496,7 @@ func TestConfigMaps_updatePeers_disconnect(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() p1ID := uuid.UUID{1} @@ -564,7 +564,7 @@ func TestConfigMaps_updatePeers_lost(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() mClock := quartz.NewMock(t) start := mClock.Now() @@ -649,7 +649,7 @@ func TestConfigMaps_updatePeers_lost_and_found(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() mClock := quartz.NewMock(t) start := mClock.Now() @@ -734,7 +734,7 @@ func TestConfigMaps_setAllPeersLost(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() mClock := quartz.NewMock(t) start := mClock.Now() @@ -820,7 +820,7 @@ func TestConfigMaps_setBlockEndpoints_different(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() p1ID := uuid.MustParse("10000000-0000-0000-0000-000000000000") @@ -864,7 +864,7 @@ func TestConfigMaps_setBlockEndpoints_same(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() p1ID := uuid.MustParse("10000000-0000-0000-0000-000000000000") @@ -907,7 +907,7 @@ func TestConfigMaps_setDERPMap_different(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() derpMap := &tailcfg.DERPMap{ @@ -948,7 +948,7 @@ func TestConfigMaps_setDERPMap_same(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() // Given: DERP Map already set @@ -1017,7 +1017,7 @@ func TestConfigMaps_fillPeerDiagnostics(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() // Given: DERP Map and peer already set @@ -1125,7 +1125,7 @@ func TestConfigMaps_updatePeers_nonexist(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), CoderDNSSuffixFQDN) defer uut.close() // Then: we don't configure @@ -1166,7 +1166,8 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) { nodePrivateKey := key.NewNode() nodeID := tailcfg.NodeID(5) discoKey := key.NewDisco() - uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public()) + suffix := dnsname.FQDN("test.") + uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public(), suffix) defer uut.close() addr1 := CoderServicePrefix.AddrFromUUID(uuid.New()) @@ -1190,8 +1191,10 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) { req := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) require.Equal(t, req.dnsCfg, &dns.Config{ Routes: map[dnsname.FQDN][]*dnstype.Resolver{ - CoderDNSSuffix: nil, + suffix: nil, }, + // Note that host names and Routes are independent --- so we faithfully reproduce the hosts, even though + // they don't match the route. Hosts: map[dnsname.FQDN][]netip.Addr{ "agent.myws.me.coder.": { addr1, @@ -1219,7 +1222,7 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) { req = testutil.RequireRecvCtx(ctx, t, fEng.reconfig) require.Equal(t, req.dnsCfg, &dns.Config{ Routes: map[dnsname.FQDN][]*dnstype.Resolver{ - CoderDNSSuffix: nil, + suffix: nil, }, Hosts: map[dnsname.FQDN][]netip.Addr{ "newagent.myws.me.coder.": { diff --git a/tailnet/conn.go b/tailnet/conn.go index 0a1ee1977e98b..c3ebd246c539f 100644 --- a/tailnet/conn.go +++ b/tailnet/conn.go @@ -120,6 +120,9 @@ type Options struct { // WireguardMonitor is optional, and is passed to the underlying wireguard // engine. WireguardMonitor *netmon.Monitor + // DNSMatchDomain is the DNS suffix to use as a match domain. Only relevant for TUN connections that configure the + // OS DNS resolver. + DNSMatchDomain string } // TelemetrySink allows tailnet.Conn to send network telemetry to the Coder @@ -267,12 +270,21 @@ func NewConn(options *Options) (conn *Conn, err error) { netStack.ProcessLocalIPs = true } + if options.DNSMatchDomain == "" { + options.DNSMatchDomain = CoderDNSSuffix + } + matchDomain, err := dnsname.ToFQDN(options.DNSMatchDomain + ".") + if err != nil { + return nil, xerrors.Errorf("convert hostname suffix (%s) to fully-qualified domain: %w", + options.DNSMatchDomain, err) + } cfgMaps := newConfigMaps( options.Logger, wireguardEngine, nodeID, nodePrivateKey, magicConn.DiscoPublicKey(), + matchDomain, ) cfgMaps.setAddresses(options.Addresses) if options.DERPMap != nil { diff --git a/tailnet/controllers.go b/tailnet/controllers.go index b5f37311a0f71..1d2a348b985f3 100644 --- a/tailnet/controllers.go +++ b/tailnet/controllers.go @@ -1309,7 +1309,7 @@ func NewTunnelAllWorkspaceUpdatesController( t := &TunnelAllWorkspaceUpdatesController{ logger: logger, coordCtrl: c, - dnsNameOptions: DNSNameOptions{"coder"}, + dnsNameOptions: DNSNameOptions{CoderDNSSuffix}, } for _, opt := range opts { opt(t) diff --git a/tailnet/controllers_test.go b/tailnet/controllers_test.go index 089d1b1e82a29..41b2479c6643c 100644 --- a/tailnet/controllers_test.go +++ b/tailnet/controllers_test.go @@ -1637,7 +1637,7 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { fUH := newFakeUpdateHandler(ctx, t) fDNS := newFakeDNSSetter(ctx, t) coordC, updateC, updateCtrl := setupConnectedAllWorkspaceUpdatesController(ctx, t, logger, - tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: "coder"}), + tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: tailnet.CoderDNSSuffix}), tailnet.WithHandler(fUH), ) @@ -1664,7 +1664,8 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { require.Equal(t, w1a1ID[:], coordCall.req.GetAddTunnel().GetId()) testutil.RequireSendCtx(ctx, t, coordCall.err, nil) - expectedCoderConnectFQDN, err := dnsname.ToFQDN(fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "coder")) + expectedCoderConnectFQDN, err := dnsname.ToFQDN( + fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, tailnet.CoderDNSSuffix)) require.NoError(t, err) // DNS for w1a1 @@ -1785,7 +1786,7 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) { fConn := &fakeCoordinatee{} tsc := tailnet.NewTunnelSrcCoordController(logger, fConn) uut := tailnet.NewTunnelAllWorkspaceUpdatesController(logger, tsc, - tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: "coder"}), + tailnet.WithDNS(fDNS, "testy", tailnet.DNSNameOptions{Suffix: tailnet.CoderDNSSuffix}), ) updateC := newFakeWorkspaceUpdateClient(ctx, t) @@ -1806,7 +1807,8 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) { upRecvCall := testutil.RequireRecvCtx(ctx, t, updateC.recv) testutil.RequireSendCtx(ctx, t, upRecvCall.resp, initUp) - expectedCoderConnectFQDN, err := dnsname.ToFQDN(fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "coder")) + expectedCoderConnectFQDN, err := dnsname.ToFQDN( + fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, tailnet.CoderDNSSuffix)) require.NoError(t, err) // DNS for w1a1 diff --git a/vpn/client.go b/vpn/client.go index 85e0d45c3d6f8..da066bbcd62b3 100644 --- a/vpn/client.go +++ b/vpn/client.go @@ -7,6 +7,7 @@ import ( "net/url" "golang.org/x/xerrors" + "tailscale.com/net/dns" "tailscale.com/net/netmon" "tailscale.com/wgengine/router" @@ -108,9 +109,11 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string return nil, xerrors.Errorf("get connection info: %w", err) } // default to DNS suffix of "coder" if the server hasn't set it (might be too old). - dnsNameOptions := tailnet.DNSNameOptions{Suffix: "coder"} + dnsNameOptions := tailnet.DNSNameOptions{Suffix: tailnet.CoderDNSSuffix} + dnsMatch := tailnet.CoderDNSSuffix if connInfo.HostnameSuffix != "" { dnsNameOptions.Suffix = connInfo.HostnameSuffix + dnsMatch = connInfo.HostnameSuffix } headers.Set(codersdk.SessionTokenHeader, token) @@ -134,6 +137,7 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string Router: options.Router, TUNDev: options.TUNDevice, WireguardMonitor: options.WireguardMonitor, + DNSMatchDomain: dnsMatch, }) if err != nil { return nil, xerrors.Errorf("create tailnet: %w", err) From 6a6e1ec50c5d6399ae83c037fa44992c25b93685 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 11 Apr 2025 15:16:37 +0100 Subject: [PATCH 068/384] feat: add support for icons and warning variant in Badge component (#17350) Screenshot 2025-04-10 at 23 11 32 --- site/src/components/Badge/Badge.stories.tsx | 23 +++++++++++ site/src/components/Badge/Badge.tsx | 43 ++++++++++++--------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/site/src/components/Badge/Badge.stories.tsx b/site/src/components/Badge/Badge.stories.tsx index 939e1d20f8d21..7d900b49ff6f6 100644 --- a/site/src/components/Badge/Badge.stories.tsx +++ b/site/src/components/Badge/Badge.stories.tsx @@ -1,4 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { Settings, TriangleAlert } from "lucide-react"; import { Badge } from "./Badge"; const meta: Meta = { @@ -13,3 +14,25 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; + +export const Warning: Story = { + args: { + variant: "warning", + }, +}; + +export const SmallWithIcon: Story = { + args: { + variant: "default", + size: "sm", + children: <>{} Preset, + }, +}; + +export const MediumWithIcon: Story = { + args: { + variant: "warning", + size: "md", + children: <>{} Immutable, + }, +}; diff --git a/site/src/components/Badge/Badge.tsx b/site/src/components/Badge/Badge.tsx index 453e852da7a37..7ee7cc4f94fe0 100644 --- a/site/src/components/Badge/Badge.tsx +++ b/site/src/components/Badge/Badge.tsx @@ -2,21 +2,26 @@ * Copied from shadc/ui on 11/13/2024 * @see {@link https://ui.shadcn.com/docs/components/badge} */ +import { Slot } from "@radix-ui/react-slot"; import { type VariantProps, cva } from "class-variance-authority"; -import type { FC } from "react"; +import { forwardRef } from "react"; import { cn } from "utils/cn"; export const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2 py-1 transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + `inline-flex items-center rounded-md border px-2 py-1 transition-colors + focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 + [&_svg]:pointer-events-none [&_svg]:pr-0.5 [&_svg]:py-0.5 [&_svg]:mr-0.5`, { variants: { variant: { default: "border-transparent bg-surface-secondary text-content-secondary shadow", + warning: + "border-transparent bg-surface-orange text-content-warning shadow", }, size: { - sm: "text-2xs font-regular", - md: "text-xs font-medium", + sm: "text-2xs font-regular h-5.5 [&_svg]:size-icon-xs", + md: "text-xs font-medium [&_svg]:size-icon-sm", }, }, defaultVariants: { @@ -28,18 +33,20 @@ export const badgeVariants = cva( export interface BadgeProps extends React.HTMLAttributes, - VariantProps {} + VariantProps { + asChild?: boolean; +} -export const Badge: FC = ({ - className, - variant, - size, - ...props -}) => { - return ( -
    - ); -}; +export const Badge = forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "div"; + + return ( + + ); + }, +); From 6330b0d5451d981be2115bec87b556397f28eced Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Fri, 11 Apr 2025 11:55:04 -0400 Subject: [PATCH 069/384] docs: add steps to pre-install JetBrains IDE backend (#15962) closes #13207 [preview](https://coder.com/docs/@13207-preinstall-jetbrains/user-guides/workspace-access/jetbrains) --------- Co-authored-by: M Atif Ali Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- .../extending-templates/jetbrains-gateway.md | 119 +++++ docs/changelogs/v2.1.5.md | 2 +- docs/install/offline.md | 2 +- docs/manifest.json | 14 +- docs/user-guides/workspace-access/index.md | 6 +- .../user-guides/workspace-access/jetbrains.md | 411 ------------------ .../workspace-access/jetbrains/index.md | 250 +++++++++++ .../jetbrains/jetbrains-airgapped.md | 164 +++++++ .../jetbrains/jetbrains-pre-install.md | 119 +++++ docs/user-guides/workspace-lifecycle.md | 2 +- 10 files changed, 671 insertions(+), 418 deletions(-) create mode 100644 docs/admin/templates/extending-templates/jetbrains-gateway.md delete mode 100644 docs/user-guides/workspace-access/jetbrains.md create mode 100644 docs/user-guides/workspace-access/jetbrains/index.md create mode 100644 docs/user-guides/workspace-access/jetbrains/jetbrains-airgapped.md create mode 100644 docs/user-guides/workspace-access/jetbrains/jetbrains-pre-install.md diff --git a/docs/admin/templates/extending-templates/jetbrains-gateway.md b/docs/admin/templates/extending-templates/jetbrains-gateway.md new file mode 100644 index 0000000000000..33db219bcac9f --- /dev/null +++ b/docs/admin/templates/extending-templates/jetbrains-gateway.md @@ -0,0 +1,119 @@ +# Pre-install JetBrains Gateway in a template + +For a faster JetBrains Gateway experience, pre-install the IDEs backend in your template. + +> [!NOTE] +> This guide only talks about installing the IDEs backend. For a complete guide on setting up JetBrains Gateway with client IDEs, refer to the [JetBrains Gateway air-gapped guide](../../../user-guides/workspace-access/jetbrains/jetbrains-airgapped.md). + +## Install the Client Downloader + +Install the JetBrains Client Downloader binary: + +```shell +wget https://download.jetbrains.com/idea/code-with-me/backend/jetbrains-clients-downloader-linux-x86_64-1867.tar.gz && \ +tar -xzvf jetbrains-clients-downloader-linux-x86_64-1867.tar.gz +rm jetbrains-clients-downloader-linux-x86_64-1867.tar.gz +``` + +## Install Gateway backend + +```shell +mkdir ~/JetBrains +./jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader --products-filter --build-filter --platforms-filter linux-x64 --download-backends ~/JetBrains +``` + +For example, to install the build `243.26053.27` of IntelliJ IDEA: + +```shell +./jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader --products-filter IU --build-filter 243.26053.27 --platforms-filter linux-x64 --download-backends ~/JetBrains +tar -xzvf ~/JetBrains/backends/IU/*.tar.gz -C ~/JetBrains/backends/IU +rm -rf ~/JetBrains/backends/IU/*.tar.gz +``` + +## Register the Gateway backend + +Add the following command to your template's `startup_script`: + +```shell +~/JetBrains/backends/IU/ideaIU-243.26053.27/bin/remote-dev-server.sh registerBackendLocationForGateway +``` + +## Configure JetBrains Gateway Module + +If you are using our [jetbrains-gateway](https://registry.coder.com/modules/jetbrains-gateway) module, you can configure it by adding the following snippet to your template: + +```tf +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.28" + agent_id = coder_agent.main.id + folder = "/home/coder/example" + jetbrains_ides = ["IU"] + default = "IU" + latest = false + jetbrains_ide_versions = { + "IU" = { + build_number = "243.26053.27" + version = "2024.3" + } + } +} + +resource "coder_agent" "main" { + ... + startup_script = <<-EOF + ~/JetBrains/backends/IU/ideaIU-243.26053.27/bin/remote-dev-server.sh registerBackendLocationForGateway + EOF +} +``` + +## Dockerfile example + +If you are using Docker based workspaces, you can add the command to your Dockerfile: + +```dockerfile +FROM ubuntu + +# Combine all apt operations in a single RUN command +# Install only necessary packages +# Clean up apt cache in the same layer +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + curl \ + git \ + golang \ + sudo \ + vim \ + wget \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Create user in a single layer +ARG USER=coder +RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \ + && echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \ + && chmod 0440 /etc/sudoers.d/${USER} + +USER ${USER} +WORKDIR /home/${USER} + +# Install JetBrains Gateway in a single RUN command to reduce layers +# Download, extract, use, and clean up in the same layer +RUN mkdir -p ~/JetBrains \ + && wget -q https://download.jetbrains.com/idea/code-with-me/backend/jetbrains-clients-downloader-linux-x86_64-1867.tar.gz -P /tmp \ + && tar -xzf /tmp/jetbrains-clients-downloader-linux-x86_64-1867.tar.gz -C /tmp \ + && /tmp/jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader \ + --products-filter IU \ + --build-filter 243.26053.27 \ + --platforms-filter linux-x64 \ + --download-backends ~/JetBrains \ + && tar -xzf ~/JetBrains/backends/IU/*.tar.gz -C ~/JetBrains/backends/IU \ + && rm -f ~/JetBrains/backends/IU/*.tar.gz \ + && rm -rf /tmp/jetbrains-clients-downloader-linux-x86_64-1867* \ + && rm -rf /tmp/*.tar.gz +``` + +## Next steps + +- [Pre-install the Client IDEs](../../../user-guides/workspace-access/jetbrains/jetbrains-airgapped.md#1-deploy-the-server-and-install-the-client-downloader) diff --git a/docs/changelogs/v2.1.5.md b/docs/changelogs/v2.1.5.md index 1e440bd97e75a..915144319b05c 100644 --- a/docs/changelogs/v2.1.5.md +++ b/docs/changelogs/v2.1.5.md @@ -56,7 +56,7 @@ - Add -[JetBrains Gateway Offline Mode](https://coder.com/docs/user-guides/workspace-access/jetbrains.md#jetbrains-gateway-in-an-offline-environment) +[JetBrains Gateway Offline Mode](https://coder.com/docs/user-guides/workspace-access/jetbrains/jetbrains-airgapped.md) config steps (#9388) (@ericpaulsen) - Describe diff --git a/docs/install/offline.md b/docs/install/offline.md index fa976df79f688..56fd293f0d974 100644 --- a/docs/install/offline.md +++ b/docs/install/offline.md @@ -253,7 +253,7 @@ Coder is installed. ## JetBrains IDEs Gateway, JetBrains' remote development product that works with Coder, -[has documented offline deployment steps.](../user-guides/workspace-access/jetbrains.md#jetbrains-gateway-in-an-offline-environment) +[has documented offline deployment steps.](../user-guides/workspace-access/jetbrains/jetbrains-airgapped.md) ## Microsoft VS Code Remote - SSH diff --git a/docs/manifest.json b/docs/manifest.json index ec5157c354b5c..c3858dfd486ea 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -137,7 +137,14 @@ { "title": "JetBrains IDEs", "description": "Use JetBrains IDEs with Gateway", - "path": "./user-guides/workspace-access/jetbrains.md" + "path": "./user-guides/workspace-access/jetbrains/index.md", + "children": [ + { + "title": "JetBrains Gateway in an air-gapped environment", + "description": "Use JetBrains Gateway in an air-gapped offline environment", + "path": "./user-guides/workspace-access/jetbrains/jetbrains-airgapped.md" + } + ] }, { "title": "Remote Desktop", @@ -449,6 +456,11 @@ "description": "Add and configure Web IDEs in your templates as coder apps", "path": "./admin/templates/extending-templates/web-ides.md" }, + { + "title": "Pre-install JetBrains Gateway", + "description": "Pre-install JetBrains Gateway in a template for faster IDE startup", + "path": "./admin/templates/extending-templates/jetbrains-gateway.md" + }, { "title": "Docker in Workspaces", "description": "Use Docker in your workspaces", diff --git a/docs/user-guides/workspace-access/index.md b/docs/user-guides/workspace-access/index.md index 7d9adb7425290..7260cfe309a2d 100644 --- a/docs/user-guides/workspace-access/index.md +++ b/docs/user-guides/workspace-access/index.md @@ -105,10 +105,10 @@ IDEs are supported for remote development: - Rider - RubyMine - WebStorm -- [JetBrains Fleet](./jetbrains.md#jetbrains-fleet) +- [JetBrains Fleet](./jetbrains/index.md#jetbrains-fleet) -Read our [docs on JetBrains Gateway](./jetbrains.md) for more information on -connecting your JetBrains IDEs. +Read our [docs on JetBrains Gateway](./jetbrains/index.md) for more information +on connecting your JetBrains IDEs. ## code-server diff --git a/docs/user-guides/workspace-access/jetbrains.md b/docs/user-guides/workspace-access/jetbrains.md deleted file mode 100644 index 9f78767863590..0000000000000 --- a/docs/user-guides/workspace-access/jetbrains.md +++ /dev/null @@ -1,411 +0,0 @@ -# JetBrains IDEs - -We support JetBrains IDEs using -[Gateway](https://www.jetbrains.com/remote-development/gateway/). The following -IDEs are supported for remote development: - -- IntelliJ IDEA -- CLion -- GoLand -- PyCharm -- Rider -- RubyMine -- WebStorm -- PhpStorm -- RustRover -- [JetBrains Fleet](#jetbrains-fleet) - -## JetBrains Gateway - -JetBrains Gateway is a compact desktop app that allows you to work remotely with -a JetBrains IDE without even downloading one. Visit the -[JetBrains website](https://www.jetbrains.com/remote-development/gateway/) to -learn more about Gateway. - -Gateway can connect to a Coder workspace by using Coder's Gateway plugin or -manually setting up an SSH connection. - -### How to use the plugin - -1. [Install Gateway](https://www.jetbrains.com/help/idea/jetbrains-gateway.html) - and open the application. -1. Under **Install More Providers**, find the Coder icon and click **Install** - to install the Coder plugin. -1. After Gateway installs the plugin, it will appear in the **Run the IDE - Remotely** section. - - Click **Connect to Coder** to launch the plugin: - - ![Gateway Connect to Coder](../../images/gateway/plugin-connect-to-coder.png) - -1. Enter your Coder deployment's - [Access Url](../../admin/setup/index.md#access-url) and click **Connect**. - - Gateway opens your Coder deployment's `cli-auth` page with a session token. - Click the copy button, paste the session token in the Gateway **Session - Token** window, then click **OK**: - - ![Gateway Session Token](../../images/gateway/plugin-session-token.png) - -1. To create a new workspace: - - Click the + icon to open a browser and go to the templates page in - your Coder deployment to create a workspace. - -1. If a workspace already exists but is stopped, select the workspace from the - list, then click the green arrow to start the workspace. - -1. When the workspace status is **Running**, click **Select IDE and Project**: - - ![Gateway IDE List](../../images/gateway/plugin-select-ide.png) - -1. Select the JetBrains IDE for your project and the project directory then - click **Start IDE and connect**: - - ![Gateway Select IDE](../../images/gateway/plugin-ide-list.png) - - Gateway connects using the IDE you selected: - - ![Gateway IDE Opened](../../images/gateway/gateway-intellij-opened.png) - -The JetBrains IDE is remotely installed into `~/.cache/JetBrains/RemoteDev/dist` - -If you experience any issues, please -[create a GitHub issue](https://github.com/coder/coder/issues) or share in -[our Discord channel](https://discord.gg/coder). - -### Update a Coder plugin version - -1. Click the gear icon at the bottom left of the Gateway home screen and then - "Settings" - -1. In the **Marketplace** tab within Plugins, enter Coder and if a newer plugin - release is available, click **Update** then **OK**: - - ![Gateway Settings and Marketplace](../../images/gateway/plugin-settings-marketplace.png) - -### Configuring the Gateway plugin to use internal certificates - -When attempting to connect to a Coder deployment that uses internally signed -certificates, you may receive the following error in Gateway: - -```console -Failed to configure connection to https://coder.internal.enterprise/: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target -``` - -To resolve this issue, you will need to add Coder's certificate to the Java -trust store present on your local machine as well as to the Coder plugin settings. - -1. Add the certificate to the Java trust store: - -
    - - #### Linux - - ```none - /jbr/lib/security/cacerts - ``` - - Use the `keytool` utility that ships with Java: - - ```shell - keytool -import -alias coder -file -keystore /path/to/trust/store - ``` - - #### macOS - - ```none - /jbr/lib/security/cacerts - /Library/Application Support/JetBrains/Toolbox/apps/JetBrainsGateway/ch-0//JetBrains Gateway.app/Contents/jbr/Contents/Home/lib/security/cacerts # Path for Toolbox installation - ``` - - Use the `keytool` included in the JetBrains Gateway installation: - - ```shell - keytool -import -alias coder -file cacert.pem -keystore /Applications/JetBrains\ Gateway.app/Contents/jbr/Contents/Home/lib/security/cacerts - ``` - - #### Windows - - ```none - C:\Program Files (x86)\\jre\lib\security\cacerts\%USERPROFILE%\AppData\Local\JetBrains\Toolbox\bin\jre\lib\security\cacerts # Path for Toolbox installation - ``` - - Use the `keytool` included in the JetBrains Gateway installation: - - ```powershell - & 'C:\Program Files\JetBrains\JetBrains Gateway /jbr/bin/keytool.exe' 'C:\Program Files\JetBrains\JetBrains Gateway /jre/lib/security/cacerts' -import -alias coder -file - - # command for Toolbox installation - & '%USERPROFILE%\AppData\Local\JetBrains\Toolbox\apps\Gateway\ch-0\\jbr\bin\keytool.exe' '%USERPROFILE%\AppData\Local\JetBrains\Toolbox\bin\jre\lib\security\cacerts' -import -alias coder -file - ``` - -
    - -1. In JetBrains, go to **Settings** > **Tools** > **Coder**. - -1. Paste the path to the certificate in **CA Path**. - -## Manually Configuring A JetBrains Gateway Connection - -This is in lieu of using Coder's Gateway plugin which automatically performs these steps. - -1. [Install Gateway](https://www.jetbrains.com/help/idea/jetbrains-gateway.html). - -1. [Configure the `coder` CLI](../../user-guides/workspace-access/index.md#configure-ssh). - -1. Open Gateway, make sure **SSH** is selected under **Remote Development**. - -1. Click **New Connection**: - - ![Gateway Home](../../images/gateway/gateway-home.png) - -1. In the resulting dialog, click the gear icon to the right of **Connection**: - - ![Gateway New Connection](../../images/gateway/gateway-new-connection.png) - -1. Click + to add a new SSH connection: - - ![Gateway Add Connection](../../images/gateway/gateway-add-ssh-configuration.png) - -1. For the Host, enter `coder.` - -1. For the Port, enter `22` (this is ignored by Coder) - -1. For the Username, enter your workspace username. - -1. For the Authentication Type, select **OpenSSH config and authentication - agent**. - -1. Make sure the checkbox for **Parse config file ~/.ssh/config** is checked. - -1. Click **Test Connection** to validate these settings. - -1. Click **OK**: - - ![Gateway SSH Configuration](../../images/gateway/gateway-create-ssh-configuration.png) - -1. Select the connection you just added: - - ![Gateway Welcome](../../images/gateway/gateway-welcome.png) - -1. Click **Check Connection and Continue**: - - ![Gateway Continue](../../images/gateway/gateway-continue.png) - -1. Select the JetBrains IDE for your project and the project directory. SSH into - your server to create a directory or check out code if you haven't already. - - ![Gateway Choose IDE](../../images/gateway/gateway-choose-ide.png) - - The JetBrains IDE is remotely installed into `~/.cache/JetBrains/RemoteDev/dist` - -1. Click **Download and Start IDE** to connect. - - ![Gateway IDE Opened](../../images/gateway/gateway-intellij-opened.png) - -## Using an existing JetBrains installation in the workspace - -If you would like to use an existing JetBrains IDE in a Coder workspace (or you -are air-gapped, and cannot reach jetbrains.com), run the following script in the -JetBrains IDE directory to point the default Gateway directory to the IDE -directory. This step must be done before configuring Gateway. - -```shell -cd /opt/idea/bin -./remote-dev-server.sh registerBackendLocationForGateway -``` - -> [!NOTE] -> Gateway only works with paid versions of JetBrains IDEs so the script will not -> be located in the `bin` directory of JetBrains Community editions. - -[Here is the JetBrains article](https://www.jetbrains.com/help/idea/remote-development-troubleshooting.html#setup:~:text=Can%20I%20point%20Remote%20Development%20to%20an%20existing%20IDE%20on%20my%20remote%20server%3F%20Is%20it%20possible%20to%20install%20IDE%20manually%3F) -explaining this IDE specification. - -## JetBrains Gateway in an offline environment - -In networks that restrict access to the internet, you will need to leverage the -JetBrains Client Installer to download and save the IDE clients locally. Please -see the -[JetBrains documentation for more information](https://www.jetbrains.com/help/idea/fully-offline-mode.html). - -### Configuration Steps - -The Coder team built a POC of the JetBrains Gateway Offline Mode solution. Here -are the steps we took (and "gotchas"): - -### 1. Deploy the server and install the Client Downloader - -We deployed a simple Ubuntu VM and installed the JetBrains Client Downloader -binary. Note that the server must be a Linux-based distribution. - -```shell -wget https://download.jetbrains.com/idea/code-with-me/backend/jetbrains-clients-downloader-linux-x86_64-1867.tar.gz && \ -tar -xzvf jetbrains-clients-downloader-linux-x86_64-1867.tar.gz -``` - -### 2. Install backends and clients - -JetBrains Gateway requires both a backend to be installed on the remote host -(your Coder workspace) and a client to be installed on your local machine. You -can host both on the server in this example. - -See here for the full -[JetBrains product list and builds](https://data.services.jetbrains.com/products). -Below is the full list of supported `--platforms-filter` values: - -```console -windows-x64, windows-aarch64, linux-x64, linux-aarch64, osx-x64, osx-aarch64 -``` - -To install both backends and clients, you will need to run two commands. - -#### Backends - -```shell -mkdir ~/backends -./jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader --products-filter --build-filter --platforms-filter linux-x64,windows-x64,osx-x64 --download-backends ~/backends -``` - -#### Clients - -This is the same command as above, with the `--download-backends` flag removed. - -```shell -mkdir ~/clients -./jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader --products-filter --build-filter --platforms-filter linux-x64,windows-x64,osx-x64 ~/clients -``` - -We now have both clients and backends installed. - -### 3. Install a web server - -You will need to run a web server in order to serve requests to the backend and -client files. We installed `nginx` and setup an FQDN and routed all requests to -`/`. See below: - -```console -server { - listen 80 default_server; - listen [::]:80 default_server; - - root /var/www/html; - - index index.html index.htm index.nginx-debian.html; - - server_name _; - - location / { - root /home/ubuntu; - } -} -``` - -Then, configure your DNS entry to point to the IP address of the server. For the -purposes of the POC, we did not configure TLS, although that is a supported -option. - -### 4. Add Client Files - -You will need to add the following files on your local machine in order for -Gateway to pull the backend and client from the server. - -```shell -$ cat productsInfoUrl # a path to products.json that was generated by the backend's downloader (it could be http://, https://, or file://) - -https://internal.site/backends//products.json - -$ cat clientDownloadUrl # a path for clients that you got from the clients' downloader (it could be http://, https://, or file://) - -https://internal.site/clients/ - -$ cat jreDownloadUrl # a path for JBR that you got from the clients' downloader (it could be http://, https://, or file://) - -https://internal.site/jre/ - -$ cat pgpPublicKeyUrl # a URL to the KEYS file that was downloaded with the clients builds. - -https://internal.site/KEYS -``` - -The location of these files will depend upon your local operating system: - -#### macOS - -```console -# User-specific settings -/Users/UserName/Library/Application Support/JetBrains/RemoteDev -# System-wide settings -/Library/Application Support/JetBrains/RemoteDev/ -``` - -#### Linux - -```console -# User-specific settings -$HOME/.config/JetBrains/RemoteDev -# System-wide settings -/etc/xdg/JetBrains/RemoteDev/ -``` - -#### Windows - -```console -# User-specific settings -HKEY_CURRENT_USER registry -# System-wide settings -HKEY_LOCAL_MACHINE registry -``` - -Additionally, create a string for each setting with its appropriate value in -`SOFTWARE\JetBrains\RemoteDev`: - -![Alt text](../../images/gateway/jetbrains-offline-windows.png) - -### 5. Setup SSH connection with JetBrains Gateway - -With the server now configured, you can now configure your local machine to use -Gateway. Here is the documentation to -[setup SSH config via the Coder CLI](../../user-guides/workspace-access/index.md#configure-ssh). -On the Gateway side, follow our guide here until step 16. - -Instead of downloading from jetbrains.com, we will point Gateway to our server -endpoint. Select `Installation options...` and select `Use download link`. Note -that the URL must explicitly reference the archive file: - -![Offline Gateway](../../images/gateway/offline-gateway.png) - -Click `Download IDE and Connect`. Gateway should now download the backend and -clients from the server into your remote workspace and local machine, -respectively. - -## JetBrains Fleet - -JetBrains Fleet is a code editor and lightweight IDE designed to support various -programming languages and development environments. - -[See JetBrains' website to learn about Fleet](https://www.jetbrains.com/fleet/) - -Fleet can connect to a Coder workspace by following these steps. - -1. [Install Fleet](https://www.jetbrains.com/fleet/download) -2. Install Coder CLI - - ```shell - curl -L https://coder.com/install.sh | sh - ``` - -3. Login and configure Coder SSH. - - ```shell - coder login coder.example.com - coder config-ssh - ``` - -4. Connect via SSH with the Host set to `coder.workspace-name` - ![Fleet Connect to Coder](../../images/fleet/ssh-connect-to-coder.png) - -If you experience any issues, please -[create a GitHub issue](https://github.com/coder/coder/issues) or share in -[our Discord channel](https://discord.gg/coder). diff --git a/docs/user-guides/workspace-access/jetbrains/index.md b/docs/user-guides/workspace-access/jetbrains/index.md new file mode 100644 index 0000000000000..66de625866e1b --- /dev/null +++ b/docs/user-guides/workspace-access/jetbrains/index.md @@ -0,0 +1,250 @@ +# JetBrains IDEs + +Coder supports JetBrains IDEs using +[Gateway](https://www.jetbrains.com/remote-development/gateway/). The following +IDEs are supported for remote development: + +- IntelliJ IDEA +- CLion +- GoLand +- PyCharm +- Rider +- RubyMine +- WebStorm +- PhpStorm +- RustRover +- [JetBrains Fleet](#jetbrains-fleet) + +## JetBrains Gateway + +JetBrains Gateway is a compact desktop app that allows you to work remotely with +a JetBrains IDE without downloading one. Visit the +[JetBrains Gateway website](https://www.jetbrains.com/remote-development/gateway/) +to learn more about Gateway. + +Gateway can connect to a Coder workspace using Coder's Gateway plugin or through a +manually configured SSH connection. + +You can [pre-install the JetBrains Gateway backend](../../../admin/templates/extending-templates/jetbrains-gateway.md) in a template to help JetBrains load faster in workspaces. + +### How to use the plugin + +> If you experience problems, please +> [create a GitHub issue](https://github.com/coder/coder/issues) or share in +> [our Discord channel](https://discord.gg/coder). + +1. [Install Gateway](https://www.jetbrains.com/help/idea/jetbrains-gateway.html) + and open the application. +1. Under **Install More Providers**, find the Coder icon and click **Install** + to install the Coder plugin. +1. After Gateway installs the plugin, it will appear in the **Run the IDE + Remotely** section. + + Click **Connect to Coder** to launch the plugin: + + ![Gateway Connect to Coder](../../../images/gateway/plugin-connect-to-coder.png) + +1. Enter your Coder deployment's + [Access Url](../../../admin/setup/index.md#access-url) and click **Connect**. + + Gateway opens your Coder deployment's `cli-auth` page with a session token. + Click the copy button, paste the session token in the Gateway **Session + Token** window, then click **OK**: + + ![Gateway Session Token](../../../images/gateway/plugin-session-token.png) + +1. To create a new workspace: + + Click the + icon to open a browser and go to the templates page in + your Coder deployment to create a workspace. + +1. If a workspace already exists but is stopped, select the workspace from the + list, then click the green arrow to start the workspace. + +1. When the workspace status is **Running**, click **Select IDE and Project**: + + ![Gateway IDE List](../../../images/gateway/plugin-select-ide.png) + +1. Select the JetBrains IDE for your project and the project directory then + click **Start IDE and connect**: + + ![Gateway Select IDE](../../../images/gateway/plugin-ide-list.png) + + Gateway connects using the IDE you selected: + + ![Gateway IDE Opened](../../../images/gateway/gateway-intellij-opened.png) + + The JetBrains IDE is remotely installed into `~/.cache/JetBrains/RemoteDev/dist`. + +### Update a Coder plugin version + +1. Click the gear icon at the bottom left of the Gateway home screen, then + **Settings**. + +1. In the **Marketplace** tab within Plugins, enter Coder and if a newer plugin + release is available, click **Update** then **OK**: + + ![Gateway Settings and Marketplace](../../../images/gateway/plugin-settings-marketplace.png) + +### Configuring the Gateway plugin to use internal certificates + +When you attempt to connect to a Coder deployment that uses internally signed +certificates, you might receive the following error in Gateway: + +```console +Failed to configure connection to https://coder.internal.enterprise/: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target +``` + +To resolve this issue, you will need to add Coder's certificate to the Java +trust store present on your local machine as well as to the Coder plugin settings. + +1. Add the certificate to the Java trust store: + +
    + + #### Linux + + ```none + /jbr/lib/security/cacerts + ``` + + Use the `keytool` utility that ships with Java: + + ```shell + keytool -import -alias coder -file -keystore /path/to/trust/store + ``` + + #### macOS + + ```none + /jbr/lib/security/cacerts + /Library/Application Support/JetBrains/Toolbox/apps/JetBrainsGateway/ch-0//JetBrains Gateway.app/Contents/jbr/Contents/Home/lib/security/cacerts # Path for Toolbox installation + ``` + + Use the `keytool` included in the JetBrains Gateway installation: + + ```shell + keytool -import -alias coder -file cacert.pem -keystore /Applications/JetBrains\ Gateway.app/Contents/jbr/Contents/Home/lib/security/cacerts + ``` + + #### Windows + + ```none + C:\Program Files (x86)\\jre\lib\security\cacerts\%USERPROFILE%\AppData\Local\JetBrains\Toolbox\bin\jre\lib\security\cacerts # Path for Toolbox installation + ``` + + Use the `keytool` included in the JetBrains Gateway installation: + + ```powershell + & 'C:\Program Files\JetBrains\JetBrains Gateway /jbr/bin/keytool.exe' 'C:\Program Files\JetBrains\JetBrains Gateway /jre/lib/security/cacerts' -import -alias coder -file + + # command for Toolbox installation + & '%USERPROFILE%\AppData\Local\JetBrains\Toolbox\apps\Gateway\ch-0\\jbr\bin\keytool.exe' '%USERPROFILE%\AppData\Local\JetBrains\Toolbox\bin\jre\lib\security\cacerts' -import -alias coder -file + ``` + +
    + +1. In JetBrains, go to **Settings** > **Tools** > **Coder**. + +1. Paste the path to the certificate in **CA Path**. + +## Manually Configuring A JetBrains Gateway Connection + +This is in lieu of using Coder's Gateway plugin which automatically performs these steps. + +1. [Install Gateway](https://www.jetbrains.com/help/idea/jetbrains-gateway.html). + +1. [Configure the `coder` CLI](../../../user-guides/workspace-access/index.md#configure-ssh). + +1. Open Gateway, make sure **SSH** is selected under **Remote Development**. + +1. Click **New Connection**: + + ![Gateway Home](../../../images/gateway/gateway-home.png) + +1. In the resulting dialog, click the gear icon to the right of **Connection**: + + ![Gateway New Connection](../../../images/gateway/gateway-new-connection.png) + +1. Click + to add a new SSH connection: + + ![Gateway Add Connection](../../../images/gateway/gateway-add-ssh-configuration.png) + +1. For the Host, enter `coder.` + +1. For the Port, enter `22` (this is ignored by Coder) + +1. For the Username, enter your workspace username. + +1. For the Authentication Type, select **OpenSSH config and authentication + agent**. + +1. Make sure the checkbox for **Parse config file ~/.ssh/config** is checked. + +1. Click **Test Connection** to validate these settings. + +1. Click **OK**: + + ![Gateway SSH Configuration](../../../images/gateway/gateway-create-ssh-configuration.png) + +1. Select the connection you just added: + + ![Gateway Welcome](../../../images/gateway/gateway-welcome.png) + +1. Click **Check Connection and Continue**: + + ![Gateway Continue](../../../images/gateway/gateway-continue.png) + +1. Select the JetBrains IDE for your project and the project directory. SSH into + your server to create a directory or check out code if you haven't already. + + ![Gateway Choose IDE](../../../images/gateway/gateway-choose-ide.png) + + The JetBrains IDE is remotely installed into `~/.cache/JetBrains/RemoteDev/dist` + +1. Click **Download and Start IDE** to connect. + + ![Gateway IDE Opened](../../../images/gateway/gateway-intellij-opened.png) + +## Using an existing JetBrains installation in the workspace + +For JetBrains IDEs, you can use an existing installation in the workspace. +Please ask your administrator to install the JetBrains Gateway backend in the workspace by following the [pre-install guide](../../../admin/templates/extending-templates/jetbrains-gateway.md). + +> [!NOTE] +> Gateway only works with paid versions of JetBrains IDEs so the script will not +> be located in the `bin` directory of JetBrains Community editions. + +[Here is the JetBrains article](https://www.jetbrains.com/help/idea/remote-development-troubleshooting.html#setup:~:text=Can%20I%20point%20Remote%20Development%20to%20an%20existing%20IDE%20on%20my%20remote%20server%3F%20Is%20it%20possible%20to%20install%20IDE%20manually%3F) +explaining this IDE specification. + +## JetBrains Fleet + +JetBrains Fleet is a code editor and lightweight IDE designed to support various +programming languages and development environments. + +[See JetBrains's website](https://www.jetbrains.com/fleet/) to learn more about Fleet. + +To connect Fleet to a Coder workspace: + +1. [Install Fleet](https://www.jetbrains.com/fleet/download) + +1. Install Coder CLI + + ```shell + curl -L https://coder.com/install.sh | sh + ``` + +1. Login and configure Coder SSH. + + ```shell + coder login coder.example.com + coder config-ssh + ``` + +1. Connect via SSH with the Host set to `coder.workspace-name` + ![Fleet Connect to Coder](../../../images/fleet/ssh-connect-to-coder.png) + +If you experience any issues, please +[create a GitHub issue](https://github.com/coder/coder/issues) or share in +[our Discord channel](https://discord.gg/coder). diff --git a/docs/user-guides/workspace-access/jetbrains/jetbrains-airgapped.md b/docs/user-guides/workspace-access/jetbrains/jetbrains-airgapped.md new file mode 100644 index 0000000000000..197cce2b5fa33 --- /dev/null +++ b/docs/user-guides/workspace-access/jetbrains/jetbrains-airgapped.md @@ -0,0 +1,164 @@ +# JetBrains Gateway in an air-gapped environment + +In networks that restrict access to the internet, you will need to leverage the +JetBrains Client Installer to download and save the IDE clients locally. Please +see the +[JetBrains documentation for more information](https://www.jetbrains.com/help/idea/fully-offline-mode.html). + +This page is an example that the Coder team used as a proof-of-concept (POC) of the JetBrains Gateway Offline Mode solution. + +We used Ubuntu on a virtual machine to test the steps. +If you have a suggestion or encounter an issue, please +[file a GitHub issue](https://github.com/coder/coder/issues/new?title=request%28docs%29%3A+jetbrains-airgapped+-+request+title+here%0D%0A&labels=["community","docs"]&body=doc%3A+%5Bjetbrains-airgapped%5D%28https%3A%2F%2Fcoder.com%2Fdocs%2Fuser-guides%2Fworkspace-access%2Fjetbrains%2Fjetbrains-airgapped%29%0D%0A%0D%0Aplease+enter+your+request+here%0D%0A). + +## 1. Deploy the server and install the Client Downloader + +Install the JetBrains Client Downloader binary. Note that the server must be a Linux-based distribution: + +```shell +wget https://download.jetbrains.com/idea/code-with-me/backend/jetbrains-clients-downloader-linux-x86_64-1867.tar.gz && \ +tar -xzvf jetbrains-clients-downloader-linux-x86_64-1867.tar.gz +``` + +## 2. Install backends and clients + +JetBrains Gateway requires both a backend to be installed on the remote host +(your Coder workspace) and a client to be installed on your local machine. You +can host both on the server in this example. + +See here for the full +[JetBrains product list and builds](https://data.services.jetbrains.com/products). +Below is the full list of supported `--platforms-filter` values: + +```console +windows-x64, windows-aarch64, linux-x64, linux-aarch64, osx-x64, osx-aarch64 +``` + +To install both backends and clients, you will need to run two commands. + +### Backends + +```shell +mkdir ~/backends +./jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader --products-filter --build-filter --platforms-filter linux-x64,windows-x64,osx-x64 --download-backends ~/backends +``` + +### Clients + +This is the same command as above, with the `--download-backends` flag removed. + +```shell +mkdir ~/clients +./jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader --products-filter --build-filter --platforms-filter linux-x64,windows-x64,osx-x64 ~/clients +``` + +We now have both clients and backends installed. + +## 3. Install a web server + +You will need to run a web server in order to serve requests to the backend and +client files. We installed `nginx` and setup an FQDN and routed all requests to +`/`. See below: + +```console +server { + listen 80 default_server; + listen [::]:80 default_server; + + root /var/www/html; + + index index.html index.htm index.nginx-debian.html; + + server_name _; + + location / { + root /home/ubuntu; + } +} +``` + +Then, configure your DNS entry to point to the IP address of the server. For the +purposes of the POC, we did not configure TLS, although that is a supported +option. + +## 4. Add Client Files + +You will need to add the following files on your local machine in order for +Gateway to pull the backend and client from the server. + +```shell +$ cat productsInfoUrl # a path to products.json that was generated by the backend's downloader (it could be http://, https://, or file://) + +https://internal.site/backends//products.json + +$ cat clientDownloadUrl # a path for clients that you got from the clients' downloader (it could be http://, https://, or file://) + +https://internal.site/clients/ + +$ cat jreDownloadUrl # a path for JBR that you got from the clients' downloader (it could be http://, https://, or file://) + +https://internal.site/jre/ + +$ cat pgpPublicKeyUrl # a URL to the KEYS file that was downloaded with the clients builds. + +https://internal.site/KEYS +``` + +The location of these files will depend upon your local operating system: + +
    + +### macOS + +```console +# User-specific settings +/Users/UserName/Library/Application Support/JetBrains/RemoteDev +# System-wide settings +/Library/Application Support/JetBrains/RemoteDev/ +``` + +### Linux + +```console +# User-specific settings +$HOME/.config/JetBrains/RemoteDev +# System-wide settings +/etc/xdg/JetBrains/RemoteDev/ +``` + +### Windows + +```console +# User-specific settings +HKEY_CURRENT_USER registry +# System-wide settings +HKEY_LOCAL_MACHINE registry +``` + +Additionally, create a string for each setting with its appropriate value in +`SOFTWARE\JetBrains\RemoteDev`: + +![JetBrains offline - Windows](../../../images/gateway/jetbrains-offline-windows.png) + +
    + +## 5. Setup SSH connection with JetBrains Gateway + +With the server now configured, you can now configure your local machine to use +Gateway. Here is the documentation to +[setup SSH config via the Coder CLI](../../../user-guides/workspace-access/index.md#configure-ssh). +On the Gateway side, follow our guide here until step 16. + +Instead of downloading from jetbrains.com, we will point Gateway to our server +endpoint. Select `Installation options...` and select `Use download link`. Note +that the URL must explicitly reference the archive file: + +![Offline Gateway](../../../images/gateway/offline-gateway.png) + +Click `Download IDE and Connect`. Gateway should now download the backend and +clients from the server into your remote workspace and local machine, +respectively. + +## Next steps + +- [Pre-install the JetBrains IDEs backend in your workspace](../../../admin/templates/extending-templates/jetbrains-gateway.md) diff --git a/docs/user-guides/workspace-access/jetbrains/jetbrains-pre-install.md b/docs/user-guides/workspace-access/jetbrains/jetbrains-pre-install.md new file mode 100644 index 0000000000000..862aee9c66fdd --- /dev/null +++ b/docs/user-guides/workspace-access/jetbrains/jetbrains-pre-install.md @@ -0,0 +1,119 @@ +# Pre-install JetBrains Gateway in a template + +For a faster JetBrains Gateway experience, pre-install the IDEs backend in your template. + +> [!NOTE] +> This guide only talks about installing the IDEs backend. For a complete guide on setting up JetBrains Gateway with client IDEs, refer to the [JetBrains Gateway air-gapped guide](./jetbrains-airgapped.md). + +## Install the Client Downloader + +Install the JetBrains Client Downloader binary: + +```shell +wget https://download.jetbrains.com/idea/code-with-me/backend/jetbrains-clients-downloader-linux-x86_64-1867.tar.gz && \ +tar -xzvf jetbrains-clients-downloader-linux-x86_64-1867.tar.gz +rm jetbrains-clients-downloader-linux-x86_64-1867.tar.gz +``` + +## Install Gateway backend + +```shell +mkdir ~/JetBrains +./jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader --products-filter --build-filter --platforms-filter linux-x64 --download-backends ~/JetBrains +``` + +For example, to install the build `243.26053.27` of IntelliJ IDEA: + +```shell +./jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader --products-filter IU --build-filter 243.26053.27 --platforms-filter linux-x64 --download-backends ~/JetBrains +tar -xzvf ~/JetBrains/backends/IU/*.tar.gz -C ~/JetBrains/backends/IU +rm -rf ~/JetBrains/backends/IU/*.tar.gz +``` + +## Register the Gateway backend + +Add the following command to your template's `startup_script`: + +```shell +~/JetBrains/backends/IU/ideaIU-243.26053.27/bin/remote-dev-server.sh registerBackendLocationForGateway +``` + +## Configure JetBrains Gateway Module + +If you are using our [jetbrains-gateway](https://registry.coder.com/modules/jetbrains-gateway) module, you can configure it by adding the following snippet to your template: + +```tf +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.28" + agent_id = coder_agent.main.id + folder = "/home/coder/example" + jetbrains_ides = ["IU"] + default = "IU" + latest = false + jetbrains_ide_versions = { + "IU" = { + build_number = "243.26053.27" + version = "2024.3" + } + } +} + +resource "coder_agent" "main" { + ... + startup_script = <<-EOF + ~/JetBrains/backends/IU/ideaIU-243.26053.27/bin/remote-dev-server.sh registerBackendLocationForGateway + EOF +} +``` + +## Dockerfile example + +If you are using Docker based workspaces, you can add the command to your Dockerfile: + +```dockerfile +FROM ubuntu + +# Combine all apt operations in a single RUN command +# Install only necessary packages +# Clean up apt cache in the same layer +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + curl \ + git \ + golang \ + sudo \ + vim \ + wget \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Create user in a single layer +ARG USER=coder +RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \ + && echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \ + && chmod 0440 /etc/sudoers.d/${USER} + +USER ${USER} +WORKDIR /home/${USER} + +# Install JetBrains Gateway in a single RUN command to reduce layers +# Download, extract, use, and clean up in the same layer +RUN mkdir -p ~/JetBrains \ + && wget -q https://download.jetbrains.com/idea/code-with-me/backend/jetbrains-clients-downloader-linux-x86_64-1867.tar.gz -P /tmp \ + && tar -xzf /tmp/jetbrains-clients-downloader-linux-x86_64-1867.tar.gz -C /tmp \ + && /tmp/jetbrains-clients-downloader-linux-x86_64-1867/bin/jetbrains-clients-downloader \ + --products-filter IU \ + --build-filter 243.26053.27 \ + --platforms-filter linux-x64 \ + --download-backends ~/JetBrains \ + && tar -xzf ~/JetBrains/backends/IU/*.tar.gz -C ~/JetBrains/backends/IU \ + && rm -f ~/JetBrains/backends/IU/*.tar.gz \ + && rm -rf /tmp/jetbrains-clients-downloader-linux-x86_64-1867* \ + && rm -rf /tmp/*.tar.gz +``` + +## Next steps + +- [Pre install the Client IDEs](./jetbrains-airgapped.md#1-deploy-the-server-and-install-the-client-downloader) diff --git a/docs/user-guides/workspace-lifecycle.md b/docs/user-guides/workspace-lifecycle.md index 833bc1307c4fd..f09cd63b8055d 100644 --- a/docs/user-guides/workspace-lifecycle.md +++ b/docs/user-guides/workspace-lifecycle.md @@ -55,7 +55,7 @@ contain some computational resource to run the Coder agent process. The provisioned workspace's computational resources start the agent process, which opens connections to your workspace via SSH, the terminal, and IDES such -as [JetBrains](./workspace-access/jetbrains.md) or +as [JetBrains](./workspace-access/jetbrains/index.md) or [VSCode](./workspace-access/vscode.md). Once started, the Coder agent is responsible for running your workspace startup From 7b0422b49b8e5e95850711b18fa04ecb8ff5fd0a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 11 Apr 2025 18:58:17 +0100 Subject: [PATCH 070/384] fix(codersdk/toolsdk): fix tool schemata (#17365) Fixes two issues with the MCP server: - Ensures we have a non-null schema, as the following schema was making claude-code unhappy: ``` "inputSchema": { "type": "object", "properties": null }, ``` - Skip adding the coder_report_task tool if an agent client is not available. Otherwise the agent may try to report tasks and get confused. --- cli/exp_mcp.go | 12 ++++++++++++ codersdk/toolsdk/toolsdk.go | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/cli/exp_mcp.go b/cli/exp_mcp.go index 8b8c96ab41863..35032a43d68fc 100644 --- a/cli/exp_mcp.go +++ b/cli/exp_mcp.go @@ -402,7 +402,9 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct // Create a new context for the tools with all relevant information. clientCtx := toolsdk.WithClient(ctx, client) // Get the workspace agent token from the environment. + var hasAgentClient bool if agentToken, err := getAgentToken(fs); err == nil && agentToken != "" { + hasAgentClient = true agentClient := agentsdk.New(client.URL) agentClient.SetSessionToken(agentToken) clientCtx = toolsdk.WithAgentClient(clientCtx, agentClient) @@ -417,6 +419,11 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct // Register tools based on the allowlist (if specified) for _, tool := range toolsdk.All { + // Skip adding the coder_report_task tool if there is no agent client + if !hasAgentClient && tool.Tool.Name == "coder_report_task" { + cliui.Warnf(inv.Stderr, "Task reporting not available") + continue + } if len(allowedTools) == 0 || slices.ContainsFunc(allowedTools, func(t string) bool { return t == tool.Tool.Name }) { @@ -689,6 +696,11 @@ func getAgentToken(fs afero.Fs) (string, error) { // mcpFromSDK adapts a toolsdk.Tool to go-mcp's server.ServerTool. // It assumes that the tool responds with a valid JSON object. func mcpFromSDK(sdkTool toolsdk.Tool[any]) server.ServerTool { + // NOTE: some clients will silently refuse to use tools if there is an issue + // with the tool's schema or configuration. + if sdkTool.Schema.Properties == nil { + panic("developer error: schema properties cannot be nil") + } return server.ServerTool{ Tool: mcp.Tool{ Name: sdkTool.Tool.Name, diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index 835c37a65180e..134c30c4f1474 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -259,6 +259,10 @@ is provisioned correctly and the agent can connect to the control plane. Tool: aisdk.Tool{ Name: "coder_list_templates", Description: "Lists templates for the authenticated user.", + Schema: aisdk.Schema{ + Properties: map[string]any{}, + Required: []string{}, + }, }, Handler: func(ctx context.Context, _ map[string]any) ([]MinimalTemplate, error) { client, err := clientFromContext(ctx) @@ -318,6 +322,10 @@ is provisioned correctly and the agent can connect to the control plane. Tool: aisdk.Tool{ Name: "coder_get_authenticated_user", Description: "Get the currently authenticated user, similar to the `whoami` command.", + Schema: aisdk.Schema{ + Properties: map[string]any{}, + Required: []string{}, + }, }, Handler: func(ctx context.Context, _ map[string]any) (codersdk.User, error) { client, err := clientFromContext(ctx) From 15584e69ef214f0d7e2cbd7532f9a2811fc3b47e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 11 Apr 2025 13:21:46 -0500 Subject: [PATCH 071/384] chore: fixup typegen for preview types (#17339) Preview types override the json marshal behavior. --- codersdk/templateversions.go | 8 +++ scripts/apitypings/main.go | 25 ++++++-- site/src/api/typesGenerated.ts | 110 ++++++++++++++++++++------------- 3 files changed, 95 insertions(+), 48 deletions(-) diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go index e21991d0e98f3..0bcc4b5463903 100644 --- a/codersdk/templateversions.go +++ b/codersdk/templateversions.go @@ -141,6 +141,14 @@ type DynamicParametersResponse struct { // TODO: Workspace tags } +// FriendlyDiagnostic is included to guarantee it is generated in the output +// types. This is used as the type override for `previewtypes.Diagnostic`. +type FriendlyDiagnostic = previewtypes.FriendlyDiagnostic + +// NullHCLString is included to guarantee it is generated in the output +// types. This is used as the type override for `previewtypes.HCLString`. +type NullHCLString = previewtypes.NullHCLString + func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) { conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/templateversions/%s/dynamic-parameters", version), nil) if err != nil { diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 5dcf6ae5dfc15..3fd25948162dd 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -32,10 +32,9 @@ func main() { // Serpent has some types referenced in the codersdk. // We want the referenced types generated. referencePackages := map[string]string{ - "github.com/coder/preview": "", - "github.com/coder/serpent": "Serpent", - "github.com/hashicorp/hcl/v2": "Hcl", - "tailscale.com/derp": "", + "github.com/coder/preview/types": "Preview", + "github.com/coder/serpent": "Serpent", + "tailscale.com/derp": "", // Conflicting name "DERPRegion" "tailscale.com/tailcfg": "Tail", "tailscale.com/net/netcheck": "Netcheck", @@ -90,8 +89,22 @@ func TypeMappings(gen *guts.GoParser) error { gen.IncludeCustomDeclaration(map[string]guts.TypeOverride{ "github.com/coder/coder/v2/codersdk.NullTime": config.OverrideNullable(config.OverrideLiteral(bindings.KeywordString)), // opt.Bool can return 'null' if unset - "tailscale.com/types/opt.Bool": config.OverrideNullable(config.OverrideLiteral(bindings.KeywordBoolean)), - "github.com/hashicorp/hcl/v2.Expression": config.OverrideLiteral(bindings.KeywordUnknown), + "tailscale.com/types/opt.Bool": config.OverrideNullable(config.OverrideLiteral(bindings.KeywordBoolean)), + // hcl diagnostics should be cast to `preview.FriendlyDiagnostic` + "github.com/hashicorp/hcl/v2.Diagnostic": func() bindings.ExpressionType { + return bindings.Reference(bindings.Identifier{ + Name: "FriendlyDiagnostic", + Package: nil, + Prefix: "", + }) + }, + "github.com/coder/preview/types.HCLString": func() bindings.ExpressionType { + return bindings.Reference(bindings.Identifier{ + Name: "NullHCLString", + Package: nil, + Prefix: "", + }) + }, }) err := gen.IncludeCustom(map[string]string{ diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d1f38243988a3..0bca431b7a574 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -715,10 +715,8 @@ export interface DynamicParametersRequest { // From codersdk/templateversions.go export interface DynamicParametersResponse { readonly id: number; - // this is likely an enum in an external package "github.com/coder/preview/types.Diagnostics" - readonly diagnostics: readonly (HclDiagnostic | null)[]; - // external type "github.com/coder/preview/types.Parameter", to include this type the package must be explicitly included in the parsing - readonly parameters: readonly unknown[]; + readonly diagnostics: PreviewDiagnostics; + readonly parameters: readonly PreviewParameter[]; } // From codersdk/externalauth.go @@ -914,6 +912,13 @@ export const FeatureSets: FeatureSet[] = ["enterprise", "", "premium"]; // From codersdk/files.go export const FormatZip = "zip"; +// From codersdk/templateversions.go +export interface FriendlyDiagnostic { + readonly severity: PreviewDiagnosticSeverityString; + readonly summary: string; + readonly detail: string; +} + // From codersdk/apikey.go export interface GenerateAPIKeyResponse { readonly key: string; @@ -997,44 +1002,6 @@ export interface HTTPCookieConfig { readonly same_site?: string; } -// From hcl/diagnostic.go -export interface HclDiagnostic { - readonly Severity: HclDiagnosticSeverity; - readonly Summary: string; - readonly Detail: string; - readonly Subject: HclRange | null; - readonly Context: HclRange | null; - readonly Expression: unknown; - readonly EvalContext: HclEvalContext | null; - // empty interface{} type, falling back to unknown - readonly Extra: unknown; -} - -// From hcl/diagnostic.go -export type HclDiagnosticSeverity = number; - -// From hcl/eval_context.go -export interface HclEvalContext { - // external type "github.com/zclconf/go-cty/cty.Value", to include this type the package must be explicitly included in the parsing - readonly Variables: Record; - // external type "github.com/zclconf/go-cty/cty/function.Function", to include this type the package must be explicitly included in the parsing - readonly Functions: Record; -} - -// From hcl/pos.go -export interface HclPos { - readonly Line: number; - readonly Column: number; - readonly Byte: number; -} - -// From hcl/pos.go -export interface HclRange { - readonly Filename: string; - readonly Start: HclPos; - readonly End: HclPos; -} - // From health/model.go export type HealthCode = | "EACS03" @@ -1439,6 +1406,12 @@ export interface NotificationsWebhookConfig { readonly endpoint: string; } +// From codersdk/templateversions.go +export interface NullHCLString { + readonly value: string; + readonly valid: boolean; +} + // From codersdk/oauth2.go export interface OAuth2AppEndpoints { readonly authorization: string; @@ -1740,6 +1713,59 @@ export interface PresetParameter { readonly Value: string; } +// From types/diagnostics.go +export type PreviewDiagnosticSeverityString = string; + +// From types/diagnostics.go +export type PreviewDiagnostics = readonly (FriendlyDiagnostic | null)[]; + +// From types/parameter.go +export interface PreviewParameter extends PreviewParameterData { + readonly value: NullHCLString; + readonly diagnostics: PreviewDiagnostics; +} + +// From types/parameter.go +export interface PreviewParameterData { + readonly name: string; + readonly display_name: string; + readonly description: string; + readonly type: PreviewParameterType; + // this is likely an enum in an external package "github.com/coder/terraform-provider-coder/v2/provider.ParameterFormType" + readonly form_type: string; + // empty interface{} type, falling back to unknown + readonly styling: unknown; + readonly mutable: boolean; + readonly default_value: NullHCLString; + readonly icon: string; + readonly options: readonly (PreviewParameterOption | null)[]; + readonly validations: readonly (PreviewParameterValidation | null)[]; + readonly required: boolean; + readonly order: number; + readonly ephemeral: boolean; +} + +// From types/parameter.go +export interface PreviewParameterOption { + readonly name: string; + readonly description: string; + readonly value: NullHCLString; + readonly icon: string; +} + +// From types/enum.go +export type PreviewParameterType = string; + +// From types/parameter.go +export interface PreviewParameterValidation { + readonly validation_error: string; + readonly validation_regex: string | null; + readonly validation_min: number | null; + readonly validation_max: number | null; + readonly validation_monotonic: string | null; + readonly validation_invalid: boolean | null; +} + // From codersdk/deployment.go export interface PrometheusConfig { readonly enable: boolean; From c06ef7c1eb45a129ca47cdfeaf75e853e6cee95b Mon Sep 17 00:00:00 2001 From: Jon Ayers Date: Fri, 11 Apr 2025 14:45:21 -0400 Subject: [PATCH 072/384] chore!: remove JFrog integration (#17353) - Removes displaying XRay scan results in the dashboard. I'm not sure anyone was even using this integration so it's just debt for us to maintain. We can open up a separate issue to get rid of the db tables once we know for sure that we haven't broken anyone. --- coderd/apidoc/docs.go | 103 --------------- coderd/apidoc/swagger.json | 93 ------------- coderd/database/dbauthz/dbauthz.go | 28 ---- coderd/database/dbauthz/dbauthz_test.go | 68 ---------- coderd/database/dbmem/dbmem.go | 56 +------- coderd/database/dbmetrics/querymetrics.go | 14 -- coderd/database/dbmock/dbmock.go | 29 ----- coderd/database/querier.go | 2 - coderd/database/queries.sql.go | 69 ---------- coderd/database/queries/jfrog.sql | 26 ---- codersdk/jfrog.go | 50 ------- docs/reference/api/enterprise.md | 101 --------------- docs/reference/api/schemas.md | 24 ---- enterprise/coderd/coderd.go | 10 -- enterprise/coderd/jfrog.go | 120 ----------------- enterprise/coderd/jfrog_test.go | 122 ------------------ site/src/api/api.ts | 28 ---- site/src/api/queries/integrations.ts | 9 -- site/src/api/typesGenerated.ts | 10 -- .../modules/resources/AgentRow.stories.tsx | 21 --- site/src/modules/resources/AgentRow.tsx | 9 -- site/src/modules/resources/XRayScanAlert.tsx | 108 ---------------- site/src/testHelpers/handlers.ts | 4 - 23 files changed, 2 insertions(+), 1102 deletions(-) delete mode 100644 coderd/database/queries/jfrog.sql delete mode 100644 codersdk/jfrog.go delete mode 100644 enterprise/coderd/jfrog.go delete mode 100644 enterprise/coderd/jfrog_test.go delete mode 100644 site/src/api/queries/integrations.ts delete mode 100644 site/src/modules/resources/XRayScanAlert.tsx diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index ba1cf6cc30bac..b9d54d989a723 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1432,84 +1432,6 @@ const docTemplate = `{ } } }, - "/integrations/jfrog/xray-scan": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "Enterprise" - ], - "summary": "Get JFrog XRay scan by workspace agent ID.", - "operationId": "get-jfrog-xray-scan-by-workspace-agent-id", - "parameters": [ - { - "type": "string", - "description": "Workspace ID", - "name": "workspace_id", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Agent ID", - "name": "agent_id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.JFrogXrayScan" - } - } - } - }, - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Enterprise" - ], - "summary": "Post JFrog XRay scan by workspace agent ID.", - "operationId": "post-jfrog-xray-scan-by-workspace-agent-id", - "parameters": [ - { - "description": "Post JFrog XRay scan request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/codersdk.JFrogXrayScan" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Response" - } - } - } - } - }, "/licenses": { "get": { "security": [ @@ -12579,31 +12501,6 @@ const docTemplate = `{ } } }, - "codersdk.JFrogXrayScan": { - "type": "object", - "properties": { - "agent_id": { - "type": "string", - "format": "uuid" - }, - "critical": { - "type": "integer" - }, - "high": { - "type": "integer" - }, - "medium": { - "type": "integer" - }, - "results_url": { - "type": "string" - }, - "workspace_id": { - "type": "string", - "format": "uuid" - } - } - }, "codersdk.JobErrorCode": { "type": "string", "enum": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5a8d199e0a9d2..b5bb734260814 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1249,74 +1249,6 @@ } } }, - "/integrations/jfrog/xray-scan": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "produces": ["application/json"], - "tags": ["Enterprise"], - "summary": "Get JFrog XRay scan by workspace agent ID.", - "operationId": "get-jfrog-xray-scan-by-workspace-agent-id", - "parameters": [ - { - "type": "string", - "description": "Workspace ID", - "name": "workspace_id", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "Agent ID", - "name": "agent_id", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.JFrogXrayScan" - } - } - } - }, - "post": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "consumes": ["application/json"], - "produces": ["application/json"], - "tags": ["Enterprise"], - "summary": "Post JFrog XRay scan by workspace agent ID.", - "operationId": "post-jfrog-xray-scan-by-workspace-agent-id", - "parameters": [ - { - "description": "Post JFrog XRay scan request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/codersdk.JFrogXrayScan" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/codersdk.Response" - } - } - } - } - }, "/licenses": { "get": { "security": [ @@ -11311,31 +11243,6 @@ } } }, - "codersdk.JFrogXrayScan": { - "type": "object", - "properties": { - "agent_id": { - "type": "string", - "format": "uuid" - }, - "critical": { - "type": "integer" - }, - "high": { - "type": "integer" - }, - "medium": { - "type": "integer" - }, - "results_url": { - "type": "string" - }, - "workspace_id": { - "type": "string", - "format": "uuid" - } - } - }, "codersdk.JobErrorCode": { "type": "string", "enum": ["REQUIRED_TEMPLATE_VARIABLES"], diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 980e7fd9c1941..b9eb8b05e171e 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1895,13 +1895,6 @@ func (q *querier) GetInboxNotificationsByUserID(ctx context.Context, userID data return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetInboxNotificationsByUserID)(ctx, userID) } -func (q *querier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) { - if _, err := fetch(q.log, q.auth, q.db.GetWorkspaceByID)(ctx, arg.WorkspaceID); err != nil { - return database.JfrogXrayScan{}, err - } - return q.db.GetJFrogXrayScanByWorkspaceAndAgentID(ctx, arg) -} - func (q *querier) GetLastUpdateCheck(ctx context.Context) (string, error) { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { return "", err @@ -4767,27 +4760,6 @@ func (q *querier) UpsertHealthSettings(ctx context.Context, value string) error return q.db.UpsertHealthSettings(ctx, value) } -func (q *querier) UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { - // TODO: Having to do all this extra querying makes me a sad panda. - workspace, err := q.db.GetWorkspaceByID(ctx, arg.WorkspaceID) - if err != nil { - return xerrors.Errorf("get workspace by id: %w", err) - } - - template, err := q.db.GetTemplateByID(ctx, workspace.TemplateID) - if err != nil { - return xerrors.Errorf("get template by id: %w", err) - } - - // Only template admins should be able to write JFrog Xray scans to a workspace. - // We don't want this to be a workspace-level permission because then users - // could overwrite their own results. - if err := q.authorizeContext(ctx, policy.ActionCreate, template); err != nil { - return err - } - return q.db.UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx, arg) -} - func (q *querier) UpsertLastUpdateCheck(ctx context.Context, value string) error { if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil { return err diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 8cf58f1a360c4..711934a2c1146 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -4293,74 +4293,6 @@ func (s *MethodTestSuite) TestSystemFunctions() { s.Run("GetUserLinksByUserID", s.Subtest(func(db database.Store, check *expects) { check.Args(uuid.New()).Asserts(rbac.ResourceSystem, policy.ActionRead) })) - s.Run("GetJFrogXrayScanByWorkspaceAndAgentID", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - org := dbgen.Organization(s.T(), db, database.Organization{}) - tpl := dbgen.Template(s.T(), db, database.Template{ - OrganizationID: org.ID, - CreatedBy: u.ID, - }) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ - OwnerID: u.ID, - OrganizationID: org.ID, - TemplateID: tpl.ID, - }) - pj := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{ - JobID: pj.ID, - }) - agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ - ResourceID: res.ID, - }) - - err := db.UpsertJFrogXrayScanByWorkspaceAndAgentID(context.Background(), database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams{ - AgentID: agent.ID, - WorkspaceID: ws.ID, - Critical: 1, - High: 12, - Medium: 14, - ResultsUrl: "http://hello", - }) - require.NoError(s.T(), err) - - expect := database.JfrogXrayScan{ - WorkspaceID: ws.ID, - AgentID: agent.ID, - Critical: 1, - High: 12, - Medium: 14, - ResultsUrl: "http://hello", - } - - check.Args(database.GetJFrogXrayScanByWorkspaceAndAgentIDParams{ - WorkspaceID: ws.ID, - AgentID: agent.ID, - }).Asserts(ws, policy.ActionRead).Returns(expect) - })) - s.Run("UpsertJFrogXrayScanByWorkspaceAndAgentID", s.Subtest(func(db database.Store, check *expects) { - u := dbgen.User(s.T(), db, database.User{}) - org := dbgen.Organization(s.T(), db, database.Organization{}) - tpl := dbgen.Template(s.T(), db, database.Template{ - OrganizationID: org.ID, - CreatedBy: u.ID, - }) - ws := dbgen.Workspace(s.T(), db, database.WorkspaceTable{ - OwnerID: u.ID, - OrganizationID: org.ID, - TemplateID: tpl.ID, - }) - pj := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{}) - res := dbgen.WorkspaceResource(s.T(), db, database.WorkspaceResource{ - JobID: pj.ID, - }) - agent := dbgen.WorkspaceAgent(s.T(), db, database.WorkspaceAgent{ - ResourceID: res.ID, - }) - check.Args(database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams{ - WorkspaceID: ws.ID, - AgentID: agent.ID, - }).Asserts(tpl, policy.ActionCreate) - })) s.Run("DeleteRuntimeConfig", s.Subtest(func(db database.Store, check *expects) { check.Args("test").Asserts(rbac.ResourceSystem, policy.ActionDelete) })) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index cf8cf00ca9eed..18e68caf6ee7c 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -222,7 +222,6 @@ type data struct { gitSSHKey []database.GitSSHKey groupMembers []database.GroupMemberTable groups []database.Group - jfrogXRayScans []database.JfrogXrayScan licenses []database.License notificationMessages []database.NotificationMessage notificationPreferences []database.NotificationPreference @@ -3687,24 +3686,6 @@ func (q *FakeQuerier) GetInboxNotificationsByUserID(_ context.Context, params da return notifications, nil } -func (q *FakeQuerier) GetJFrogXrayScanByWorkspaceAndAgentID(_ context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) { - err := validateDatabaseType(arg) - if err != nil { - return database.JfrogXrayScan{}, err - } - - q.mutex.RLock() - defer q.mutex.RUnlock() - - for _, scan := range q.jfrogXRayScans { - if scan.AgentID == arg.AgentID && scan.WorkspaceID == arg.WorkspaceID { - return scan, nil - } - } - - return database.JfrogXrayScan{}, sql.ErrNoRows -} - func (q *FakeQuerier) GetLastUpdateCheck(_ context.Context) (string, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -4241,7 +4222,7 @@ func (q *FakeQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (da if preset.ID == presetID { tv, ok := versionMap[preset.TemplateVersionID] if !ok { - return empty, fmt.Errorf("template version %v does not exist", preset.TemplateVersionID) + return empty, xerrors.Errorf("template version %v does not exist", preset.TemplateVersionID) } return database.GetPresetByIDRow{ ID: preset.ID, @@ -4256,7 +4237,7 @@ func (q *FakeQuerier) GetPresetByID(ctx context.Context, presetID uuid.UUID) (da } } - return empty, fmt.Errorf("preset %v does not exist", presetID) + return empty, xerrors.Errorf("preset %v does not exist", presetID) } func (q *FakeQuerier) GetPresetByWorkspaceBuildID(_ context.Context, workspaceBuildID uuid.UUID) (database.TemplateVersionPreset, error) { @@ -11986,39 +11967,6 @@ func (q *FakeQuerier) UpsertHealthSettings(_ context.Context, data string) error return nil } -func (q *FakeQuerier) UpsertJFrogXrayScanByWorkspaceAndAgentID(_ context.Context, arg database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { - err := validateDatabaseType(arg) - if err != nil { - return err - } - - q.mutex.Lock() - defer q.mutex.Unlock() - - for i, scan := range q.jfrogXRayScans { - if scan.AgentID == arg.AgentID && scan.WorkspaceID == arg.WorkspaceID { - scan.Critical = arg.Critical - scan.High = arg.High - scan.Medium = arg.Medium - scan.ResultsUrl = arg.ResultsUrl - q.jfrogXRayScans[i] = scan - return nil - } - } - - //nolint:gosimple - q.jfrogXRayScans = append(q.jfrogXRayScans, database.JfrogXrayScan{ - WorkspaceID: arg.WorkspaceID, - AgentID: arg.AgentID, - Critical: arg.Critical, - High: arg.High, - Medium: arg.Medium, - ResultsUrl: arg.ResultsUrl, - }) - - return nil -} - func (q *FakeQuerier) UpsertLastUpdateCheck(_ context.Context, data string) error { q.mutex.Lock() defer q.mutex.Unlock() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index c90d083fa20c7..b76d70c764cf6 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -858,13 +858,6 @@ func (m queryMetricsStore) GetInboxNotificationsByUserID(ctx context.Context, us return r0, r1 } -func (m queryMetricsStore) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) { - start := time.Now() - r0, r1 := m.s.GetJFrogXrayScanByWorkspaceAndAgentID(ctx, arg) - m.queryLatencies.WithLabelValues("GetJFrogXrayScanByWorkspaceAndAgentID").Observe(time.Since(start).Seconds()) - return r0, r1 -} - func (m queryMetricsStore) GetLastUpdateCheck(ctx context.Context) (string, error) { start := time.Now() version, err := m.s.GetLastUpdateCheck(ctx) @@ -3042,13 +3035,6 @@ func (m queryMetricsStore) UpsertHealthSettings(ctx context.Context, value strin return r0 } -func (m queryMetricsStore) UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { - start := time.Now() - r0 := m.s.UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx, arg) - m.queryLatencies.WithLabelValues("UpsertJFrogXrayScanByWorkspaceAndAgentID").Observe(time.Since(start).Seconds()) - return r0 -} - func (m queryMetricsStore) UpsertLastUpdateCheck(ctx context.Context, value string) error { start := time.Now() r0 := m.s.UpsertLastUpdateCheck(ctx, value) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index e015a72094aa9..10adfd7c5a408 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -1729,21 +1729,6 @@ func (mr *MockStoreMockRecorder) GetInboxNotificationsByUserID(ctx, arg any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInboxNotificationsByUserID", reflect.TypeOf((*MockStore)(nil).GetInboxNotificationsByUserID), ctx, arg) } -// GetJFrogXrayScanByWorkspaceAndAgentID mocks base method. -func (m *MockStore) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetJFrogXrayScanByWorkspaceAndAgentID", ctx, arg) - ret0, _ := ret[0].(database.JfrogXrayScan) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetJFrogXrayScanByWorkspaceAndAgentID indicates an expected call of GetJFrogXrayScanByWorkspaceAndAgentID. -func (mr *MockStoreMockRecorder) GetJFrogXrayScanByWorkspaceAndAgentID(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJFrogXrayScanByWorkspaceAndAgentID", reflect.TypeOf((*MockStore)(nil).GetJFrogXrayScanByWorkspaceAndAgentID), ctx, arg) -} - // GetLastUpdateCheck mocks base method. func (m *MockStore) GetLastUpdateCheck(ctx context.Context) (string, error) { m.ctrl.T.Helper() @@ -6415,20 +6400,6 @@ func (mr *MockStoreMockRecorder) UpsertHealthSettings(ctx, value any) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertHealthSettings", reflect.TypeOf((*MockStore)(nil).UpsertHealthSettings), ctx, value) } -// UpsertJFrogXrayScanByWorkspaceAndAgentID mocks base method. -func (m *MockStore) UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertJFrogXrayScanByWorkspaceAndAgentID", ctx, arg) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpsertJFrogXrayScanByWorkspaceAndAgentID indicates an expected call of UpsertJFrogXrayScanByWorkspaceAndAgentID. -func (mr *MockStoreMockRecorder) UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertJFrogXrayScanByWorkspaceAndAgentID", reflect.TypeOf((*MockStore)(nil).UpsertJFrogXrayScanByWorkspaceAndAgentID), ctx, arg) -} - // UpsertLastUpdateCheck mocks base method. func (m *MockStore) UpsertLastUpdateCheck(ctx context.Context, value string) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 7494cbc04b770..1cef5ada197f5 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -200,7 +200,6 @@ type sqlcQuerier interface { // param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value // param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25 GetInboxNotificationsByUserID(ctx context.Context, arg GetInboxNotificationsByUserIDParams) ([]InboxNotification, error) - GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error) GetLastUpdateCheck(ctx context.Context) (string, error) GetLatestCryptoKeyByFeature(ctx context.Context, feature CryptoKeyFeature) (CryptoKey, error) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) @@ -619,7 +618,6 @@ type sqlcQuerier interface { // The functional values are immutable and controlled implicitly. UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error UpsertHealthSettings(ctx context.Context, value string) error - UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error UpsertLastUpdateCheck(ctx context.Context, value string) error UpsertLogoURL(ctx context.Context, value string) error // Insert or update notification report generator logs with recent activity. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 25bfe1db63bb3..0d5fa1bb7f060 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3570,75 +3570,6 @@ func (q *sqlQuerier) UpsertTemplateUsageStats(ctx context.Context) error { return err } -const getJFrogXrayScanByWorkspaceAndAgentID = `-- name: GetJFrogXrayScanByWorkspaceAndAgentID :one -SELECT - agent_id, workspace_id, critical, high, medium, results_url -FROM - jfrog_xray_scans -WHERE - agent_id = $1 -AND - workspace_id = $2 -LIMIT - 1 -` - -type GetJFrogXrayScanByWorkspaceAndAgentIDParams struct { - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` -} - -func (q *sqlQuerier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error) { - row := q.db.QueryRowContext(ctx, getJFrogXrayScanByWorkspaceAndAgentID, arg.AgentID, arg.WorkspaceID) - var i JfrogXrayScan - err := row.Scan( - &i.AgentID, - &i.WorkspaceID, - &i.Critical, - &i.High, - &i.Medium, - &i.ResultsUrl, - ) - return i, err -} - -const upsertJFrogXrayScanByWorkspaceAndAgentID = `-- name: UpsertJFrogXrayScanByWorkspaceAndAgentID :exec -INSERT INTO - jfrog_xray_scans ( - agent_id, - workspace_id, - critical, - high, - medium, - results_url - ) -VALUES - ($1, $2, $3, $4, $5, $6) -ON CONFLICT (agent_id, workspace_id) -DO UPDATE SET critical = $3, high = $4, medium = $5, results_url = $6 -` - -type UpsertJFrogXrayScanByWorkspaceAndAgentIDParams struct { - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - Critical int32 `db:"critical" json:"critical"` - High int32 `db:"high" json:"high"` - Medium int32 `db:"medium" json:"medium"` - ResultsUrl string `db:"results_url" json:"results_url"` -} - -func (q *sqlQuerier) UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg UpsertJFrogXrayScanByWorkspaceAndAgentIDParams) error { - _, err := q.db.ExecContext(ctx, upsertJFrogXrayScanByWorkspaceAndAgentID, - arg.AgentID, - arg.WorkspaceID, - arg.Critical, - arg.High, - arg.Medium, - arg.ResultsUrl, - ) - return err -} - const deleteLicense = `-- name: DeleteLicense :one DELETE FROM licenses diff --git a/coderd/database/queries/jfrog.sql b/coderd/database/queries/jfrog.sql deleted file mode 100644 index de9467c5323dd..0000000000000 --- a/coderd/database/queries/jfrog.sql +++ /dev/null @@ -1,26 +0,0 @@ --- name: GetJFrogXrayScanByWorkspaceAndAgentID :one -SELECT - * -FROM - jfrog_xray_scans -WHERE - agent_id = $1 -AND - workspace_id = $2 -LIMIT - 1; - --- name: UpsertJFrogXrayScanByWorkspaceAndAgentID :exec -INSERT INTO - jfrog_xray_scans ( - agent_id, - workspace_id, - critical, - high, - medium, - results_url - ) -VALUES - ($1, $2, $3, $4, $5, $6) -ON CONFLICT (agent_id, workspace_id) -DO UPDATE SET critical = $3, high = $4, medium = $5, results_url = $6; diff --git a/codersdk/jfrog.go b/codersdk/jfrog.go deleted file mode 100644 index aa7fec25727cd..0000000000000 --- a/codersdk/jfrog.go +++ /dev/null @@ -1,50 +0,0 @@ -package codersdk - -import ( - "context" - "encoding/json" - "net/http" - - "github.com/google/uuid" - "golang.org/x/xerrors" -) - -type JFrogXrayScan struct { - WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` - AgentID uuid.UUID `json:"agent_id" format:"uuid"` - Critical int `json:"critical"` - High int `json:"high"` - Medium int `json:"medium"` - ResultsURL string `json:"results_url"` -} - -func (c *Client) PostJFrogXrayScan(ctx context.Context, req JFrogXrayScan) error { - res, err := c.Request(ctx, http.MethodPost, "/api/v2/integrations/jfrog/xray-scan", req) - if err != nil { - return xerrors.Errorf("make request: %w", err) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusCreated { - return ReadBodyAsError(res) - } - return nil -} - -func (c *Client) JFrogXRayScan(ctx context.Context, workspaceID, agentID uuid.UUID) (JFrogXrayScan, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/integrations/jfrog/xray-scan", nil, - WithQueryParam("workspace_id", workspaceID.String()), - WithQueryParam("agent_id", agentID.String()), - ) - if err != nil { - return JFrogXrayScan{}, xerrors.Errorf("make request: %w", err) - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return JFrogXrayScan{}, ReadBodyAsError(res) - } - - var resp JFrogXrayScan - return resp, json.NewDecoder(res.Body).Decode(&resp) -} diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 152f331fc81d5..643ad81390cab 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -490,107 +490,6 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Get JFrog XRay scan by workspace agent ID - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/integrations/jfrog/xray-scan?workspace_id=string&agent_id=string \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /integrations/jfrog/xray-scan` - -### Parameters - -| Name | In | Type | Required | Description | -|----------------|-------|--------|----------|--------------| -| `workspace_id` | query | string | true | Workspace ID | -| `agent_id` | query | string | true | Agent ID | - -### Example responses - -> 200 Response - -```json -{ - "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", - "critical": 0, - "high": 0, - "medium": 0, - "results_url": "string", - "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|------------------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.JFrogXrayScan](schemas.md#codersdkjfrogxrayscan) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - -## Post JFrog XRay scan by workspace agent ID - -### Code samples - -```shell -# Example request using curl -curl -X POST http://coder-server:8080/api/v2/integrations/jfrog/xray-scan \ - -H 'Content-Type: application/json' \ - -H 'Accept: application/json' \ - -H 'Coder-Session-Token: API_KEY' -``` - -`POST /integrations/jfrog/xray-scan` - -> Body parameter - -```json -{ - "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", - "critical": 0, - "high": 0, - "medium": 0, - "results_url": "string", - "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" -} -``` - -### Parameters - -| Name | In | Type | Required | Description | -|--------|------|------------------------------------------------------------|----------|------------------------------| -| `body` | body | [codersdk.JFrogXrayScan](schemas.md#codersdkjfrogxrayscan) | true | Post JFrog XRay scan request | - -### Example responses - -> 200 Response - -```json -{ - "detail": "string", - "message": "string", - "validations": [ - { - "detail": "string", - "field": "string" - } - ] -} -``` - -### Responses - -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|--------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## Get licenses ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index fb9c3b8db782f..870c113f67ace 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -3414,30 +3414,6 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith |----------------|--------|----------|--------------|-------------| | `signed_token` | string | false | | | -## codersdk.JFrogXrayScan - -```json -{ - "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", - "critical": 0, - "high": 0, - "medium": 0, - "results_url": "string", - "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -|----------------|---------|----------|--------------|-------------| -| `agent_id` | string | false | | | -| `critical` | integer | false | | | -| `high` | integer | false | | | -| `medium` | integer | false | | | -| `results_url` | string | false | | | -| `workspace_id` | string | false | | | - ## codersdk.JobErrorCode ```json diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index c451e71fc445e..6b45bc65e2c3f 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -470,16 +470,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) { r.Get("/", api.userQuietHoursSchedule) r.Put("/", api.putUserQuietHoursSchedule) }) - r.Route("/integrations", func(r chi.Router) { - r.Use( - apiKeyMiddleware, - api.jfrogEnabledMW, - ) - - r.Post("/jfrog/xray-scan", api.postJFrogXrayScan) - r.Get("/jfrog/xray-scan", api.jFrogXrayScan) - }) - // The /notifications base route is mounted by the AGPL router, so we can't group it here. // Additionally, because we have a static route for /notifications/templates/system which conflicts // with the below route, we need to register this route without any mounts or groups to make both work. diff --git a/enterprise/coderd/jfrog.go b/enterprise/coderd/jfrog.go deleted file mode 100644 index 1b7cc27247936..0000000000000 --- a/enterprise/coderd/jfrog.go +++ /dev/null @@ -1,120 +0,0 @@ -package coderd - -import ( - "net/http" - - "github.com/google/uuid" - - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/httpapi" - "github.com/coder/coder/v2/codersdk" -) - -// Post workspace agent results for a JFrog XRay scan. -// -// @Summary Post JFrog XRay scan by workspace agent ID. -// @ID post-jfrog-xray-scan-by-workspace-agent-id -// @Security CoderSessionToken -// @Accept json -// @Produce json -// @Tags Enterprise -// @Param request body codersdk.JFrogXrayScan true "Post JFrog XRay scan request" -// @Success 200 {object} codersdk.Response -// @Router /integrations/jfrog/xray-scan [post] -func (api *API) postJFrogXrayScan(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var req codersdk.JFrogXrayScan - if !httpapi.Read(ctx, rw, r, &req) { - return - } - - err := api.Database.UpsertJFrogXrayScanByWorkspaceAndAgentID(ctx, database.UpsertJFrogXrayScanByWorkspaceAndAgentIDParams{ - WorkspaceID: req.WorkspaceID, - AgentID: req.AgentID, - // #nosec G115 - Vulnerability counts are small and fit in int32 - Critical: int32(req.Critical), - // #nosec G115 - Vulnerability counts are small and fit in int32 - High: int32(req.High), - // #nosec G115 - Vulnerability counts are small and fit in int32 - Medium: int32(req.Medium), - ResultsUrl: req.ResultsURL, - }) - if httpapi.Is404Error(err) { - httpapi.ResourceNotFound(rw) - return - } - if err != nil { - httpapi.InternalServerError(rw, err) - return - } - - httpapi.Write(ctx, rw, http.StatusCreated, codersdk.Response{ - Message: "Successfully inserted JFrog XRay scan!", - }) -} - -// Get workspace agent results for a JFrog XRay scan. -// -// @Summary Get JFrog XRay scan by workspace agent ID. -// @ID get-jfrog-xray-scan-by-workspace-agent-id -// @Security CoderSessionToken -// @Produce json -// @Tags Enterprise -// @Param workspace_id query string true "Workspace ID" -// @Param agent_id query string true "Agent ID" -// @Success 200 {object} codersdk.JFrogXrayScan -// @Router /integrations/jfrog/xray-scan [get] -func (api *API) jFrogXrayScan(rw http.ResponseWriter, r *http.Request) { - var ( - ctx = r.Context() - vals = r.URL.Query() - p = httpapi.NewQueryParamParser() - wsID = p.RequiredNotEmpty("workspace_id").UUID(vals, uuid.UUID{}, "workspace_id") - agentID = p.RequiredNotEmpty("agent_id").UUID(vals, uuid.UUID{}, "agent_id") - ) - - if len(p.Errors) > 0 { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Invalid query params.", - Validations: p.Errors, - }) - return - } - - scan, err := api.Database.GetJFrogXrayScanByWorkspaceAndAgentID(ctx, database.GetJFrogXrayScanByWorkspaceAndAgentIDParams{ - WorkspaceID: wsID, - AgentID: agentID, - }) - if httpapi.Is404Error(err) { - httpapi.ResourceNotFound(rw) - return - } - if err != nil { - httpapi.InternalServerError(rw, err) - return - } - - httpapi.Write(ctx, rw, http.StatusOK, codersdk.JFrogXrayScan{ - WorkspaceID: scan.WorkspaceID, - AgentID: scan.AgentID, - Critical: int(scan.Critical), - High: int(scan.High), - Medium: int(scan.Medium), - ResultsURL: scan.ResultsUrl, - }) -} - -func (api *API) jfrogEnabledMW(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - // This doesn't actually use the external auth feature but we want - // to lock this behind an enterprise license and it's somewhat - // related to external auth (in that it is JFrog integration). - if !api.Entitlements.Enabled(codersdk.FeatureMultipleExternalAuth) { - httpapi.RouteNotFound(rw) - return - } - - next.ServeHTTP(rw, r) - }) -} diff --git a/enterprise/coderd/jfrog_test.go b/enterprise/coderd/jfrog_test.go deleted file mode 100644 index a9841a6d92067..0000000000000 --- a/enterprise/coderd/jfrog_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package coderd_test - -import ( - "net/http" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/database/dbfake" - "github.com/coder/coder/v2/coderd/rbac" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" - "github.com/coder/coder/v2/enterprise/coderd/license" - "github.com/coder/coder/v2/testutil" -) - -func TestJFrogXrayScan(t *testing.T) { - t.Parallel() - - t.Run("Post/Get", func(t *testing.T) { - t.Parallel() - ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureMultipleExternalAuth: 1}, - }, - }) - - tac, ta := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) - - wsResp := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ - OrganizationID: owner.OrganizationID, - OwnerID: ta.ID, - }).WithAgent().Do() - - ws := coderdtest.MustWorkspace(t, tac, wsResp.Workspace.ID) - require.Len(t, ws.LatestBuild.Resources, 1) - require.Len(t, ws.LatestBuild.Resources[0].Agents, 1) - - agentID := ws.LatestBuild.Resources[0].Agents[0].ID - expectedPayload := codersdk.JFrogXrayScan{ - WorkspaceID: ws.ID, - AgentID: agentID, - Critical: 19, - High: 5, - Medium: 3, - ResultsURL: "https://hello-world", - } - - ctx := testutil.Context(t, testutil.WaitMedium) - err := tac.PostJFrogXrayScan(ctx, expectedPayload) - require.NoError(t, err) - - resp1, err := tac.JFrogXRayScan(ctx, ws.ID, agentID) - require.NoError(t, err) - require.Equal(t, expectedPayload, resp1) - - // Can update again without error. - expectedPayload = codersdk.JFrogXrayScan{ - WorkspaceID: ws.ID, - AgentID: agentID, - Critical: 20, - High: 22, - Medium: 8, - ResultsURL: "https://goodbye-world", - } - err = tac.PostJFrogXrayScan(ctx, expectedPayload) - require.NoError(t, err) - - resp2, err := tac.JFrogXRayScan(ctx, ws.ID, agentID) - require.NoError(t, err) - require.NotEqual(t, expectedPayload, resp1) - require.Equal(t, expectedPayload, resp2) - }) - - t.Run("MemberPostUnauthorized", func(t *testing.T) { - t.Parallel() - - ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{codersdk.FeatureMultipleExternalAuth: 1}, - }, - }) - - memberClient, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) - - wsResp := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{ - OrganizationID: owner.OrganizationID, - OwnerID: member.ID, - }).WithAgent().Do() - - ws := coderdtest.MustWorkspace(t, memberClient, wsResp.Workspace.ID) - require.Len(t, ws.LatestBuild.Resources, 1) - require.Len(t, ws.LatestBuild.Resources[0].Agents, 1) - - agentID := ws.LatestBuild.Resources[0].Agents[0].ID - expectedPayload := codersdk.JFrogXrayScan{ - WorkspaceID: ws.ID, - AgentID: agentID, - Critical: 19, - High: 5, - Medium: 3, - ResultsURL: "https://hello-world", - } - - ctx := testutil.Context(t, testutil.WaitMedium) - err := memberClient.PostJFrogXrayScan(ctx, expectedPayload) - require.Error(t, err) - cerr, ok := codersdk.AsError(err) - require.True(t, ok) - require.Equal(t, http.StatusNotFound, cerr.StatusCode()) - - err = ownerClient.PostJFrogXrayScan(ctx, expectedPayload) - require.NoError(t, err) - - // We should still be able to fetch. - resp1, err := memberClient.JFrogXRayScan(ctx, ws.ID, agentID) - require.NoError(t, err) - require.Equal(t, expectedPayload, resp1) - }) -} diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 81d7368741803..70d54e5ea0fee 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -381,11 +381,6 @@ export type InsightsTemplateParams = InsightsParams & { interval: "day" | "week"; }; -export type GetJFrogXRayScanParams = { - workspaceId: string; - agentId: string; -}; - export class MissingBuildParameters extends Error { parameters: TypesGen.TemplateVersionParameter[] = []; versionId: string; @@ -2277,29 +2272,6 @@ class ApiMethods { await this.axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); }; - getJFrogXRayScan = async (options: GetJFrogXRayScanParams) => { - const searchParams = new URLSearchParams({ - workspace_id: options.workspaceId, - agent_id: options.agentId, - }); - - try { - const res = await this.axios.get( - `/api/v2/integrations/jfrog/xray-scan?${searchParams}`, - ); - - return res.data; - } catch (error) { - if (isAxiosError(error) && error.response?.status === 404) { - // react-query library does not allow undefined to be returned as a - // query result - return null; - } - - throw error; - } - }; - postWorkspaceUsage = async ( workspaceID: string, options: PostWorkspaceUsageRequest, diff --git a/site/src/api/queries/integrations.ts b/site/src/api/queries/integrations.ts deleted file mode 100644 index 38b212da0e6c1..0000000000000 --- a/site/src/api/queries/integrations.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { GetJFrogXRayScanParams } from "api/api"; -import { API } from "api/api"; - -export const xrayScan = (params: GetJFrogXRayScanParams) => { - return { - queryKey: ["xray", params], - queryFn: () => API.getJFrogXRayScan(params), - }; -}; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0bca431b7a574..7a443c750de91 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1171,16 +1171,6 @@ export interface IssueReconnectingPTYSignedTokenResponse { readonly signed_token: string; } -// From codersdk/jfrog.go -export interface JFrogXrayScan { - readonly workspace_id: string; - readonly agent_id: string; - readonly critical: number; - readonly high: number; - readonly medium: number; - readonly results_url: string; -} - // From codersdk/provisionerdaemons.go export type JobErrorCode = "REQUIRED_TEMPLATE_VARIABLES"; diff --git a/site/src/modules/resources/AgentRow.stories.tsx b/site/src/modules/resources/AgentRow.stories.tsx index cdcd350d49139..0e80ee0a5ecd0 100644 --- a/site/src/modules/resources/AgentRow.stories.tsx +++ b/site/src/modules/resources/AgentRow.stories.tsx @@ -299,27 +299,6 @@ export const Deprecated: Story = { }, }; -export const WithXRayScan: Story = { - parameters: { - queries: [ - { - key: [ - "xray", - { agentId: M.MockWorkspaceAgent.id, workspaceId: M.MockWorkspace.id }, - ], - data: { - workspace_id: M.MockWorkspace.id, - agent_id: M.MockWorkspaceAgent.id, - critical: 10, - high: 3, - medium: 5, - results_url: "http://localhost:8080", - }, - }, - ], - }, -}; - export const HideApp: Story = { args: { agent: { diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index c7de9d948ac41..4d14d2f0a9a39 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -4,7 +4,6 @@ import Collapse from "@mui/material/Collapse"; import Divider from "@mui/material/Divider"; import Skeleton from "@mui/material/Skeleton"; import { API } from "api/api"; -import { xrayScan } from "api/queries/integrations"; import type { Template, Workspace, @@ -41,7 +40,6 @@ import { PortForwardButton } from "./PortForwardButton"; import { AgentSSHButton } from "./SSHButton/SSHButton"; import { TerminalLink } from "./TerminalLink/TerminalLink"; import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton"; -import { XRayScanAlert } from "./XRayScanAlert"; export interface AgentRowProps { agent: WorkspaceAgent; @@ -72,11 +70,6 @@ export const AgentRow: FC = ({ storybookAgentMetadata, sshPrefix, }) => { - // XRay integration - const xrayScanQuery = useQuery( - xrayScan({ workspaceId: workspace.id, agentId: agent.id }), - ); - // Apps visibility const visibleApps = agent.apps.filter((app) => !app.hidden); const hasAppsToDisplay = !hideVSCodeDesktopButton || visibleApps.length > 0; @@ -227,8 +220,6 @@ export const AgentRow: FC = ({ )} - {xrayScanQuery.data && } -
    {agent.status === "connected" && (
    diff --git a/site/src/modules/resources/XRayScanAlert.tsx b/site/src/modules/resources/XRayScanAlert.tsx deleted file mode 100644 index f9761639d1993..0000000000000 --- a/site/src/modules/resources/XRayScanAlert.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type { Interpolation, Theme } from "@emotion/react"; -import type { JFrogXrayScan } from "api/typesGenerated"; -import { Button } from "components/Button/Button"; -import { ExternalImage } from "components/ExternalImage/ExternalImage"; -import type { FC } from "react"; - -interface XRayScanAlertProps { - scan: JFrogXrayScan; -} - -export const XRayScanAlert: FC = ({ scan }) => { - const display = scan.critical > 0 || scan.high > 0 || scan.medium > 0; - return display ? ( -
    - -
    - - JFrog Xray detected new vulnerabilities for this agent - - -
      - {scan.critical > 0 && ( -
    • - {scan.critical} critical -
    • - )} - {scan.high > 0 && ( -
    • {scan.high} high
    • - )} - {scan.medium > 0 && ( -
    • - {scan.medium} medium -
    • - )} -
    -
    - -
    - ) : ( - <> - ); -}; - -const styles = { - root: (theme) => ({ - backgroundColor: theme.palette.background.paper, - border: `1px solid ${theme.palette.divider}`, - borderLeft: 0, - borderRight: 0, - fontSize: 14, - padding: "24px 16px 24px 32px", - lineHeight: "1.5", - display: "flex", - alignItems: "center", - gap: 24, - }), - title: { - display: "block", - fontWeight: 500, - }, - issues: { - listStyle: "none", - margin: 0, - padding: 0, - fontSize: 13, - display: "flex", - alignItems: "center", - gap: 16, - marginTop: 4, - }, - issueItem: { - display: "flex", - alignItems: "center", - gap: 8, - - "&:before": { - content: '""', - display: "block", - width: 6, - height: 6, - borderRadius: "50%", - backgroundColor: "currentColor", - }, - }, - critical: (theme) => ({ - color: theme.roles.error.fill.solid, - }), - high: (theme) => ({ - color: theme.roles.warning.fill.solid, - }), - medium: (theme) => ({ - color: theme.roles.notice.fill.solid, - }), - link: { - marginLeft: "auto", - alignSelf: "flex-start", - }, -} satisfies Record>; diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 79bc116891bf9..51906fae2ad0d 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -374,8 +374,4 @@ export const handlers = [ http.get("/api/v2/workspaceagents/:agent/listening-ports", () => { return HttpResponse.json(M.MockListeningPortsResponse); }), - - http.get("/api/v2/integrations/jfrog/xray-scan", () => { - return new HttpResponse(null, { status: 404 }); - }), ]; From 39b9d23d9626532735cd5f6bf4087a0bf2188af9 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 11 Apr 2025 15:49:18 -0500 Subject: [PATCH 073/384] chore: remove nullable list elements in ts typegen (#17369) Backend will not send partially null slices. --- scripts/apitypings/main.go | 2 ++ site/src/api/typesGenerated.ts | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 3fd25948162dd..d12d33808e59b 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -79,6 +79,8 @@ func TsMutations(ts *guts.Typescript) { // Omitempty + null is just '?' in golang json marshal // number?: number | null --> number?: number config.SimplifyOmitEmpty, + // TsType: (string | null)[] --> (string)[] + config.NullUnionSlices, ) } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7a443c750de91..3f3d8f92c27e5 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -569,7 +569,7 @@ export interface DERPRegionReport { readonly warnings: readonly HealthMessage[]; readonly error?: string; readonly region: TailDERPRegion | null; - readonly node_reports: readonly (DERPNodeReport | null)[]; + readonly node_reports: readonly DERPNodeReport[]; } // From codersdk/deployment.go @@ -1707,7 +1707,7 @@ export interface PresetParameter { export type PreviewDiagnosticSeverityString = string; // From types/diagnostics.go -export type PreviewDiagnostics = readonly (FriendlyDiagnostic | null)[]; +export type PreviewDiagnostics = readonly FriendlyDiagnostic[]; // From types/parameter.go export interface PreviewParameter extends PreviewParameterData { @@ -1728,8 +1728,8 @@ export interface PreviewParameterData { readonly mutable: boolean; readonly default_value: NullHCLString; readonly icon: string; - readonly options: readonly (PreviewParameterOption | null)[]; - readonly validations: readonly (PreviewParameterValidation | null)[]; + readonly options: readonly PreviewParameterOption[]; + readonly validations: readonly PreviewParameterValidation[]; readonly required: boolean; readonly order: number; readonly ephemeral: boolean; @@ -2463,7 +2463,7 @@ export interface TailDERPRegion { readonly RegionCode: string; readonly RegionName: string; readonly Avoid?: boolean; - readonly Nodes: readonly (TailDERPNode | null)[]; + readonly Nodes: readonly TailDERPNode[]; } // From codersdk/deployment.go From e5ce3824ca0714deb95ab22bdd0781c4fc767981 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Mon, 14 Apr 2025 09:47:46 +0400 Subject: [PATCH 074/384] feat: add IsCoderConnectRunning to workspacesdk (#17361) Adds `IsCoderConnectRunning()` to the workspacesdk. This will support the `coder` CLI being able to use CoderConnect when it's running. part of #16828 --- codersdk/workspacesdk/workspacesdk.go | 52 ++++++++++- .../workspacesdk_internal_test.go | 86 +++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 codersdk/workspacesdk/workspacesdk_internal_test.go diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index df851e5ac31e9..25188917dafc9 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -128,12 +128,19 @@ func init() { } } +type resolver interface { + LookupIP(ctx context.Context, network, host string) ([]net.IP, error) +} + type Client struct { client *codersdk.Client + + // overridden in tests + resolver resolver } func New(c *codersdk.Client) *Client { - return &Client{client: c} + return &Client{client: c, resolver: net.DefaultResolver} } // AgentConnectionInfo returns required information for establishing @@ -384,3 +391,46 @@ func (c *Client) AgentReconnectingPTY(ctx context.Context, opts WorkspaceAgentRe } return websocket.NetConn(context.Background(), conn, websocket.MessageBinary), nil } + +type CoderConnectQueryOptions struct { + HostnameSuffix string +} + +// IsCoderConnectRunning checks if Coder Connect (OS level tunnel to workspaces) is running on the system. If you +// already know the hostname suffix your deployment uses, you can pass it in the CoderConnectQueryOptions to avoid an +// API call to AgentConnectionInfoGeneric. +func (c *Client) IsCoderConnectRunning(ctx context.Context, o CoderConnectQueryOptions) (bool, error) { + suffix := o.HostnameSuffix + if suffix == "" { + info, err := c.AgentConnectionInfoGeneric(ctx) + if err != nil { + return false, xerrors.Errorf("get agent connection info: %w", err) + } + suffix = info.HostnameSuffix + } + domainName := fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, suffix) + var dnsError *net.DNSError + ips, err := c.resolver.LookupIP(ctx, "ip6", domainName) + if xerrors.As(err, &dnsError) { + if dnsError.IsNotFound { + return false, nil + } + } + if err != nil { + return false, xerrors.Errorf("lookup DNS %s: %w", domainName, err) + } + + // The returned IP addresses are probably from the Coder Connect DNS server, but there are sometimes weird captive + // internet setups where the DNS server is configured to return an address for any IP query. So, to avoid false + // positives, check if we can find an address from our service prefix. + for _, ip := range ips { + addr, ok := netip.AddrFromSlice(ip) + if !ok { + continue + } + if tailnet.CoderServicePrefix.AsNetip().Contains(addr) { + return true, nil + } + } + return false, nil +} diff --git a/codersdk/workspacesdk/workspacesdk_internal_test.go b/codersdk/workspacesdk/workspacesdk_internal_test.go new file mode 100644 index 0000000000000..1b98ebdc2e671 --- /dev/null +++ b/codersdk/workspacesdk/workspacesdk_internal_test.go @@ -0,0 +1,86 @@ +package workspacesdk + +import ( + "context" + "fmt" + "net" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" + + "tailscale.com/net/tsaddr" + + "github.com/coder/coder/v2/tailnet" +) + +func TestClient_IsCoderConnectRunning(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v2/workspaceagents/connection", r.URL.Path) + httpapi.Write(ctx, rw, http.StatusOK, AgentConnectionInfo{ + HostnameSuffix: "test", + }) + })) + defer srv.Close() + + apiURL, err := url.Parse(srv.URL) + require.NoError(t, err) + sdkClient := codersdk.New(apiURL) + client := New(sdkClient) + + // Right name, right IP + expectedName := fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "test") + client.resolver = &fakeResolver{t: t, hostMap: map[string][]net.IP{ + expectedName: {net.ParseIP(tsaddr.CoderServiceIPv6().String())}, + }} + + result, err := client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{}) + require.NoError(t, err) + require.True(t, result) + + // Wrong name + result, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{HostnameSuffix: "coder"}) + require.NoError(t, err) + require.False(t, result) + + // Not found + client.resolver = &fakeResolver{t: t, err: &net.DNSError{IsNotFound: true}} + result, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{}) + require.NoError(t, err) + require.False(t, result) + + // Some other error + client.resolver = &fakeResolver{t: t, err: xerrors.New("a bad thing happened")} + _, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{}) + require.Error(t, err) + + // Right name, wrong IP + client.resolver = &fakeResolver{t: t, hostMap: map[string][]net.IP{ + expectedName: {net.ParseIP("2001::34")}, + }} + result, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{}) + require.NoError(t, err) + require.False(t, result) +} + +type fakeResolver struct { + t testing.TB + hostMap map[string][]net.IP + err error +} + +func (f *fakeResolver) LookupIP(_ context.Context, network, host string) ([]net.IP, error) { + assert.Equal(f.t, "ip6", network) + return f.hostMap[host], f.err +} From e2ebc9d549664959fc2103d4e167c410726df66c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:53:27 +0000 Subject: [PATCH 075/384] chore: bump github.com/go-jose/go-jose/v4 from 4.0.5 to 4.1.0 (#17383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.5 to 4.1.0.
    Release notes

    Sourced from github.com/go-jose/go-jose/v4's releases.

    v4.1.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/go-jose/go-jose/compare/v4.0.5...v4.1.0

    Commits

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/go-jose/go-jose/v4&package-manager=go_modules&previous-version=4.0.5&new-version=4.1.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 dc4d94ec02408..12c22fc2d969c 100644 --- a/go.mod +++ b/go.mod @@ -124,7 +124,7 @@ require ( github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.15.0 - github.com/go-jose/go-jose/v4 v4.0.5 + github.com/go-jose/go-jose/v4 v4.1.0 github.com/go-logr/logr v1.4.2 github.com/go-playground/validator/v10 v10.26.0 github.com/gofrs/flock v0.12.0 diff --git a/go.sum b/go.sum index 65c8a706e52e3..4633a82ac9dea 100644 --- a/go.sum +++ b/go.sum @@ -1094,8 +1094,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= +github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= From 199c408dd9b5e8354e6bf2a5dde8d910e5115fd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:53:37 +0000 Subject: [PATCH 076/384] chore: bump the x group with 2 updates (#17379) Bumps the x group with 2 updates: [golang.org/x/net](https://github.com/golang/net) and [golang.org/x/tools](https://github.com/golang/tools). Updates `golang.org/x/net` from 0.38.0 to 0.39.0
    Commits

    Updates `golang.org/x/tools` from 0.31.0 to 0.32.0
    Commits
    • 456962e go.mod: update golang.org/x dependencies
    • 5916e3c internal/tokeninternal: AddExistingFiles: tweaks for proposal
    • 9a1fbbd internal/typesinternal: change Used to UsedIdent
    • e73cd5a gopls/internal/golang: implement dynamicFuncCallType with typeutil.ClassifyCall
    • 11a9b3f gopls/internal/server: fix event labels after the big rename
    • 3e7f74d go/types/typeutil: used doesn't need Info.Selections
    • b97074b internal/gofix: fix URLs
    • e850fe1 gopls/internal/golang: CodeAction: place gopls doc as the last action
    • b948add internal/gofix: move from gopls/internal/analysis/gofix
    • b437eff go/types/typeutil: implement Callee and StaticCallee with Used
    • Additional commits viewable in compare view

    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> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 12c22fc2d969c..a07dea00f2c1d 100644 --- a/go.mod +++ b/go.mod @@ -199,13 +199,13 @@ require ( golang.org/x/crypto v0.37.0 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 golang.org/x/mod v0.24.0 - golang.org/x/net v0.38.0 + golang.org/x/net v0.39.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/tools v0.31.0 + golang.org/x/tools v0.32.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da google.golang.org/api v0.228.0 google.golang.org/grpc v1.71.0 diff --git a/go.sum b/go.sum index 4633a82ac9dea..9cf6ea164f8a8 100644 --- a/go.sum +++ b/go.sum @@ -2117,8 +2117,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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +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/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= @@ -2390,8 +2390,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From b6ff6b160a83ac66fc3c7fa784c128f7ce3cf78c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:53:56 +0000 Subject: [PATCH 077/384] chore: bump github.com/charmbracelet/bubbles from 0.20.0 to 0.21.0 (#17381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/charmbracelet/bubbles](https://github.com/charmbracelet/bubbles) from 0.20.0 to 0.21.0.
    Release notes

    Sourced from github.com/charmbracelet/bubbles's releases.

    v0.21.0

    Viewport improvements

    Finally, viewport finally has horizontal scrolling ✨![^v1] To enable it, use SetHorizontalStep (default in v2 will be 6).

    You can also scroll manually with ScrollLeft and ScrollRight, and use SetXOffset to scroll to a specific position (or 0 to reset):

    vp := viewport.New()
    vp.SetHorizontalStep(10) // how many columns to scroll on each key press
    vp.ScrollRight(30)       // pan 30 columns to the right!
    vp.ScrollLeft(10)        // pan 10 columns to the left!
    vp.SetXOffset(0)         // back to the left edge
    

    To make the API more consistent, vertical scroll functions were also renamed, and the old ones were deprecated (and will be removed in v2):

    // Scroll n lines up/down:
    func (m Model) LineUp(int)     // deprecated
    func (m Model) ScrollUp(int)   // new!
    func (m Model) LineDown(int)   // deprecated
    func (m Model) ScrollDown(int) // new!
    

    // Scroll half page up/down: func (m Model) HalfViewUp() []string // deprecated func (m Model) HalfPageUp() []string // new! func (m Model) HalfViewDown() []string // deprecated func (m Model) HalfPageDown() []string // new!

    // Scroll a full page up/down: func (m Model) ViewUp(int) []string // deprecated func (m Model) PageUp(int) []string // new! func (m Model) ViewDown(int) []string // deprecated func (m Model) PageDown(int) []string // new!

    [!NOTE] In v2, these functions will not return lines []string anymore, as it is no longer needed due to HighPerformanceRendering being deprecated as well.

    Other improvements

    The list bubble got a couple of new functions: SetFilterText, SetFilterState, and GlobalIndex - which you can use to get the index of the item in the unfiltered, original item list.

    ... (truncated)

    Commits
    • 8b55efb fix(textarea): placeholder with chinese chars (#767)
    • bd2a5b0 fix: golangci-lint 2 fixes (#769)
    • cce8481 ci: sync golangci-lint config (#770)
    • ea344ab feat(viewport): horizontal scroll with mouse wheel (#761)
    • 39668ec fix(viewport): normalize method names (#763)
    • f2434c3 Revert "fix(viewport): normalize method names"
    • c7f889e fix(viewport): normalize method names
    • 9e5365e docs: add example for ValidateFunc (#705)
    • c814ac7 chore(deps): bump github.com/charmbracelet/lipgloss from 1.0.0 to 1.1.0 (#751)
    • 3befccc chore(deps): bump github.com/muesli/termenv from 0.15.2 to 0.16.0 (#740)
    • Additional commits viewable in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/charmbracelet/bubbles&package-manager=go_modules&previous-version=0.20.0&new-version=0.21.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 | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index a07dea00f2c1d..62375f199821b 100644 --- a/go.mod +++ b/go.mod @@ -89,8 +89,8 @@ require ( github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 github.com/cenkalti/backoff/v4 v4.3.0 github.com/cespare/xxhash/v2 v2.3.0 - github.com/charmbracelet/bubbles v0.20.0 - github.com/charmbracelet/bubbletea v1.1.0 + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.4 github.com/charmbracelet/glamour v0.9.1 github.com/charmbracelet/lipgloss v1.1.0 github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 diff --git a/go.sum b/go.sum index 9cf6ea164f8a8..ad2117c7a07b7 100644 --- a/go.sum +++ b/go.sum @@ -837,8 +837,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= -github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM= @@ -849,8 +849,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= From 06d707d86509df34b83e5c781b2d850b1deb7eb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:54:29 +0000 Subject: [PATCH 078/384] chore: bump github.com/prometheus/client_golang from 1.21.1 to 1.22.0 (#17382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.1 to 1.22.0.
    Release notes

    Sourced from github.com/prometheus/client_golang's releases.

    v1.22.0 - 2025-04-07

    :warning: This release contains potential breaking change if you use experimental zstd support introduce in #1496 :warning:

    Experimental support for zstd on scrape was added, controlled by the request Accept-Encoding header. It was enabled by default since version 1.20, but now you need to add a blank import to enable it. The decision to make it opt-in by default was originally made because the Go standard library was expected to have default zstd support added soon, golang/go#62513 however, the work took longer than anticipated and it will be postponed to upcoming major Go versions.

    e.g.:

    import (
    _
    "github.com/prometheus/client_golang/prometheus/promhttp/zstd"
    )
    
    • [FEATURE] prometheus: Add new CollectorFunc utility #1724
    • [CHANGE] Minimum required Go version is now 1.22 (we also test client_golang against latest go version - 1.24) #1738
    • [FEATURE] api: WithLookbackDelta and WithStats options have been added to API client. #1743
    • [CHANGE] :warning: promhttp: Isolate zstd support and klauspost/compress library use to promhttp/zstd package. #1765

    ... (truncated)

    Changelog

    Sourced from github.com/prometheus/client_golang's changelog.

    1.22.0 / 2025-04-07

    :warning: This release contains potential breaking change if you use experimental zstd support introduce in #1496 :warning:

    Experimental support for zstd on scrape was added, controlled by the request Accept-Encoding header. It was enabled by default since version 1.20, but now you need to add a blank import to enable it. The decision to make it opt-in by default was originally made because the Go standard library was expected to have default zstd support added soon, golang/go#62513 however, the work took longer than anticipated and it will be postponed to upcoming major Go versions.

    e.g.:

    import (
    _
    "github.com/prometheus/client_golang/prometheus/promhttp/zstd"
    )
    
    • [FEATURE] prometheus: Add new CollectorFunc utility #1724
    • [CHANGE] Minimum required Go version is now 1.22 (we also test client_golang against latest go version - 1.24) #1738
    • [FEATURE] api: WithLookbackDelta and WithStats options have been added to API client. #1743
    • [CHANGE] :warning: promhttp: Isolate zstd support and klauspost/compress library use to promhttp/zstd package. #1765
    Commits
    • d50be25 Cut 1.22.0 (#1793)
    • 1043db7 Cut 1.22.0-rc.0 (#1768)
    • e575c9c promhttp: Isolate zstd support and klauspost/compress library use to promhttp...
    • f2276aa Merge pull request #1764 from prometheus/dependabot/github_actions/github-act...
    • 9df772c build(deps): bump peter-evans/create-pull-request
    • a3548c5 Merge pull request #1754 from saswatamcode/exp-eh
    • 60fd2b0 Remove go.work file for now
    • 8f9d0de exp: Add dependabot config
    • c5cf981 Merge pull request #1762 from prometheus/release-1.21
    • e84c305 exp: Reset snappy buf (#1756)
    • Additional commits viewable in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/prometheus/client_golang&package-manager=go_modules&previous-version=1.21.1&new-version=1.22.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 62375f199821b..b0e768190b2ef 100644 --- a/go.mod +++ b/go.mod @@ -166,7 +166,7 @@ require ( github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/pkg/sftp v1.13.7 github.com/prometheus-community/pro-bing v0.6.0 - github.com/prometheus/client_golang v1.21.1 + github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.63.0 github.com/quasilyte/go-ruleguard/dsl v0.3.22 diff --git a/go.sum b/go.sum index ad2117c7a07b7..1ce07f7f4bc71 100644 --- a/go.sum +++ b/go.sum @@ -1669,8 +1669,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus-community/pro-bing v0.6.0 h1:04SZ/092gONTE1XUFzYFWqgB4mKwcdkqNChLMFedwhg= github.com/prometheus-community/pro-bing v0.6.0/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ= -github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= -github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= From f75d01fd58e560af1451421ea1153bf2b397f443 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:54:47 +0000 Subject: [PATCH 079/384] chore: bump github.com/gohugoio/hugo from 0.143.0 to 0.146.3 (#17384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from 0.143.0 to 0.146.3.
    Release notes

    Sourced from github.com/gohugoio/hugo's releases.

    v0.146.3

    What's Changed

    • tpl: Make any layout set in front matter higher priority 30b9c19c7 @​bep #13588
    • tpl: Fix it so embedded render-codeblock-goat is used even if custom render-codeblock exists c8710625b @​bep #13595

    v0.146.2

    What's Changed

    • tpl: Fix codeblock hook resolve issue d1c394442 @​bep #13593
    • tpl: Fix legacy section mappings 1074e0115 @​bep #13584
    • tpl: Resolve layouts/all.html for all html output formats c19f1f236 @​bep #13587
    • tpl: Fix some baseof lookup issues 9221cbca4 @​bep #13583

    v0.146.1

    This fixes a regression introduced in v0.146.0 released earlier today.

    • tpl: Skip dot and temp files inside /layouts 3b9f2a7de @​bep #13579

    v0.146.0

    [!NOTE] There's a v0.146.1 bug fix release that fixes a regression introduced in this release.

    The big new thing in this release is a fully refreshed template system – simpler and much better. We're working on the updated documentation for this, but see this issue for some more information. We have gone to great lengths to make this as backwards compatible as possible, but make sure you test your site before going live with this new version. This version also comes with a full dependency refresh and some useful new template funcs:

    • templates.Current: Info about the current executing template and its call stack. Very useful for debugging.
    • time.In: Returns the given date/time as represented in the specified IANA time zone.

    Bug fixes

    • tpl/tplimpl: Fix full screen option in vimeo and youtube shortcodes 6f14dbe24 @​jmooring #13531

    Improvements

    ... (truncated)

    Commits
    • 05ef8b7 releaser: Bump versions for release of 0.146.3
    • 30b9c19 tpl: Make any layout set in front matter higher priority
    • c871062 tpl: Fix it so embedded render-codeblock-goat is used even if custom render-c...
    • 53221f8 releaser: Prepare repository for 0.147.0-DEV
    • ff3ab19 releaser: Bump versions for release of 0.146.2
    • d1c3944 tpl: Fix codeblock hook resolve issue
    • 1074e01 tpl: Fix legacy section mappings
    • c19f1f2 tpl: Resolve layouts/all.html for all html output formats
    • 9221cbc tpl: Fix some baseof lookup issues
    • e3e3f9a releaser: Prepare repository for 0.147.0-DEV
    • Additional commits viewable in compare view

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/gohugoio/hugo&package-manager=go_modules&previous-version=0.143.0&new-version=0.146.3)](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 | 12 +++++------ go.sum | 68 +++++++++++++++++++++++++++++++--------------------------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index b0e768190b2ef..c563050a6dba9 100644 --- a/go.mod +++ b/go.mod @@ -128,7 +128,7 @@ require ( github.com/go-logr/logr v1.4.2 github.com/go-playground/validator/v10 v10.26.0 github.com/gofrs/flock v0.12.0 - github.com/gohugoio/hugo v0.143.0 + github.com/gohugoio/hugo v0.146.3 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/golang-migrate/migrate/v4 v4.18.1 github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 @@ -249,7 +249,7 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/akutz/memconn v0.1.0 // indirect - github.com/alecthomas/chroma/v2 v2.15.0 // indirect + github.com/alecthomas/chroma/v2 v2.16.0 // indirect github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/apparentlymart/go-cidr v1.1.0 // indirect @@ -273,7 +273,7 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bep/godartsass/v2 v2.3.2 // indirect + github.com/bep/godartsass/v2 v2.5.0 // indirect github.com/bep/golibsass v1.2.0 // indirect github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect @@ -284,7 +284,7 @@ require ( github.com/cloudflare/circl v1.6.0 // indirect github.com/containerd/continuity v0.4.5 // indirect github.com/coreos/go-iptables v0.6.0 // indirect - github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect github.com/docker/cli v28.0.4+incompatible // indirect github.com/docker/docker v28.0.4+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect @@ -318,7 +318,7 @@ require ( github.com/gobwas/ws v1.4.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/gohugoio/hashstructure v0.3.0 // indirect + github.com/gohugoio/hashstructure v0.5.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect @@ -392,7 +392,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect 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.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/pierrec/lz4/v4 v4.1.18 // indirect github.com/pion/transport/v2 v2.2.10 // indirect diff --git a/go.sum b/go.sum index 1ce07f7f4bc71..69053b6525f4b 100644 --- a/go.sum +++ b/go.sum @@ -794,20 +794,22 @@ github.com/bep/gitmap v1.6.0 h1:sDuQMm9HoTL0LtlrfxjbjgAg2wHQd4nkMup2FInYzhA= github.com/bep/gitmap v1.6.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw= github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA= github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c= -github.com/bep/godartsass/v2 v2.3.2 h1:meuc76J1C1soSCAnlnJRdGqJ5S4m6/GW+8hmOe9tOog= -github.com/bep/godartsass/v2 v2.3.2/go.mod h1:Qe5WOS9nVJy7G0jHssXPd3c+Pqk/f7+Tm6k/vahbVgs= +github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg= +github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU= github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI= github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= +github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5kujw= +github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg= github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY= github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI= -github.com/bep/imagemeta v0.8.3 h1:68XqpYXjWW9mFjdGurutDmAKBJa9y2aknEBHwY/+3zw= -github.com/bep/imagemeta v0.8.3/go.mod h1:5piPAq5Qomh07m/dPPCLN3mDJyFusvUG7VwdRD/vX0s= -github.com/bep/lazycache v0.7.0 h1:VM257SkkjcR9z55eslXTkUIX8QMNKoqQRNKV/4xIkCY= -github.com/bep/lazycache v0.7.0/go.mod h1:NmRm7Dexh3pmR1EignYR8PjO2cWybFQ68+QgY3VMCSc= +github.com/bep/imagemeta v0.11.0 h1:jL92HhL1H70NC+f8OVVn5D/nC3FmdxTnM3R+csj54mE= +github.com/bep/imagemeta v0.11.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8= +github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8= +github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk= github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= -github.com/bep/overlayfs v0.9.2 h1:qJEmFInsW12L7WW7dOTUhnMfyk/fN9OCDEO5Gr8HSDs= -github.com/bep/overlayfs v0.9.2/go.mod h1:aYY9W7aXQsGcA7V9x/pzeR8LjEgIxbtisZm8Q7zPz40= +github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE= +github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro= github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= @@ -973,8 +975,8 @@ github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvd github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= -github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/cli v28.0.4+incompatible h1:pBJSJeNd9QeIWPjRcV91RVJihd/TXB77q1ef64XEu4A= github.com/docker/cli v28.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.0.4+incompatible h1:JNNkBctYKurkw6FrHfKqY0nKIDf5nrbxjVBtS+cdcok= @@ -1028,8 +1030,8 @@ github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfU github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/evanw/esbuild v0.24.2 h1:PQExybVBrjHjN6/JJiShRGIXh1hWVm6NepVnhZhrt0A= -github.com/evanw/esbuild v0.24.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/evanw/esbuild v0.25.2 h1:ublSEmZSjzOc6jLO1OTQy/vHc1wiqyDF4oB3hz5sM6s= +github.com/evanw/esbuild v0.25.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -1052,8 +1054,8 @@ github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= @@ -1062,8 +1064,8 @@ github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3G github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a h1:fwNLHrP5Rbg/mGSXCjtPdpbqv2GucVTA/KMi8wEm6mE= github.com/gen2brain/beeep v0.0.0-20220402123239-6a3042f4b71a/go.mod h1:/WeFVhhxMOGypVKS0w8DUJxUBbHypnWkUVnW7p5c9Pw= -github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= -github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= +github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE= +github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= @@ -1160,16 +1162,16 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY= github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ= -github.com/gohugoio/hashstructure v0.3.0 h1:orHavfqnBv0ffQmobOp41Y9HKEMcjrR/8EFAzpngmGs= -github.com/gohugoio/hashstructure v0.3.0/go.mod h1:8ohPTAfQLTs2WdzB6k9etmQYclDUeNsIHGPAFejbsEA= +github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg= +github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec= github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs= github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI= -github.com/gohugoio/hugo v0.143.0 h1:acmpu/j47LHQcVQJ1YIIGKe+dH7cGmxarMq/aeGY3AM= -github.com/gohugoio/hugo v0.143.0/go.mod h1:G0uwM5aRUXN4cbnqrDQx9Dlgmf/ukUpPADajL8FbL9M= -github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0 h1:MNdY6hYCTQEekY0oAfsxWZU1CDt6iH+tMLgyMJQh/sg= -github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0/go.mod h1:oBdBVuiZ0fv9xd8xflUgt53QxW5jOCb1S+xntcN4SKo= -github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0 h1:7PY5PIJ2mck7v6R52yCFvvYHvsPMEbulgRviw3I9lP4= -github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0/go.mod h1:r8g5S7bHfdj0+9ShBog864ufCsVODKQZNjYYY8OnJpM= +github.com/gohugoio/hugo v0.146.3 h1:agRqbPbAdTF8+Tj10MRLJSs+iX0AnOrf2OtOWAAI+nw= +github.com/gohugoio/hugo v0.146.3/go.mod h1:WsWhL6F5z0/ER9LgREuNp96eovssVKVCEDHgkibceuU= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.3.0 h1:gj49kTR5Z4Hnm0ZaQrgPVazL3DUkppw+x6XhHCmh+Wk= +github.com/gohugoio/hugo-goldmark-extensions/extras v0.3.0/go.mod h1:IMMj7xiUbLt1YNJ6m7AM4cnsX4cFnnfkleO/lBHGzUg= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1 h1:nUzXfRTszLliZuN0JTKeunXTRaiFX6ksaWP0puLLYAY= +github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1/go.mod h1:Wy8ThAA8p2/w1DY05vEzq6EIeI2mzDjvHsu7ULBVwog= github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc= github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= @@ -1408,8 +1410,6 @@ github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwso github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= -github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= -github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= @@ -1603,6 +1603,10 @@ github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMim github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= 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= @@ -1627,8 +1631,8 @@ github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOv github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +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= @@ -1701,8 +1705,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -2019,8 +2023,8 @@ golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeap golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g= -golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4= +golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= +golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= From 1c040edec4545e9be4e3d07c58c85b6660981f99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:02:26 +0000 Subject: [PATCH 080/384] chore: bump vite from 5.4.17 to 5.4.18 in /site (#17385) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.17 to 5.4.18.
    Release notes

    Sourced from vite's releases.

    v5.4.18

    Please refer to CHANGELOG.md for details.

    Changelog

    Sourced from vite's changelog.

    5.4.18 (2025-04-10)

    Commits

    [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.4.17&new-version=5.4.18)](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> --- site/package.json | 2 +- site/pnpm-lock.yaml | 228 ++++++++++++++++++++++---------------------- 2 files changed, 115 insertions(+), 115 deletions(-) diff --git a/site/package.json b/site/package.json index 6f164005ab49e..7b5670c36cbee 100644 --- a/site/package.json +++ b/site/package.json @@ -192,7 +192,7 @@ "ts-proto": "1.164.0", "ts-prune": "0.10.3", "typescript": "5.6.3", - "vite": "5.4.17", + "vite": "5.4.18", "vite-plugin-checker": "0.8.0", "vite-plugin-turbosnap": "1.0.3" }, diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 92382a11b2ad7..913e292f7aba5 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -264,7 +264,7 @@ importers: version: 1.5.1 rollup-plugin-visualizer: specifier: 5.14.0 - version: 5.14.0(rollup@4.39.0) + version: 5.14.0(rollup@4.40.0) semver: specifier: 7.6.2 version: 7.6.2 @@ -334,7 +334,7 @@ importers: version: 8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3) '@storybook/react-vite': specifier: 8.4.6 - version: 8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.39.0)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3)(vite@5.4.17(@types/node@20.17.16)) + version: 8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.0)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3)(vite@5.4.18(@types/node@20.17.16)) '@storybook/test': specifier: 8.4.6 version: 8.4.6(storybook@8.5.3(prettier@3.4.1)) @@ -415,7 +415,7 @@ importers: version: 9.0.2 '@vitejs/plugin-react': specifier: 4.3.4 - version: 4.3.4(vite@5.4.17(@types/node@20.17.16)) + version: 4.3.4(vite@5.4.18(@types/node@20.17.16)) autoprefixer: specifier: 10.4.20 version: 10.4.20(postcss@8.5.1) @@ -486,11 +486,11 @@ importers: specifier: 5.6.3 version: 5.6.3 vite: - specifier: 5.4.17 - version: 5.4.17(@types/node@20.17.16) + specifier: 5.4.18 + version: 5.4.18(@types/node@20.17.16) vite-plugin-checker: specifier: 0.8.0 - version: 0.8.0(@biomejs/biome@1.9.4)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.17(@types/node@20.17.16)) + version: 0.8.0(@biomejs/biome@1.9.4)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.18(@types/node@20.17.16)) vite-plugin-turbosnap: specifier: 1.0.3 version: 1.0.3 @@ -1010,8 +1010,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.5.1': - resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==, tarball: https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz} + '@eslint-community/eslint-utils@4.6.0': + resolution: {integrity: sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==, tarball: https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -2030,103 +2030,103 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.39.0': - resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz} + '@rollup/rollup-android-arm-eabi@4.40.0': + resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.39.0': - resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.39.0.tgz} + '@rollup/rollup-android-arm64@4.40.0': + resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==, tarball: https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.39.0': - resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz} + '@rollup/rollup-darwin-arm64@4.40.0': + resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.39.0': - resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.39.0.tgz} + '@rollup/rollup-darwin-x64@4.40.0': + resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==, tarball: https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.39.0': - resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.39.0.tgz} + '@rollup/rollup-freebsd-arm64@4.40.0': + resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.39.0': - resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.39.0.tgz} + '@rollup/rollup-freebsd-x64@4.40.0': + resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==, tarball: https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.39.0': - resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.39.0.tgz} + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': + resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.39.0': - resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.39.0.tgz} + '@rollup/rollup-linux-arm-musleabihf@4.40.0': + resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.39.0': - resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.39.0.tgz} + '@rollup/rollup-linux-arm64-gnu@4.40.0': + resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.39.0': - resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.39.0.tgz} + '@rollup/rollup-linux-arm64-musl@4.40.0': + resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.39.0': - resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.39.0.tgz} + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': + resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': - resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.39.0.tgz} + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': + resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.39.0': - resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.39.0.tgz} + '@rollup/rollup-linux-riscv64-gnu@4.40.0': + resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.39.0': - resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.39.0.tgz} + '@rollup/rollup-linux-riscv64-musl@4.40.0': + resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.39.0': - resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.39.0.tgz} + '@rollup/rollup-linux-s390x-gnu@4.40.0': + resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.39.0': - resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz} + '@rollup/rollup-linux-x64-gnu@4.40.0': + resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.39.0': - resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.39.0.tgz} + '@rollup/rollup-linux-x64-musl@4.40.0': + resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==, tarball: https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.39.0': - resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.39.0.tgz} + '@rollup/rollup-win32-arm64-msvc@4.40.0': + resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.39.0': - resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.39.0.tgz} + '@rollup/rollup-win32-ia32-msvc@4.40.0': + resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.39.0': - resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz} + '@rollup/rollup-win32-x64-msvc@4.40.0': + resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==, tarball: https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz} cpu: [x64] os: [win32] @@ -5628,8 +5628,8 @@ packages: rollup: optional: true - rollup@4.39.0: - resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==, tarball: https://registry.npmjs.org/rollup/-/rollup-4.39.0.tgz} + rollup@4.40.0: + resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==, tarball: https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -6283,8 +6283,8 @@ packages: vite-plugin-turbosnap@1.0.3: resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==, tarball: https://registry.npmjs.org/vite-plugin-turbosnap/-/vite-plugin-turbosnap-1.0.3.tgz} - vite@5.4.17: - resolution: {integrity: sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==, tarball: https://registry.npmjs.org/vite/-/vite-5.4.17.tgz} + vite@5.4.18: + resolution: {integrity: sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==, tarball: https://registry.npmjs.org/vite/-/vite-5.4.18.tgz} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -6980,7 +6980,7 @@ snapshots: '@esbuild/win32-x64@0.25.2': optional: true - '@eslint-community/eslint-utils@4.5.1(eslint@8.52.0)': + '@eslint-community/eslint-utils@4.6.0(eslint@8.52.0)': dependencies: eslint: 8.52.0 eslint-visitor-keys: 3.4.3 @@ -7299,11 +7299,11 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.6.3)(vite@5.4.17(@types/node@20.17.16))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.6.3)(vite@5.4.18(@types/node@20.17.16))': dependencies: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.6.3) - vite: 5.4.17(@types/node@20.17.16) + vite: 5.4.18(@types/node@20.17.16) optionalDependencies: typescript: 5.6.3 @@ -8122,72 +8122,72 @@ snapshots: '@remix-run/router@1.19.2': {} - '@rollup/pluginutils@5.0.5(rollup@4.39.0)': + '@rollup/pluginutils@5.0.5(rollup@4.40.0)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 4.39.0 + rollup: 4.40.0 - '@rollup/rollup-android-arm-eabi@4.39.0': + '@rollup/rollup-android-arm-eabi@4.40.0': optional: true - '@rollup/rollup-android-arm64@4.39.0': + '@rollup/rollup-android-arm64@4.40.0': optional: true - '@rollup/rollup-darwin-arm64@4.39.0': + '@rollup/rollup-darwin-arm64@4.40.0': optional: true - '@rollup/rollup-darwin-x64@4.39.0': + '@rollup/rollup-darwin-x64@4.40.0': optional: true - '@rollup/rollup-freebsd-arm64@4.39.0': + '@rollup/rollup-freebsd-arm64@4.40.0': optional: true - '@rollup/rollup-freebsd-x64@4.39.0': + '@rollup/rollup-freebsd-x64@4.40.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.39.0': + '@rollup/rollup-linux-arm-gnueabihf@4.40.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.39.0': + '@rollup/rollup-linux-arm-musleabihf@4.40.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.39.0': + '@rollup/rollup-linux-arm64-gnu@4.40.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.39.0': + '@rollup/rollup-linux-arm64-musl@4.40.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.39.0': + '@rollup/rollup-linux-loongarch64-gnu@4.40.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': + '@rollup/rollup-linux-powerpc64le-gnu@4.40.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.39.0': + '@rollup/rollup-linux-riscv64-gnu@4.40.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.39.0': + '@rollup/rollup-linux-riscv64-musl@4.40.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.39.0': + '@rollup/rollup-linux-s390x-gnu@4.40.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.39.0': + '@rollup/rollup-linux-x64-gnu@4.40.0': optional: true - '@rollup/rollup-linux-x64-musl@4.39.0': + '@rollup/rollup-linux-x64-musl@4.40.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.39.0': + '@rollup/rollup-win32-arm64-msvc@4.40.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.39.0': + '@rollup/rollup-win32-ia32-msvc@4.40.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.39.0': + '@rollup/rollup-win32-x64-msvc@4.40.0': optional: true '@sinclair/typebox@0.27.8': {} @@ -8328,13 +8328,13 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.4.6(storybook@8.5.3(prettier@3.4.1))(vite@5.4.17(@types/node@20.17.16))': + '@storybook/builder-vite@8.4.6(storybook@8.5.3(prettier@3.4.1))(vite@5.4.18(@types/node@20.17.16))': dependencies: '@storybook/csf-plugin': 8.4.6(storybook@8.5.3(prettier@3.4.1)) browser-assert: 1.2.1 storybook: 8.5.3(prettier@3.4.1) ts-dedent: 2.2.0 - vite: 5.4.17(@types/node@20.17.16) + vite: 5.4.18(@types/node@20.17.16) '@storybook/channels@8.1.11': dependencies: @@ -8431,11 +8431,11 @@ snapshots: react-dom: 18.3.1(react@18.3.1) storybook: 8.5.3(prettier@3.4.1) - '@storybook/react-vite@8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.39.0)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3)(vite@5.4.17(@types/node@20.17.16))': + '@storybook/react-vite@8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.40.0)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3)(vite@5.4.18(@types/node@20.17.16))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.6.3)(vite@5.4.17(@types/node@20.17.16)) - '@rollup/pluginutils': 5.0.5(rollup@4.39.0) - '@storybook/builder-vite': 8.4.6(storybook@8.5.3(prettier@3.4.1))(vite@5.4.17(@types/node@20.17.16)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.6.3)(vite@5.4.18(@types/node@20.17.16)) + '@rollup/pluginutils': 5.0.5(rollup@4.40.0) + '@storybook/builder-vite': 8.4.6(storybook@8.5.3(prettier@3.4.1))(vite@5.4.18(@types/node@20.17.16)) '@storybook/react': 8.4.6(@storybook/test@8.4.6(storybook@8.5.3(prettier@3.4.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.5.3(prettier@3.4.1))(typescript@5.6.3) find-up: 5.0.0 magic-string: 0.30.5 @@ -8445,7 +8445,7 @@ snapshots: resolve: 1.22.8 storybook: 8.5.3(prettier@3.4.1) tsconfig-paths: 4.2.0 - vite: 5.4.17(@types/node@20.17.16) + vite: 5.4.18(@types/node@20.17.16) transitivePeerDependencies: - '@storybook/test' - rollup @@ -8946,14 +8946,14 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@4.3.4(vite@5.4.17(@types/node@20.17.16))': + '@vitejs/plugin-react@4.3.4(vite@5.4.18(@types/node@20.17.16))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.17(@types/node@20.17.16) + vite: 5.4.18(@types/node@20.17.16) transitivePeerDependencies: - supports-color @@ -9869,7 +9869,7 @@ snapshots: eslint@8.52.0: dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@8.52.0) + '@eslint-community/eslint-utils': 4.6.0(eslint@8.52.0) '@eslint-community/regexpp': 4.12.1 '@eslint/eslintrc': 2.1.4 '@eslint/js': 8.52.0 @@ -12448,39 +12448,39 @@ snapshots: glob: 7.2.3 optional: true - rollup-plugin-visualizer@5.14.0(rollup@4.39.0): + rollup-plugin-visualizer@5.14.0(rollup@4.40.0): dependencies: open: 8.4.2 picomatch: 4.0.2 source-map: 0.7.4 yargs: 17.7.2 optionalDependencies: - rollup: 4.39.0 + rollup: 4.40.0 - rollup@4.39.0: + rollup@4.40.0: dependencies: '@types/estree': 1.0.7 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.39.0 - '@rollup/rollup-android-arm64': 4.39.0 - '@rollup/rollup-darwin-arm64': 4.39.0 - '@rollup/rollup-darwin-x64': 4.39.0 - '@rollup/rollup-freebsd-arm64': 4.39.0 - '@rollup/rollup-freebsd-x64': 4.39.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.39.0 - '@rollup/rollup-linux-arm-musleabihf': 4.39.0 - '@rollup/rollup-linux-arm64-gnu': 4.39.0 - '@rollup/rollup-linux-arm64-musl': 4.39.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.39.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0 - '@rollup/rollup-linux-riscv64-gnu': 4.39.0 - '@rollup/rollup-linux-riscv64-musl': 4.39.0 - '@rollup/rollup-linux-s390x-gnu': 4.39.0 - '@rollup/rollup-linux-x64-gnu': 4.39.0 - '@rollup/rollup-linux-x64-musl': 4.39.0 - '@rollup/rollup-win32-arm64-msvc': 4.39.0 - '@rollup/rollup-win32-ia32-msvc': 4.39.0 - '@rollup/rollup-win32-x64-msvc': 4.39.0 + '@rollup/rollup-android-arm-eabi': 4.40.0 + '@rollup/rollup-android-arm64': 4.40.0 + '@rollup/rollup-darwin-arm64': 4.40.0 + '@rollup/rollup-darwin-x64': 4.40.0 + '@rollup/rollup-freebsd-arm64': 4.40.0 + '@rollup/rollup-freebsd-x64': 4.40.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.40.0 + '@rollup/rollup-linux-arm-musleabihf': 4.40.0 + '@rollup/rollup-linux-arm64-gnu': 4.40.0 + '@rollup/rollup-linux-arm64-musl': 4.40.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.40.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-gnu': 4.40.0 + '@rollup/rollup-linux-riscv64-musl': 4.40.0 + '@rollup/rollup-linux-s390x-gnu': 4.40.0 + '@rollup/rollup-linux-x64-gnu': 4.40.0 + '@rollup/rollup-linux-x64-musl': 4.40.0 + '@rollup/rollup-win32-arm64-msvc': 4.40.0 + '@rollup/rollup-win32-ia32-msvc': 4.40.0 + '@rollup/rollup-win32-x64-msvc': 4.40.0 fsevents: 2.3.3 run-parallel@1.2.0: @@ -13168,7 +13168,7 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-checker@0.8.0(@biomejs/biome@1.9.4)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.17(@types/node@20.17.16)): + vite-plugin-checker@0.8.0(@biomejs/biome@1.9.4)(eslint@8.52.0)(optionator@0.9.3)(typescript@5.6.3)(vite@5.4.18(@types/node@20.17.16)): dependencies: '@babel/code-frame': 7.25.7 ansi-escapes: 4.3.2 @@ -13180,7 +13180,7 @@ snapshots: npm-run-path: 4.0.1 strip-ansi: 6.0.1 tiny-invariant: 1.3.3 - vite: 5.4.17(@types/node@20.17.16) + vite: 5.4.18(@types/node@20.17.16) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.12 @@ -13193,11 +13193,11 @@ snapshots: vite-plugin-turbosnap@1.0.3: {} - vite@5.4.17(@types/node@20.17.16): + vite@5.4.18(@types/node@20.17.16): dependencies: esbuild: 0.25.2 postcss: 8.5.1 - rollup: 4.39.0 + rollup: 4.40.0 optionalDependencies: '@types/node': 20.17.16 fsevents: 2.3.3 From 34752fa1485c79b5cef6ba420f22461b1e64a336 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Mon, 14 Apr 2025 08:03:25 -0400 Subject: [PATCH 081/384] docs: add note about sign in with GitHub button should be hidden when flow is disabled (#17367) Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/admin/users/github-auth.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/admin/users/github-auth.md b/docs/admin/users/github-auth.md index d895764c44f29..c556c87a2accb 100644 --- a/docs/admin/users/github-auth.md +++ b/docs/admin/users/github-auth.md @@ -41,6 +41,14 @@ own app or set: CODER_OAUTH2_GITHUB_DEFAULT_PROVIDER_ENABLE=false ``` +> [!NOTE] +> After you disable the default GitHub provider with the setting above, the +> **Sign in with GitHub** button might still appear on your login page even though +> the authentication flow is disabled. +> +> To completely hide the GitHub sign-in button, you must both disable the default +> provider and ensure you don't have a custom GitHub OAuth app configured. + ## Step 1: Configure the OAuth application in GitHub First, From d8fcb062bc898a61897a1562c0d9c2acbad89209 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Mon, 14 Apr 2025 16:15:06 +0400 Subject: [PATCH 082/384] chore: add logging for coderdtest server lifecycle (#17376) regarding https://github.com/coder/internal/issues/581 Adds logging around the lifecyle of the coderd HTTP server. --- coderd/coderdtest/coderdtest.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 0f0a99807a37d..dbf1f62abfb28 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -421,6 +421,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can handler.ServeHTTP(w, r) } })) + t.Logf("coderdtest server listening on %s", srv.Listener.Addr().String()) srv.Config.BaseContext = func(_ net.Listener) context.Context { return ctx } @@ -433,7 +434,12 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can } else { srv.Start() } - t.Cleanup(srv.Close) + t.Logf("coderdtest server started on %s", srv.URL) + t.Cleanup(func() { + t.Logf("closing coderdtest server on %s", srv.Listener.Addr().String()) + srv.Close() + t.Logf("closed coderdtest server on %s", srv.Listener.Addr().String()) + }) tcpAddr, ok := srv.Listener.Addr().(*net.TCPAddr) require.True(t, ok) From 73f5af82ad5ef440bcc1d8727aa9d4495e21d203 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Mon, 14 Apr 2025 16:20:50 +0400 Subject: [PATCH 083/384] test: fix TestAgent_Lifecycle/ShutdownScriptOnce to wait for stats (#17387) fixes: https://github.com/coder/internal/issues/576 TestAgent_Lifecycle/ShutdownScriptOnce hits error logs which cause test failures. These logs are legit errors and have to do with shutting down the agent before it has fully come up. This PR changes the test to wait for the agent to send stats (a good indicator that it's fully up, and beyond the errors that have triggered test failures in past) before closing it. --- agent/agent_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/agent/agent_test.go b/agent/agent_test.go index 69423a2f83be7..97790860ba70a 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -1650,8 +1650,10 @@ func TestAgent_Lifecycle(t *testing.T) { t.Run("ShutdownScriptOnce", func(t *testing.T) { t.Parallel() logger := testutil.Logger(t) + ctx := testutil.Context(t, testutil.WaitMedium) expected := "this-is-shutdown" derpMap, _ := tailnettest.RunDERPAndSTUN(t) + statsCh := make(chan *proto.Stats, 50) client := agenttest.NewClient(t, logger, @@ -1670,7 +1672,7 @@ func TestAgent_Lifecycle(t *testing.T) { RunOnStop: true, }}, }, - make(chan *proto.Stats, 50), + statsCh, tailnet.NewCoordinator(logger), ) defer client.Close() @@ -1695,6 +1697,11 @@ func TestAgent_Lifecycle(t *testing.T) { return len(content) > 0 // something is in the startup log file }, testutil.WaitShort, testutil.IntervalMedium) + // In order to avoid shutting down the agent before it is fully started and triggering + // errors, we'll wait until the agent is fully up. It's a bit hokey, but among the last things the agent starts + // is the stats reporting, so getting a stats report is a good indication the agent is fully up. + _ = testutil.RequireRecvCtx(ctx, t, statsCh) + err := agent.Close() require.NoError(t, err, "agent should be closed successfully") From a98605913ad89baa15eefaa4c54805998be76598 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Mon, 14 Apr 2025 15:34:50 +0200 Subject: [PATCH 084/384] feat: mark prebuilds as such and set their preset ids (#16965) This pull request closes https://github.com/coder/internal/issues/513 --- cli/testdata/coder_list_--output_json.golden | 3 +- coderd/apidoc/docs.go | 13 + coderd/apidoc/swagger.json | 13 + coderd/database/dbmem/dbmem.go | 27 +- .../provisionerdserver/provisionerdserver.go | 5 +- .../provisionerdserver_test.go | 515 +++++++++--------- coderd/workspacebuilds.go | 9 +- coderd/workspacebuilds_test.go | 44 ++ coderd/workspaces.go | 3 +- coderd/workspaces_test.go | 45 ++ coderd/wsbuilder/wsbuilder.go | 53 +- coderd/wsbuilder/wsbuilder_test.go | 62 +++ codersdk/organizations.go | 5 +- codersdk/workspacebuilds.go | 1 + codersdk/workspaces.go | 2 + docs/reference/api/builds.md | 7 + docs/reference/api/schemas.md | 44 +- docs/reference/api/workspaces.md | 8 + provisioner/terraform/provision.go | 4 + provisioner/terraform/provision_test.go | 38 ++ provisionerd/provisionerd.go | 2 + site/src/api/typesGenerated.ts | 3 + .../CreateWorkspacePageView.tsx | 4 + site/src/testHelpers/entities.ts | 4 + 24 files changed, 606 insertions(+), 308 deletions(-) diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index ac9bcc2153668..5f293787de719 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -67,7 +67,8 @@ "count": 0, "available": 0, "most_recently_seen": null - } + }, + "template_version_preset_id": null }, "latest_app_status": null, "outdated": false, diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index b9d54d989a723..6ad75b2d65a26 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11394,6 +11394,11 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "template_version_preset_id": { + "description": "TemplateVersionPresetID is the ID of the template version preset to use for the build.", + "type": "string", + "format": "uuid" + }, "transition": { "enum": [ "start", @@ -11458,6 +11463,10 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "template_version_preset_id": { + "type": "string", + "format": "uuid" + }, "ttl_ms": { "type": "integer" } @@ -17037,6 +17046,10 @@ const docTemplate = `{ "template_version_name": { "type": "string" }, + "template_version_preset_id": { + "type": "string", + "format": "uuid" + }, "transition": { "enum": [ "start", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index b5bb734260814..77758feb75c70 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10160,6 +10160,11 @@ "type": "string", "format": "uuid" }, + "template_version_preset_id": { + "description": "TemplateVersionPresetID is the ID of the template version preset to use for the build.", + "type": "string", + "format": "uuid" + }, "transition": { "enum": ["start", "stop", "delete"], "allOf": [ @@ -10216,6 +10221,10 @@ "type": "string", "format": "uuid" }, + "template_version_preset_id": { + "type": "string", + "format": "uuid" + }, "ttl_ms": { "type": "integer" } @@ -15548,6 +15557,10 @@ "template_version_name": { "type": "string" }, + "template_version_preset_id": { + "type": "string", + "format": "uuid" + }, "transition": { "enum": ["start", "stop", "delete"], "allOf": [ diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 18e68caf6ee7c..7fa583489a32e 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9788,19 +9788,20 @@ func (q *FakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.Inser defer q.mutex.Unlock() workspaceBuild := database.WorkspaceBuild{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - UpdatedAt: arg.UpdatedAt, - WorkspaceID: arg.WorkspaceID, - TemplateVersionID: arg.TemplateVersionID, - BuildNumber: arg.BuildNumber, - Transition: arg.Transition, - InitiatorID: arg.InitiatorID, - JobID: arg.JobID, - ProvisionerState: arg.ProvisionerState, - Deadline: arg.Deadline, - MaxDeadline: arg.MaxDeadline, - Reason: arg.Reason, + ID: arg.ID, + CreatedAt: arg.CreatedAt, + UpdatedAt: arg.UpdatedAt, + WorkspaceID: arg.WorkspaceID, + TemplateVersionID: arg.TemplateVersionID, + BuildNumber: arg.BuildNumber, + Transition: arg.Transition, + InitiatorID: arg.InitiatorID, + JobID: arg.JobID, + ProvisionerState: arg.ProvisionerState, + Deadline: arg.Deadline, + MaxDeadline: arg.MaxDeadline, + Reason: arg.Reason, + TemplateVersionPresetID: arg.TemplateVersionPresetID, } q.workspaceBuilds = append(q.workspaceBuilds, workspaceBuild) return nil diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 6f8c3707f7279..47fecfb4a1688 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -27,6 +27,8 @@ import ( "cdr.dev/slog" + "github.com/coder/quartz" + "github.com/coder/coder/v2/coderd/apikey" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" @@ -46,7 +48,6 @@ import ( "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" sdkproto "github.com/coder/coder/v2/provisionersdk/proto" - "github.com/coder/quartz" ) const ( @@ -635,6 +636,7 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo WorkspaceBuildId: workspaceBuild.ID.String(), WorkspaceOwnerLoginType: string(owner.LoginType), WorkspaceOwnerRbacRoles: ownerRbacRoles, + IsPrebuild: input.IsPrebuild, }, LogLevel: input.LogLevel, }, @@ -2460,6 +2462,7 @@ type TemplateVersionImportJob struct { type WorkspaceProvisionJob struct { WorkspaceBuildID uuid.UUID `json:"workspace_build_id"` DryRun bool `json:"dry_run"` + IsPrebuild bool `json:"is_prebuild,omitempty"` LogLevel string `json:"log_level,omitempty"` } diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 698520d6f8d02..87f6be1507866 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -164,279 +164,286 @@ func TestAcquireJob(t *testing.T) { _, err = tc.acquire(ctx, srv) require.ErrorContains(t, err, "sql: no rows in result set") }) - t.Run(tc.name+"_WorkspaceBuildJob", func(t *testing.T) { - t.Parallel() - // Set the max session token lifetime so we can assert we - // create an API key with an expiration within the bounds of the - // deployment config. - dv := &codersdk.DeploymentValues{ - Sessions: codersdk.SessionLifetime{ - MaximumTokenDuration: serpent.Duration(time.Hour), - }, - } - gitAuthProvider := &sdkproto.ExternalAuthProviderResource{ - Id: "github", - } + for _, prebuiltWorkspace := range []bool{false, true} { + prebuiltWorkspace := prebuiltWorkspace + t.Run(tc.name+"_WorkspaceBuildJob", func(t *testing.T) { + t.Parallel() + // Set the max session token lifetime so we can assert we + // create an API key with an expiration within the bounds of the + // deployment config. + dv := &codersdk.DeploymentValues{ + Sessions: codersdk.SessionLifetime{ + MaximumTokenDuration: serpent.Duration(time.Hour), + }, + } + gitAuthProvider := &sdkproto.ExternalAuthProviderResource{ + Id: "github", + } - srv, db, ps, pd := setup(t, false, &overrides{ - deploymentValues: dv, - externalAuthConfigs: []*externalauth.Config{{ - ID: gitAuthProvider.Id, - InstrumentedOAuth2Config: &testutil.OAuth2Config{}, - }}, - }) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() + srv, db, ps, pd := setup(t, false, &overrides{ + deploymentValues: dv, + externalAuthConfigs: []*externalauth.Config{{ + ID: gitAuthProvider.Id, + InstrumentedOAuth2Config: &testutil.OAuth2Config{}, + }}, + }) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() - user := dbgen.User(t, db, database.User{}) - group1 := dbgen.Group(t, db, database.Group{ - Name: "group1", - OrganizationID: pd.OrganizationID, - }) - sshKey := dbgen.GitSSHKey(t, db, database.GitSSHKey{ - UserID: user.ID, - }) - err := db.InsertGroupMember(ctx, database.InsertGroupMemberParams{ - UserID: user.ID, - GroupID: group1.ID, - }) - require.NoError(t, err) - link := dbgen.UserLink(t, db, database.UserLink{ - LoginType: database.LoginTypeOIDC, - UserID: user.ID, - OAuthExpiry: dbtime.Now().Add(time.Hour), - OAuthAccessToken: "access-token", - }) - dbgen.ExternalAuthLink(t, db, database.ExternalAuthLink{ - ProviderID: gitAuthProvider.Id, - UserID: user.ID, - }) - 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}) - versionFile := dbgen.File(t, db, database.File{CreatedBy: user.ID}) - version := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - OrganizationID: pd.OrganizationID, - TemplateID: uuid.NullUUID{ - UUID: template.ID, - Valid: true, - }, - JobID: uuid.New(), - }) - externalAuthProviders, err := json.Marshal([]database.ExternalAuthProvider{{ - ID: gitAuthProvider.Id, - Optional: gitAuthProvider.Optional, - }}) - require.NoError(t, err) - err = db.UpdateTemplateVersionExternalAuthProvidersByJobID(ctx, database.UpdateTemplateVersionExternalAuthProvidersByJobIDParams{ - JobID: version.JobID, - ExternalAuthProviders: json.RawMessage(externalAuthProviders), - UpdatedAt: dbtime.Now(), - }) - require.NoError(t, err) - // Import version job - _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - OrganizationID: pd.OrganizationID, - ID: version.JobID, - InitiatorID: user.ID, - FileID: versionFile.ID, - Provisioner: database.ProvisionerTypeEcho, - StorageMethod: database.ProvisionerStorageMethodFile, - Type: database.ProvisionerJobTypeTemplateVersionImport, - Input: must(json.Marshal(provisionerdserver.TemplateVersionImportJob{ - TemplateVersionID: version.ID, - UserVariableValues: []codersdk.VariableValue{ - {Name: "second", Value: "bah"}, + user := dbgen.User(t, db, database.User{}) + group1 := dbgen.Group(t, db, database.Group{ + Name: "group1", + OrganizationID: pd.OrganizationID, + }) + sshKey := dbgen.GitSSHKey(t, db, database.GitSSHKey{ + UserID: user.ID, + }) + err := db.InsertGroupMember(ctx, database.InsertGroupMemberParams{ + UserID: user.ID, + GroupID: group1.ID, + }) + require.NoError(t, err) + link := dbgen.UserLink(t, db, database.UserLink{ + LoginType: database.LoginTypeOIDC, + UserID: user.ID, + OAuthExpiry: dbtime.Now().Add(time.Hour), + OAuthAccessToken: "access-token", + }) + dbgen.ExternalAuthLink(t, db, database.ExternalAuthLink{ + ProviderID: gitAuthProvider.Id, + UserID: user.ID, + }) + 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}) + versionFile := dbgen.File(t, db, database.File{CreatedBy: user.ID}) + version := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + OrganizationID: pd.OrganizationID, + TemplateID: uuid.NullUUID{ + UUID: template.ID, + Valid: true, }, - })), - }) - _ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{ - TemplateVersionID: version.ID, - Name: "first", - Value: "first_value", - DefaultValue: "default_value", - Sensitive: true, - }) - _ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{ - TemplateVersionID: version.ID, - Name: "second", - Value: "second_value", - DefaultValue: "default_value", - Required: true, - Sensitive: false, - }) - workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ - TemplateID: template.ID, - OwnerID: user.ID, - OrganizationID: pd.OrganizationID, - }) - build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - WorkspaceID: workspace.ID, - BuildNumber: 1, - JobID: uuid.New(), - TemplateVersionID: version.ID, - Transition: database.WorkspaceTransitionStart, - Reason: database.BuildReasonInitiator, - }) - _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - ID: build.ID, - OrganizationID: pd.OrganizationID, - InitiatorID: user.ID, - Provisioner: database.ProvisionerTypeEcho, - StorageMethod: database.ProvisionerStorageMethodFile, - FileID: file.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: build.ID, - })), - }) + JobID: uuid.New(), + }) + externalAuthProviders, err := json.Marshal([]database.ExternalAuthProvider{{ + ID: gitAuthProvider.Id, + Optional: gitAuthProvider.Optional, + }}) + require.NoError(t, err) + err = db.UpdateTemplateVersionExternalAuthProvidersByJobID(ctx, database.UpdateTemplateVersionExternalAuthProvidersByJobIDParams{ + JobID: version.JobID, + ExternalAuthProviders: json.RawMessage(externalAuthProviders), + UpdatedAt: dbtime.Now(), + }) + require.NoError(t, err) + // Import version job + _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + OrganizationID: pd.OrganizationID, + ID: version.JobID, + InitiatorID: user.ID, + FileID: versionFile.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + Type: database.ProvisionerJobTypeTemplateVersionImport, + Input: must(json.Marshal(provisionerdserver.TemplateVersionImportJob{ + TemplateVersionID: version.ID, + UserVariableValues: []codersdk.VariableValue{ + {Name: "second", Value: "bah"}, + }, + })), + }) + _ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{ + TemplateVersionID: version.ID, + Name: "first", + Value: "first_value", + DefaultValue: "default_value", + Sensitive: true, + }) + _ = dbgen.TemplateVersionVariable(t, db, database.TemplateVersionVariable{ + TemplateVersionID: version.ID, + Name: "second", + Value: "second_value", + DefaultValue: "default_value", + Required: true, + Sensitive: false, + }) + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: template.ID, + OwnerID: user.ID, + OrganizationID: pd.OrganizationID, + }) + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + BuildNumber: 1, + JobID: uuid.New(), + TemplateVersionID: version.ID, + Transition: database.WorkspaceTransitionStart, + Reason: database.BuildReasonInitiator, + }) + _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + ID: build.ID, + OrganizationID: pd.OrganizationID, + InitiatorID: user.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + FileID: file.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: build.ID, + IsPrebuild: prebuiltWorkspace, + })), + }) - startPublished := make(chan struct{}) - var closed bool - closeStartSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), - wspubsub.HandleWorkspaceEvent( - func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { - if err != nil { - return - } - if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { - if !closed { - close(startPublished) - closed = true + startPublished := make(chan struct{}) + var closed bool + closeStartSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return } - } - })) - require.NoError(t, err) - defer closeStartSubscribe() + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { + if !closed { + close(startPublished) + closed = true + } + } + })) + require.NoError(t, err) + defer closeStartSubscribe() - var job *proto.AcquiredJob + var job *proto.AcquiredJob - for { - // Grab jobs until we find the workspace build job. There is also - // an import version job that we need to ignore. - job, err = tc.acquire(ctx, srv) - require.NoError(t, err) - if _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_); ok { - break + for { + // Grab jobs until we find the workspace build job. There is also + // an import version job that we need to ignore. + job, err = tc.acquire(ctx, srv) + require.NoError(t, err) + if _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_); ok { + break + } } - } - <-startPublished + <-startPublished - got, err := json.Marshal(job.Type) - require.NoError(t, err) + got, err := json.Marshal(job.Type) + require.NoError(t, err) - // Validate that a session token is generated during the job. - sessionToken := job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken - require.NotEmpty(t, sessionToken) - toks := strings.Split(sessionToken, "-") - require.Len(t, toks, 2, "invalid api key") - key, err := db.GetAPIKeyByID(ctx, toks[0]) - require.NoError(t, err) - require.Equal(t, int64(dv.Sessions.MaximumTokenDuration.Value().Seconds()), key.LifetimeSeconds) - require.WithinDuration(t, time.Now().Add(dv.Sessions.MaximumTokenDuration.Value()), key.ExpiresAt, time.Minute) - - want, err := json.Marshal(&proto.AcquiredJob_WorkspaceBuild_{ - WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ - WorkspaceBuildId: build.ID.String(), - WorkspaceName: workspace.Name, - VariableValues: []*sdkproto.VariableValue{ - { - Name: "first", - Value: "first_value", - Sensitive: true, - }, - { - Name: "second", - Value: "second_value", + // Validate that a session token is generated during the job. + sessionToken := job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken + require.NotEmpty(t, sessionToken) + toks := strings.Split(sessionToken, "-") + require.Len(t, toks, 2, "invalid api key") + key, err := db.GetAPIKeyByID(ctx, toks[0]) + require.NoError(t, err) + require.Equal(t, int64(dv.Sessions.MaximumTokenDuration.Value().Seconds()), key.LifetimeSeconds) + require.WithinDuration(t, time.Now().Add(dv.Sessions.MaximumTokenDuration.Value()), key.ExpiresAt, time.Minute) + + wantedMetadata := &sdkproto.Metadata{ + CoderUrl: (&url.URL{}).String(), + WorkspaceTransition: sdkproto.WorkspaceTransition_START, + WorkspaceName: workspace.Name, + WorkspaceOwner: user.Username, + WorkspaceOwnerEmail: user.Email, + WorkspaceOwnerName: user.Name, + WorkspaceOwnerOidcAccessToken: link.OAuthAccessToken, + WorkspaceOwnerGroups: []string{group1.Name}, + WorkspaceId: workspace.ID.String(), + WorkspaceOwnerId: user.ID.String(), + TemplateId: template.ID.String(), + TemplateName: template.Name, + TemplateVersion: version.Name, + WorkspaceOwnerSessionToken: sessionToken, + WorkspaceOwnerSshPublicKey: sshKey.PublicKey, + WorkspaceOwnerSshPrivateKey: sshKey.PrivateKey, + WorkspaceBuildId: build.ID.String(), + WorkspaceOwnerLoginType: string(user.LoginType), + WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: "member", OrgId: pd.OrganizationID.String()}}, + } + if prebuiltWorkspace { + wantedMetadata.IsPrebuild = true + } + want, err := json.Marshal(&proto.AcquiredJob_WorkspaceBuild_{ + WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ + WorkspaceBuildId: build.ID.String(), + WorkspaceName: workspace.Name, + VariableValues: []*sdkproto.VariableValue{ + { + Name: "first", + Value: "first_value", + Sensitive: true, + }, + { + Name: "second", + Value: "second_value", + }, }, + ExternalAuthProviders: []*sdkproto.ExternalAuthProvider{{ + Id: gitAuthProvider.Id, + AccessToken: "access_token", + }}, + Metadata: wantedMetadata, }, - ExternalAuthProviders: []*sdkproto.ExternalAuthProvider{{ - Id: gitAuthProvider.Id, - AccessToken: "access_token", - }}, - Metadata: &sdkproto.Metadata{ - CoderUrl: (&url.URL{}).String(), - WorkspaceTransition: sdkproto.WorkspaceTransition_START, - WorkspaceName: workspace.Name, - WorkspaceOwner: user.Username, - WorkspaceOwnerEmail: user.Email, - WorkspaceOwnerName: user.Name, - WorkspaceOwnerOidcAccessToken: link.OAuthAccessToken, - WorkspaceOwnerGroups: []string{group1.Name}, - WorkspaceId: workspace.ID.String(), - WorkspaceOwnerId: user.ID.String(), - TemplateId: template.ID.String(), - TemplateName: template.Name, - TemplateVersion: version.Name, - WorkspaceOwnerSessionToken: sessionToken, - WorkspaceOwnerSshPublicKey: sshKey.PublicKey, - WorkspaceOwnerSshPrivateKey: sshKey.PrivateKey, - WorkspaceBuildId: build.ID.String(), - WorkspaceOwnerLoginType: string(user.LoginType), - WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: "member", OrgId: pd.OrganizationID.String()}}, - }, - }, - }) - require.NoError(t, err) - - require.JSONEq(t, string(want), string(got)) + }) + require.NoError(t, err) - // Assert that we delete the session token whenever - // a stop is issued. - stopbuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - WorkspaceID: workspace.ID, - BuildNumber: 2, - JobID: uuid.New(), - TemplateVersionID: version.ID, - Transition: database.WorkspaceTransitionStop, - Reason: database.BuildReasonInitiator, - }) - _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ - ID: stopbuild.ID, - InitiatorID: user.ID, - Provisioner: database.ProvisionerTypeEcho, - StorageMethod: database.ProvisionerStorageMethodFile, - FileID: file.ID, - Type: database.ProvisionerJobTypeWorkspaceBuild, - Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: stopbuild.ID, - })), - }) + require.JSONEq(t, string(want), string(got)) - stopPublished := make(chan struct{}) - closeStopSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), - wspubsub.HandleWorkspaceEvent( - func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { - if err != nil { - return - } - if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { - close(stopPublished) - } - })) - require.NoError(t, err) - defer closeStopSubscribe() + // Assert that we delete the session token whenever + // a stop is issued. + stopbuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + BuildNumber: 2, + JobID: uuid.New(), + TemplateVersionID: version.ID, + Transition: database.WorkspaceTransitionStop, + Reason: database.BuildReasonInitiator, + }) + _ = dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + ID: stopbuild.ID, + InitiatorID: user.ID, + Provisioner: database.ProvisionerTypeEcho, + StorageMethod: database.ProvisionerStorageMethodFile, + FileID: file.ID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + Input: must(json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: stopbuild.ID, + })), + }) - // Grab jobs until we find the workspace build job. There is also - // an import version job that we need to ignore. - job, err = tc.acquire(ctx, srv) - require.NoError(t, err) - _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_) - require.True(t, ok, "acquired job not a workspace build?") + stopPublished := make(chan struct{}) + closeStopSubscribe, err := ps.SubscribeWithErr(wspubsub.WorkspaceEventChannel(workspace.OwnerID), + wspubsub.HandleWorkspaceEvent( + func(_ context.Context, e wspubsub.WorkspaceEvent, err error) { + if err != nil { + return + } + if e.Kind == wspubsub.WorkspaceEventKindStateChange && e.WorkspaceID == workspace.ID { + close(stopPublished) + } + })) + require.NoError(t, err) + defer closeStopSubscribe() - <-stopPublished + // Grab jobs until we find the workspace build job. There is also + // an import version job that we need to ignore. + job, err = tc.acquire(ctx, srv) + require.NoError(t, err) + _, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_) + require.True(t, ok, "acquired job not a workspace build?") - // Validate that a session token is deleted during a stop job. - sessionToken = job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken - require.Empty(t, sessionToken) - _, err = db.GetAPIKeyByID(ctx, key.ID) - require.ErrorIs(t, err, sql.ErrNoRows) - }) + <-stopPublished + // Validate that a session token is deleted during a stop job. + sessionToken = job.Type.(*proto.AcquiredJob_WorkspaceBuild_).WorkspaceBuild.Metadata.WorkspaceOwnerSessionToken + require.Empty(t, sessionToken) + _, err = db.GetAPIKeyByID(ctx, key.ID) + require.ErrorIs(t, err, sql.ErrNoRows) + }) + } t.Run(tc.name+"_TemplateVersionDryRun", func(t *testing.T) { t.Parallel() srv, db, ps, _ := setup(t, false, nil) diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 7bd32e00cd830..94f1822df797c 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -337,7 +337,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Initiator(apiKey.UserID). RichParameterValues(createBuild.RichParameterValues). LogLevel(string(createBuild.LogLevel)). - DeploymentValues(api.Options.DeploymentValues) + DeploymentValues(api.Options.DeploymentValues). + TemplateVersionPresetID(createBuild.TemplateVersionPresetID) var ( previousWorkspaceBuild database.WorkspaceBuild @@ -1065,6 +1066,11 @@ func (api *API) convertWorkspaceBuild( return apiResources[i].Name < apiResources[j].Name }) + var presetID *uuid.UUID + if build.TemplateVersionPresetID.Valid { + presetID = &build.TemplateVersionPresetID.UUID + } + apiJob := convertProvisionerJob(job) transition := codersdk.WorkspaceTransition(build.Transition) return codersdk.WorkspaceBuild{ @@ -1090,6 +1096,7 @@ func (api *API) convertWorkspaceBuild( Status: codersdk.ConvertWorkspaceStatus(apiJob.Status, transition), DailyCost: build.DailyCost, MatchedProvisioners: &matchedProvisioners, + TemplateVersionPresetID: presetID, }, nil } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 84efaa7ed0e23..08a8f3f26e0fa 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1307,6 +1307,50 @@ func TestPostWorkspaceBuild(t *testing.T) { require.Equal(t, wantState, gotState) }) + t.Run("SetsPresetID", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + 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", + }}, + }, + }, + }}, + ProvisionApply: echo.ApplyComplete, + }) + 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) + require.Nil(t, workspace.LatestBuild.TemplateVersionPresetID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Equal(t, 1, len(presets)) + require.Equal(t, "test", presets[0].Name) + + build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: version.ID, + Transition: codersdk.WorkspaceTransitionStart, + TemplateVersionPresetID: presets[0].ID, + }) + require.NoError(t, err) + require.NotNil(t, build.TemplateVersionPresetID) + + workspace, err = client.Workspace(ctx, workspace.ID) + require.NoError(t, err) + require.Equal(t, build.TemplateVersionPresetID, workspace.LatestBuild.TemplateVersionPresetID) + }) + t.Run("Delete", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index d49de2388af59..a654597faeadd 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -671,7 +671,8 @@ func createWorkspace( Reason(database.BuildReasonInitiator). Initiator(initiatorID). ActiveVersion(). - RichParameterValues(req.RichParameterValues) + RichParameterValues(req.RichParameterValues). + TemplateVersionPresetID(req.TemplateVersionPresetID) if req.TemplateVersionID != uuid.Nil { builder = builder.VersionID(req.TemplateVersionID) } diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 76e85b0716181..136e259d541f9 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -423,6 +423,51 @@ func TestWorkspace(t *testing.T) { require.ErrorAs(t, err, &apiError) require.Equal(t, http.StatusForbidden, apiError.StatusCode()) }) + + t.Run("TemplateVersionPreset", func(t *testing.T) { + t.Parallel() + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + authz := coderdtest.AssertRBAC(t, api, client) + 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", + }}, + }, + }, + }}, + ProvisionApply: echo.ApplyComplete, + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + ctx := testutil.Context(t, testutil.WaitLong) + + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Equal(t, 1, len(presets)) + require.Equal(t, "test", presets[0].Name) + + workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(request *codersdk.CreateWorkspaceRequest) { + request.TemplateVersionPresetID = presets[0].ID + }) + + authz.Reset() // Reset all previous checks done in setup. + ws, err := client.Workspace(ctx, workspace.ID) + authz.AssertChecked(t, policy.ActionRead, ws) + require.NoError(t, err) + require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID) + require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason) + require.Equal(t, presets[0].ID, *ws.LatestBuild.TemplateVersionPresetID) + + org, err := client.Organization(ctx, ws.OrganizationID) + require.NoError(t, err) + require.Equal(t, ws.OrganizationName, org.Name) + }) } func TestResolveAutostart(t *testing.T) { diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index f6d6d7381a24f..469c8fbcfdd6d 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -51,9 +51,10 @@ type Builder struct { logLevel string deploymentValues *codersdk.DeploymentValues - richParameterValues []codersdk.WorkspaceBuildParameter - initiator uuid.UUID - reason database.BuildReason + richParameterValues []codersdk.WorkspaceBuildParameter + initiator uuid.UUID + reason database.BuildReason + templateVersionPresetID uuid.UUID // used during build, makes function arguments less verbose ctx context.Context @@ -73,6 +74,8 @@ type Builder struct { parameterNames *[]string parameterValues *[]string + prebuild bool + verifyNoLegacyParametersOnce bool } @@ -168,6 +171,12 @@ func (b Builder) RichParameterValues(p []codersdk.WorkspaceBuildParameter) Build return b } +func (b Builder) MarkPrebuild() Builder { + // nolint: revive + b.prebuild = true + return b +} + // SetLastWorkspaceBuildInTx prepopulates the Builder's cache with the last workspace build. This allows us // to avoid a repeated database query when the Builder's caller also needs the workspace build, e.g. auto-start & // auto-stop. @@ -192,6 +201,12 @@ func (b Builder) SetLastWorkspaceBuildJobInTx(job *database.ProvisionerJob) Buil return b } +func (b Builder) TemplateVersionPresetID(id uuid.UUID) Builder { + // nolint: revive + b.templateVersionPresetID = id + return b +} + type BuildError struct { // Status is a suitable HTTP status code Status int @@ -295,6 +310,7 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ WorkspaceBuildID: workspaceBuildID, LogLevel: b.logLevel, + IsPrebuild: b.prebuild, }) if err != nil { return nil, nil, nil, BuildError{ @@ -363,20 +379,23 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object var workspaceBuild database.WorkspaceBuild err = b.store.InTx(func(store database.Store) error { err = store.InsertWorkspaceBuild(b.ctx, database.InsertWorkspaceBuildParams{ - ID: workspaceBuildID, - CreatedAt: now, - UpdatedAt: now, - WorkspaceID: b.workspace.ID, - TemplateVersionID: templateVersionID, - BuildNumber: buildNum, - ProvisionerState: state, - InitiatorID: b.initiator, - Transition: b.trans, - JobID: provisionerJob.ID, - Reason: b.reason, - Deadline: time.Time{}, // set by provisioner upon completion - MaxDeadline: time.Time{}, // set by provisioner upon completion - TemplateVersionPresetID: uuid.NullUUID{}, // TODO (sasswart): add this in from the caller + ID: workspaceBuildID, + CreatedAt: now, + UpdatedAt: now, + WorkspaceID: b.workspace.ID, + TemplateVersionID: templateVersionID, + BuildNumber: buildNum, + ProvisionerState: state, + InitiatorID: b.initiator, + Transition: b.trans, + JobID: provisionerJob.ID, + Reason: b.reason, + Deadline: time.Time{}, // set by provisioner upon completion + MaxDeadline: time.Time{}, // set by provisioner upon completion + TemplateVersionPresetID: uuid.NullUUID{ + UUID: b.templateVersionPresetID, + Valid: b.templateVersionPresetID != uuid.Nil, + }, }) if err != nil { code := http.StatusInternalServerError diff --git a/coderd/wsbuilder/wsbuilder_test.go b/coderd/wsbuilder/wsbuilder_test.go index d8f25c5a8cda3..bd6e64a60414a 100644 --- a/coderd/wsbuilder/wsbuilder_test.go +++ b/coderd/wsbuilder/wsbuilder_test.go @@ -41,6 +41,7 @@ var ( lastBuildID = uuid.MustParse("12341234-0000-0000-000b-000000000000") lastBuildJobID = uuid.MustParse("12341234-0000-0000-000c-000000000000") otherUserID = uuid.MustParse("12341234-0000-0000-000d-000000000000") + presetID = uuid.MustParse("12341234-0000-0000-000e-000000000000") ) func TestBuilder_NoOptions(t *testing.T) { @@ -773,6 +774,67 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { }) } +func TestWorkspaceBuildWithPreset(t *testing.T) { + t.Parallel() + + req := require.New(t) + asrt := assert.New(t) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var buildID uuid.UUID + + mDB := expectDB(t, + // Inputs + withTemplate, + withActiveVersion(nil), + withLastBuildNotFound, + withTemplateVersionVariables(activeVersionID, nil), + withParameterSchemas(activeJobID, nil), + withWorkspaceTags(activeVersionID, nil), + withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}), + + // Outputs + expectProvisionerJob(func(job database.InsertProvisionerJobParams) { + asrt.Equal(userID, job.InitiatorID) + asrt.Equal(activeFileID, job.FileID) + input := provisionerdserver.WorkspaceProvisionJob{} + err := json.Unmarshal(job.Input, &input) + req.NoError(err) + // store build ID for later + buildID = input.WorkspaceBuildID + }), + + withInTx, + expectBuild(func(bld database.InsertWorkspaceBuildParams) { + asrt.Equal(activeVersionID, bld.TemplateVersionID) + asrt.Equal(workspaceID, bld.WorkspaceID) + asrt.Equal(int32(1), bld.BuildNumber) + asrt.Equal(userID, bld.InitiatorID) + asrt.Equal(database.WorkspaceTransitionStart, bld.Transition) + asrt.Equal(database.BuildReasonInitiator, bld.Reason) + asrt.Equal(buildID, bld.ID) + asrt.True(bld.TemplateVersionPresetID.Valid) + asrt.Equal(presetID, bld.TemplateVersionPresetID.UUID) + }), + withBuild, + expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) { + asrt.Equal(buildID, params.WorkspaceBuildID) + asrt.Empty(params.Name) + asrt.Empty(params.Value) + }), + ) + + ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID} + uut := wsbuilder.New(ws, database.WorkspaceTransitionStart). + ActiveVersion(). + TemplateVersionPresetID(presetID) + // nolint: dogsled + _, _, _, err := uut.Build(ctx, mDB, nil, audit.WorkspaceBuildBaggage{}) + req.NoError(err) +} + type txExpect func(mTx *dbmock.MockStore) func expectDB(t *testing.T, opts ...txExpect) *dbmock.MockStore { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 8a028d46e098c..b981e3bed28fa 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -217,8 +217,9 @@ type CreateWorkspaceRequest struct { TTLMillis *int64 `json:"ttl_ms,omitempty"` // RichParameterValues allows for additional parameters to be provided // during the initial provision. - RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"` - AutomaticUpdates AutomaticUpdates `json:"automatic_updates,omitempty"` + RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"` + AutomaticUpdates AutomaticUpdates `json:"automatic_updates,omitempty"` + TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` } func (c *Client) OrganizationByName(ctx context.Context, name string) (Organization, error) { diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 2718735f01177..7b67dc3b86171 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -73,6 +73,7 @@ type WorkspaceBuild struct { Status WorkspaceStatus `json:"status" enums:"pending,starting,running,stopping,stopped,failed,canceling,canceled,deleting,deleted"` DailyCost int32 `json:"daily_cost"` MatchedProvisioners *MatchedProvisioners `json:"matched_provisioners,omitempty"` + TemplateVersionPresetID *uuid.UUID `json:"template_version_preset_id" format:"uuid"` } // WorkspaceResource describes resources used to create a workspace, for instance: diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index f9377c1767451..311c4bcba35d4 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -107,6 +107,8 @@ type CreateWorkspaceBuildRequest struct { // Log level changes the default logging verbosity of a provider ("info" if empty). LogLevel ProvisionerLogLevel `json:"log_level,omitempty" validate:"omitempty,oneof=debug"` + // TemplateVersionPresetID is the ID of the template version preset to use for the build. + TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` } type WorkspaceOptions struct { diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 0bb4b2e5b0ef3..1e5ff95026eaf 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -212,6 +212,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -440,6 +441,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1138,6 +1140,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1439,6 +1442,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1605,6 +1609,7 @@ Status Code **200** | `» status` | [codersdk.WorkspaceStatus](schemas.md#codersdkworkspacestatus) | false | | | | `» template_version_id` | string(uuid) | false | | | | `» template_version_name` | string | false | | | +| `» template_version_preset_id` | string(uuid) | false | | | | `» transition` | [codersdk.WorkspaceTransition](schemas.md#codersdkworkspacetransition) | false | | | | `» updated_at` | string(date-time) | false | | | | `» workspace_id` | string(uuid) | false | | | @@ -1707,6 +1712,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ 0 ], "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start" } ``` @@ -1909,6 +1915,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 870c113f67ace..e5fa809ef23f0 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1411,21 +1411,23 @@ None 0 ], "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start" } ``` ### Properties -| Name | Type | Required | Restrictions | Description | -|-------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `dry_run` | boolean | false | | | -| `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). | -| `orphan` | boolean | false | | Orphan may be set for the Destroy transition. | -| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values are optional. It will write params to the 'workspace' scope. This will overwrite any existing parameters with the same name. This will not delete old params not included in this list. | -| `state` | array of integer | false | | | -| `template_version_id` | string | false | | | -| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | true | | | +| Name | Type | Required | Restrictions | Description | +|------------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `dry_run` | boolean | false | | | +| `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). | +| `orphan` | boolean | false | | Orphan may be set for the Destroy transition. | +| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values are optional. It will write params to the 'workspace' scope. This will overwrite any existing parameters with the same name. This will not delete old params not included in this list. | +| `state` | array of integer | false | | | +| `template_version_id` | string | false | | | +| `template_version_preset_id` | string | false | | Template version preset ID is the ID of the template version preset to use for the build. | +| `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | true | | | #### Enumerated Values @@ -1469,6 +1471,7 @@ None ], "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "ttl_ms": 0 } ``` @@ -1477,15 +1480,16 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o ### Properties -| Name | Type | Required | Restrictions | Description | -|-------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------| -| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | | -| `autostart_schedule` | string | false | | | -| `name` | string | true | | | -| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values allows for additional parameters to be provided during the initial provision. | -| `template_id` | string | false | | Template ID specifies which template should be used for creating the workspace. | -| `template_version_id` | string | false | | Template version ID can be used to specify a specific version of a template for creating the workspace. | -| `ttl_ms` | integer | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------| +| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | | +| `autostart_schedule` | string | false | | | +| `name` | string | true | | | +| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values allows for additional parameters to be provided during the initial provision. | +| `template_id` | string | false | | Template ID specifies which template should be used for creating the workspace. | +| `template_version_id` | string | false | | Template version ID can be used to specify a specific version of a template for creating the workspace. | +| `template_version_preset_id` | string | false | | | +| `ttl_ms` | integer | false | | | ## codersdk.CryptoKey @@ -7761,6 +7765,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -8712,6 +8717,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -8741,6 +8747,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `status` | [codersdk.WorkspaceStatus](#codersdkworkspacestatus) | false | | | | `template_version_id` | string | false | | | | `template_version_name` | string | false | | | +| `template_version_preset_id` | string | false | | | | `transition` | [codersdk.WorkspaceTransition](#codersdkworkspacetransition) | false | | | | `updated_at` | string | false | | | | `workspace_id` | string | false | | | @@ -9408,6 +9415,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 00400942d34db..5e727cee297fe 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -34,6 +34,7 @@ of the template will be used. ], "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "ttl_ms": 0 } ``` @@ -265,6 +266,7 @@ of the template will be used. "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -541,6 +543,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -611,6 +614,7 @@ of the template will be used. ], "template_id": "c6d67e98-83ea-49f0-8812-e4abae2b68bc", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "ttl_ms": 0 } ``` @@ -841,6 +845,7 @@ of the template will be used. "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1103,6 +1108,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1380,6 +1386,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", @@ -1772,6 +1779,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "status": "pending", "template_version_id": "0ba39c92-1f1b-4c32-aa3e-9925d7713eb1", "template_version_name": "string", + "template_version_preset_id": "512a53a7-30da-446e-a1fc-713c630baff1", "transition": "start", "updated_at": "2019-08-24T14:15:22Z", "workspace_id": "0967198e-ec7b-4c6b-b4d3-f71244cadbe9", diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index 171deb35c4bbc..f8f82bbad7b9a 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -270,6 +270,10 @@ func provisionEnv( "CODER_WORKSPACE_TEMPLATE_VERSION="+metadata.GetTemplateVersion(), "CODER_WORKSPACE_BUILD_ID="+metadata.GetWorkspaceBuildId(), ) + if metadata.GetIsPrebuild() { + env = append(env, provider.IsPrebuildEnvironmentVariable()+"=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 00b459ca1df1a..e7b64046f3ab3 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -798,6 +798,44 @@ func TestProvision(t *testing.T) { }}, }, }, + { + Name: "is-prebuild", + Files: map[string]string{ + "main.tf": `terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.3.0-pre2" + } + } + } + data "coder_workspace" "me" {} + resource "null_resource" "example" {} + resource "coder_metadata" "example" { + resource_id = null_resource.example.id + item { + key = "is_prebuild" + value = data.coder_workspace.me.is_prebuild + } + } + `, + }, + Request: &proto.PlanRequest{ + Metadata: &proto.Metadata{ + IsPrebuild: true, + }, + }, + Response: &proto.PlanComplete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "null_resource", + Metadata: []*proto.Resource_Metadata{{ + Key: "is_prebuild", + Value: "true", + }}, + }}, + }, + }, } for _, testCase := range testCases { diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index b461bc593ee36..8e9df48b9a1e8 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -367,6 +367,7 @@ func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) { 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), ) span.SetAttributes( @@ -376,6 +377,7 @@ func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) { 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), ) } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3f3d8f92c27e5..24562dab7c04a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -447,6 +447,7 @@ export interface CreateWorkspaceBuildRequest { readonly orphan?: boolean; readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; readonly log_level?: ProvisionerLogLevel; + readonly template_version_preset_id?: string; } // From codersdk/workspaceproxy.go @@ -465,6 +466,7 @@ export interface CreateWorkspaceRequest { readonly ttl_ms?: number; readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; readonly automatic_updates?: AutomaticUpdates; + readonly template_version_preset_id?: string; } // From codersdk/deployment.go @@ -3482,6 +3484,7 @@ export interface WorkspaceBuild { readonly status: WorkspaceStatus; readonly daily_cost: number; readonly matched_provisioners?: MatchedProvisioners; + readonly template_version_preset_id: string | null; } // From codersdk/workspacebuilds.go diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 5dc9c8d0a4818..66d0033ea6a74 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -369,6 +369,10 @@ export const CreateWorkspacePageView: FC = ({ return; } setSelectedPresetIndex(index); + form.setFieldValue( + "template_version_preset_id", + option?.value, + ); }} placeholder="Select a preset" selectedOption={presetOptions[selectedPresetIndex]} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 804291df30729..a434c56200a87 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1266,6 +1266,7 @@ export const MockWorkspaceBuild: TypesGen.WorkspaceBuild = { count: 1, available: 1, }, + template_version_preset_id: null, }; export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = { @@ -1289,6 +1290,7 @@ export const MockWorkspaceBuildAutostart: TypesGen.WorkspaceBuild = { resources: [MockWorkspaceResource], status: "running", daily_cost: 20, + template_version_preset_id: null, }; export const MockWorkspaceBuildAutostop: TypesGen.WorkspaceBuild = { @@ -1312,6 +1314,7 @@ export const MockWorkspaceBuildAutostop: TypesGen.WorkspaceBuild = { resources: [MockWorkspaceResource], status: "running", daily_cost: 20, + template_version_preset_id: null, }; export const MockFailedWorkspaceBuild = ( @@ -1337,6 +1340,7 @@ export const MockFailedWorkspaceBuild = ( resources: [], status: "failed", daily_cost: 20, + template_version_preset_id: null, }); export const MockWorkspaceBuildStop: TypesGen.WorkspaceBuild = { From 2d2c9bda98993c83be536e332d200d428b75ac78 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 14 Apr 2025 16:24:02 +0100 Subject: [PATCH 085/384] fix(cli): correct logic around CODER_MCP_APP_STATUS_SLUG (#17391) Past me was not smart. --- cli/exp_mcp.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/exp_mcp.go b/cli/exp_mcp.go index 35032a43d68fc..63ee0db04b552 100644 --- a/cli/exp_mcp.go +++ b/cli/exp_mcp.go @@ -411,7 +411,7 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct } else { cliui.Warnf(inv.Stderr, "CODER_AGENT_TOKEN is not set, task reporting will not be available") } - if appStatusSlug != "" { + if appStatusSlug == "" { cliui.Warnf(inv.Stderr, "CODER_MCP_APP_STATUS_SLUG is not set, task reporting will not be available.") } else { clientCtx = toolsdk.WithWorkspaceAppStatusSlug(clientCtx, appStatusSlug) From 272edba1d873ca050a74f873ed961586bfd7828a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 14 Apr 2025 17:29:43 +0100 Subject: [PATCH 086/384] feat(codersdk/toolsdk): add template_version_id to coder_create_workspace_build (#17364) The `coder_create_workspace_build` tool was missing the ability to change the template version. --- codersdk/toolsdk/toolsdk.go | 23 +++++++++++++-- codersdk/toolsdk/toolsdk_test.go | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index 134c30c4f1474..6cadbe611f335 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -348,6 +348,11 @@ is provisioned correctly and the agent can connect to the control plane. "transition": map[string]any{ "type": "string", "description": "The transition to perform. Must be one of: start, stop, delete", + "enum": []string{"start", "stop", "delete"}, + }, + "template_version_id": map[string]any{ + "type": "string", + "description": "(Optional) The template version ID to use for the workspace build. If not provided, the previously built version will be used.", }, }, Required: []string{"workspace_id", "transition"}, @@ -366,9 +371,17 @@ is provisioned correctly and the agent can connect to the control plane. if !ok { return codersdk.WorkspaceBuild{}, xerrors.New("transition must be a string") } - return client.CreateWorkspaceBuild(ctx, workspaceID, codersdk.CreateWorkspaceBuildRequest{ + templateVersionID, err := uuidFromArgs(args, "template_version_id") + if err != nil { + return codersdk.WorkspaceBuild{}, err + } + cbr := codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransition(rawTransition), - }) + } + if templateVersionID != uuid.Nil { + cbr.TemplateVersionID = templateVersionID + } + return client.CreateWorkspaceBuild(ctx, workspaceID, cbr) }, } @@ -1240,7 +1253,11 @@ func workspaceAppStatusSlugFromContext(ctx context.Context) (string, bool) { } func uuidFromArgs(args map[string]any, key string) (uuid.UUID, error) { - raw, ok := args[key].(string) + argKey, ok := args[key] + if !ok { + return uuid.Nil, nil // No error if key is not present + } + raw, ok := argKey.(string) if !ok { return uuid.Nil, xerrors.Errorf("%s must be a string", key) } diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index ee48a6dd8c780..aca4045f36e8e 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" @@ -154,6 +155,8 @@ func TestTools(t *testing.T) { require.NoError(t, err) require.Equal(t, codersdk.WorkspaceTransitionStop, result.Transition) require.Equal(t, r.Workspace.ID, result.WorkspaceID) + require.Equal(t, r.TemplateVersion.ID, result.TemplateVersionID) + require.Equal(t, codersdk.WorkspaceTransitionStop, result.Transition) // Important: cancel the build. We don't run any provisioners, so this // will remain in the 'pending' state indefinitely. @@ -172,11 +175,58 @@ func TestTools(t *testing.T) { require.NoError(t, err) require.Equal(t, codersdk.WorkspaceTransitionStart, result.Transition) require.Equal(t, r.Workspace.ID, result.WorkspaceID) + require.Equal(t, r.TemplateVersion.ID, result.TemplateVersionID) + require.Equal(t, codersdk.WorkspaceTransitionStart, result.Transition) // Important: cancel the build. We don't run any provisioners, so this // will remain in the 'pending' state indefinitely. require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID)) }) + + t.Run("TemplateVersionChange", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + // Get the current template version ID before updating + workspace, err := memberClient.Workspace(ctx, r.Workspace.ID) + require.NoError(t, err) + originalVersionID := workspace.LatestBuild.TemplateVersionID + + // Create a new template version to update to + newVersion := dbfake.TemplateVersion(t, store). + // nolint:gocritic // This is in a test package and does not end up in the build + Seed(database.TemplateVersion{ + OrganizationID: owner.OrganizationID, + CreatedBy: owner.UserID, + TemplateID: uuid.NullUUID{UUID: r.Template.ID, Valid: true}, + }).Do() + + // Update to new version + updateBuild, err := testTool(ctx, t, toolsdk.CreateWorkspaceBuild, map[string]any{ + "workspace_id": r.Workspace.ID.String(), + "transition": "start", + "template_version_id": newVersion.TemplateVersion.ID.String(), + }) + require.NoError(t, err) + require.Equal(t, codersdk.WorkspaceTransitionStart, updateBuild.Transition) + require.Equal(t, r.Workspace.ID.String(), updateBuild.WorkspaceID.String()) + require.Equal(t, newVersion.TemplateVersion.ID.String(), updateBuild.TemplateVersionID.String()) + // Cancel the build so it doesn't remain in the 'pending' state indefinitely. + require.NoError(t, client.CancelWorkspaceBuild(ctx, updateBuild.ID)) + + // Roll back to the original version + rollbackBuild, err := testTool(ctx, t, toolsdk.CreateWorkspaceBuild, map[string]any{ + "workspace_id": r.Workspace.ID.String(), + "transition": "start", + "template_version_id": originalVersionID.String(), + }) + require.NoError(t, err) + require.Equal(t, codersdk.WorkspaceTransitionStart, rollbackBuild.Transition) + require.Equal(t, r.Workspace.ID.String(), rollbackBuild.WorkspaceID.String()) + require.Equal(t, originalVersionID.String(), rollbackBuild.TemplateVersionID.String()) + // Cancel the build so it doesn't remain in the 'pending' state indefinitely. + require.NoError(t, client.CancelWorkspaceBuild(ctx, rollbackBuild.ID)) + }) }) t.Run("ListTemplateVersionParameters", func(t *testing.T) { From e54aa442ebda87ae9ab70f5015b778688b386e76 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Mon, 14 Apr 2025 21:34:52 +0500 Subject: [PATCH 087/384] chore(dogfood): switch to JetBrains Toolbox module (#17392) --- dogfood/coder/main.tf | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/dogfood/coder/main.tf b/dogfood/coder/main.tf index 30e728ce76c09..5bf9e682bbf43 100644 --- a/dogfood/coder/main.tf +++ b/dogfood/coder/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "2.2.0-pre0" + version = "2.3.0" } docker = { source = "kreuzwerker/docker" @@ -191,16 +191,15 @@ module "vscode-web" { 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 "jetbrains" { + count = data.coder_workspace.me.start_count + source = "git::https://github.com/coder/modules.git//jetbrains?ref=jetbrains" + agent_id = coder_agent.dev.id + folder = local.repo_dir + options = ["WS", "GO"] + default = "GO" + latest = true + channel = "eap" } module "filebrowser" { From fa594f4f6a603c12925fbcce428d6f4c26364aa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:55:01 +0000 Subject: [PATCH 088/384] ci: bump the github-actions group across 1 directory with 8 updates (#17377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.11.0` | `2.11.1` | | [crate-ci/typos](https://github.com/crate-ci/typos) | `1.29.10` | `1.31.1` | | [actions/setup-java](https://github.com/actions/setup-java) | `4.7.0` | `4.7.1` | | [tj-actions/changed-files](https://github.com/tj-actions/changed-files) | `27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99` | `9934ab3fdf63239da75d9e0fbd339c48620c72c4` | | [tj-actions/branch-names](https://github.com/tj-actions/branch-names) | `8.1.0` | `8.2.1` | | [github/codeql-action](https://github.com/github/codeql-action) | `3.28.12` | `3.28.15` | | [coder/start-workspace-action](https://github.com/coder/start-workspace-action) | `26d3600161d67901f24d8612793d3b82771cde2d` | `35a4608cefc7e8cc56573cae7c3b85304575cb72` | | [umbrelladocs/action-linkspector](https://github.com/umbrelladocs/action-linkspector) | `1.3.2` | `1.3.4` | Updates `step-security/harden-runner` from 2.11.0 to 2.11.1
    Release notes

    Sourced from step-security/harden-runner's releases.

    v2.11.1

    What's Changed

    Full Changelog: https://github.com/step-security/harden-runner/compare/v2...v2.11.1

    Commits

    Updates `crate-ci/typos` from 1.29.10 to 1.31.1
    Release notes

    Sourced from crate-ci/typos's releases.

    v1.31.1

    [1.31.1] - 2025-03-31

    Fixes

    • (dict) Also correct typ to type

    v1.31.0

    [1.31.0] - 2025-03-28

    Features

    • Updated the dictionary with the March 2025 changes

    v1.30.3

    [1.30.3] - 2025-03-24

    Features

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

    v1.30.2

    [1.30.2] - 2025-03-10

    Features

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

    v1.30.1

    [1.30.1] - 2025-03-04

    Features

    • (action) Create v1 tag

    v1.30.0

    [1.30.0] - 2025-03-01

    Features

    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.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

    [1.30.1] - 2025-03-04

    Features

    • (action) Create v1 tag

    [1.30.0] - 2025-03-01

    Features

    [1.29.10] - 2025-02-25

    Fixes

    • Also correct contaminent as contaminant

    ... (truncated)

    Commits

    Updates `actions/setup-java` from 4.7.0 to 4.7.1
    Release notes

    Sourced from actions/setup-java's releases.

    v4.7.1

    What's Changed

    Documentation changes

    Dependency updates:

    Full Changelog: https://github.com/actions/setup-java/compare/v4...v4.7.1

    Commits
    • c5195ef actions/cache upgrade to 4.0.3 (#773)
    • dd38875 Bump ts-jest from 29.1.2 to 29.2.5 (#743)
    • 148017a Bump @​actions/glob from 0.4.0 to 0.5.0 (#744)
    • 3b6c050 Remove duplicated GraalVM section in documentation (#716)
    • b8ebb8b upgrade @​action/cache from 4.0.0 to 4.0.2 (#766)
    • 799ee7c Add Documentation to Recommend Using GraalVM JDK 17 Version to 17.0.12 to Ali...
    • See full diff in compare view

    Updates `tj-actions/changed-files` from 27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99 to 9934ab3fdf63239da75d9e0fbd339c48620c72c4
    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
    • 9934ab3 chore(deps-dev): bump eslint-config-prettier from 10.1.1 to 10.1.2 (#2532)
    • db731a1 Upgraded to v46.0.5 (#2531)
    • ed68ef8 chore(deps): bump yaml from 2.7.0 to 2.7.1 (#2520)
    • a7bc14b chore(deps-dev): bump typescript from 5.8.2 to 5.8.3 (#2516)
    • 3d751f6 chore(deps-dev): bump @​types/node from 22.13.11 to 22.14.0 (#2517)
    • e2fda4e chore(deps-dev): bump eslint-plugin-prettier from 5.2.3 to 5.2.6 (#2519)
    • 0bed1b1 chore(deps-dev): bump ts-jest from 29.2.6 to 29.3.1 (#2518)
    • 6802458 chore(deps): bump github/codeql-action from 3.28.12 to 3.28.15 (#2530)
    • cf2e39e chore(deps): bump tj-actions/branch-names from 8.0.1 to 8.1.0 (#2521)
    • 6abeaa5 chore(deps): bump tj-actions/verify-changed-files from 20.0.1 to 20.0.4 (#2523)
    • Additional commits viewable in compare view

    Updates `tj-actions/branch-names` from 8.1.0 to 8.2.1
    Release notes

    Sourced from tj-actions/branch-names's releases.

    v8.2.1

    What's Changed

    Full Changelog: https://github.com/tj-actions/branch-names/compare/v8.2.0...v8.2.1

    v8.2.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/tj-actions/branch-names/compare/v8...v8.2.0

    Changelog

    Sourced from tj-actions/branch-names's changelog.

    Changelog

    8.2.1 - (2025-04-11)

    🐛 Bug Fixes

    • Update sync-release-version.yml to sign commits (#416) (dde14ac) - (Tonye Jack)

    8.2.0 - (2025-04-11)

    🚀 Features

    • Add support for replace forward slashes with hyphens (#412) (af40635) - (Tonye Jack)

    ➖ Remove

    • Deleted .github/workflows/rebase.yml (c209967) - (Tonye Jack)

    🔄 Update

    • Updated README.md (#415)

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

    • Update update-readme.yml (c9cf6f9) - (Tonye Jack)

    ⚙️ Miscellaneous Tasks

    • Update update-readme.yml (#414) (b1f61bc) - (Tonye Jack)

    ⬆️ Upgrades

    • Upgraded from v8.0.2 -> v8.1.0 (#410)

    (9601220) - (Tonye Jack)

    8.1.0 - (2025-03-23)

    🚀 Features

    • Add support for strip_branch_prefix (#406) (c83c87a) - (Tonye Jack)

    🔄 Update

    • Updated README.md (#408)

    (d18e657) - (Tonye Jack)

    ⚙️ Miscellaneous Tasks

    ... (truncated)

    Commits

    Updates `github/codeql-action` from 3.28.12 to 3.28.15
    Release notes

    Sourced from github/codeql-action's releases.

    v3.28.15

    CodeQL Action Changelog

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

    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

    See the full CHANGELOG.md for more information.

    v3.28.14

    CodeQL Action Changelog

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

    3.28.14 - 07 Apr 2025

    • Update default CodeQL bundle version to 2.21.0. #2838

    See the full CHANGELOG.md for more information.

    v3.28.13

    CodeQL Action Changelog

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

    3.28.13 - 24 Mar 2025

    No user facing changes.

    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.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

    3.28.7 - 29 Jan 2025

    No user facing changes.

    3.28.6 - 27 Jan 2025

    • Re-enable debug artifact upload for CLI versions 2.20.3 or greater. #2726

    ... (truncated)

    Commits
    • 45775bd Merge pull request #2854 from github/update-v3.28.15-a35ae8c38
    • dd78aab Update CHANGELOG.md with bug fix details
    • e40af59 Update changelog for v3.28.15
    • a35ae8c Merge pull request #2843 from github/cklin/diff-informed-compat
    • bb59df6 Merge pull request #2842 from github/henrymercer/zip64
    • 4b508f5 Merge pull request #2845 from github/mergeback/v3.28.14-to-main-fc7e4a0f
    • ca00afb Update checked-in dependencies
    • 2969c78 Update changelog and version after v3.28.14
    • fc7e4a0 Merge pull request #2844 from github/update-v3.28.14-362ef4ce2
    • be0175c Update changelog for v3.28.14
    • Additional commits viewable in compare view

    Updates `coder/start-workspace-action` from 26d3600161d67901f24d8612793d3b82771cde2d to 35a4608cefc7e8cc56573cae7c3b85304575cb72
    Commits
    • 35a4608 update github-username description to specify requirement for Coder 2.21 or...
    • 0054568 clarify requirements for the github-username input
    • f3cda2e fix variable names
    • a6a41dc update readme
    • a09e31d more defaults for inputs
    • 1330420 Add a screenshot to the README
    • 8d0b0d4 clarify status comment
    • 747b408 update input descriptions
    • e526e6f update example action tag
    • 212ab2f update readme and add a license
    • Additional commits viewable in compare view

    Updates `umbrelladocs/action-linkspector` from 1.3.2 to 1.3.4
    Release notes

    Sourced from umbrelladocs/action-linkspector's releases.

    Release v1.3.4

    v1.3.4: PR #42 - Update linkspector version to 0.4.4

    Release v1.3.3

    v1.3.3: PR #41 - Update linkspector version to 0.4.3

    Commits
    • a0567ce Merge pull request #42 from UmbrellaDocs/update-linkspector-version
    • f5418fd Update linkspector version to 0.4.4
    • 3e12ade Merge pull request #41 from UmbrellaDocs/update-linkspector-version
    • 8dfab65 Update linkspector version to 0.4.3
    • See full diff in compare view

    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> Co-authored-by: Muhammad Atif Ali --- .github/workflows/ci.yaml | 44 +++++++++++------------ .github/workflows/docker-base.yaml | 2 +- .github/workflows/docs-ci.yaml | 2 +- .github/workflows/dogfood.yaml | 6 ++-- .github/workflows/nightly-gauntlet.yaml | 2 +- .github/workflows/pr-auto-assign.yaml | 2 +- .github/workflows/pr-cleanup.yaml | 2 +- .github/workflows/pr-deploy.yaml | 10 +++--- .github/workflows/release-validation.yaml | 2 +- .github/workflows/release.yaml | 10 +++--- .github/workflows/scorecard.yml | 4 +-- .github/workflows/security.yaml | 10 +++--- .github/workflows/stale.yaml | 6 ++-- .github/workflows/start-workspace.yaml | 2 +- .github/workflows/typos.toml | 4 +++ .github/workflows/weekly-docs.yaml | 4 +-- 16 files changed, 58 insertions(+), 54 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a98fbe9b8f28b..54239330f2a4f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -34,7 +34,7 @@ jobs: tailnet-integration: ${{ steps.filter.outputs.tailnet-integration }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -155,7 +155,7 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -188,7 +188,7 @@ jobs: # Check for any typos - name: Check for typos - uses: crate-ci/typos@db35ee91e80fbb447f33b0e5fbddb24d2a1a884f # v1.29.10 + uses: crate-ci/typos@b1a1ef3893ff35ade0cfa71523852a49bfd05d19 # v1.31.1 with: config: .github/workflows/typos.toml @@ -227,7 +227,7 @@ jobs: if: always() steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -287,7 +287,7 @@ jobs: timeout-minutes: 7 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -331,7 +331,7 @@ jobs: - windows-2022 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -391,7 +391,7 @@ jobs: - windows-2022 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -447,7 +447,7 @@ jobs: - ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -504,7 +504,7 @@ jobs: timeout-minutes: 25 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -541,7 +541,7 @@ jobs: timeout-minutes: 25 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -579,7 +579,7 @@ jobs: timeout-minutes: 25 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -627,7 +627,7 @@ jobs: timeout-minutes: 20 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -653,7 +653,7 @@ jobs: timeout-minutes: 20 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -685,7 +685,7 @@ jobs: name: ${{ matrix.variant.name }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -754,7 +754,7 @@ jobs: if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -831,7 +831,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -905,7 +905,7 @@ jobs: if: always() steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -1035,7 +1035,7 @@ jobs: IMAGE: ghcr.io/coder/coder-preview:${{ steps.build-docker.outputs.tag }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -1059,7 +1059,7 @@ jobs: # Necessary for signing Windows binaries. - name: Setup Java - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: "zulu" java-version: "11.0" @@ -1381,7 +1381,7 @@ jobs: id-token: write steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -1445,7 +1445,7 @@ jobs: if: github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -1480,7 +1480,7 @@ jobs: if: needs.changes.outputs.db == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/docker-base.yaml b/.github/workflows/docker-base.yaml index d318c16d92334..427b7c254e97d 100644 --- a/.github/workflows/docker-base.yaml +++ b/.github/workflows/docker-base.yaml @@ -38,7 +38,7 @@ jobs: if: github.repository_owner == 'coder' steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/docs-ci.yaml b/.github/workflows/docs-ci.yaml index 7bbadbe3aba92..6d80b8068d5b5 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@27ae6b33eaed7bf87272fdeb9f1c54f9facc9d99 # v45.0.7 + - uses: tj-actions/changed-files@9934ab3fdf63239da75d9e0fbd339c48620c72c4 # v45.0.7 id: changed-files with: files: | diff --git a/.github/workflows/dogfood.yaml b/.github/workflows/dogfood.yaml index d43123781b0b9..70fbe81c09bbf 100644 --- a/.github/workflows/dogfood.yaml +++ b/.github/workflows/dogfood.yaml @@ -27,7 +27,7 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-4' || 'ubuntu-latest' }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -58,7 +58,7 @@ jobs: - name: Get branch name id: branch-name - uses: tj-actions/branch-names@f44339b51f74753b57583fbbd124e18a81170ab1 # v8.1.0 + uses: tj-actions/branch-names@dde14ac574a8b9b1cedc59a1cf312788af43d8d8 # v8.2.1 - name: "Branch name to Docker tag name" id: docker-tag-name @@ -114,7 +114,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/nightly-gauntlet.yaml b/.github/workflows/nightly-gauntlet.yaml index 2168be9c6bd93..d82ce3be08470 100644 --- a/.github/workflows/nightly-gauntlet.yaml +++ b/.github/workflows/nightly-gauntlet.yaml @@ -27,7 +27,7 @@ jobs: - windows-2022 steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/pr-auto-assign.yaml b/.github/workflows/pr-auto-assign.yaml index ef8245bbff0e3..8662252ae1d03 100644 --- a/.github/workflows/pr-auto-assign.yaml +++ b/.github/workflows/pr-auto-assign.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/pr-cleanup.yaml b/.github/workflows/pr-cleanup.yaml index 201cc386f0052..320c429880088 100644 --- a/.github/workflows/pr-cleanup.yaml +++ b/.github/workflows/pr-cleanup.yaml @@ -19,7 +19,7 @@ jobs: packages: write steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index b8b6705fe0fc9..00525eba6432a 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -39,7 +39,7 @@ jobs: PR_OPEN: ${{ steps.check_pr.outputs.pr_open }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -74,7 +74,7 @@ jobs: runs-on: "ubuntu-latest" steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -174,7 +174,7 @@ jobs: pull-requests: write # needed for commenting on PRs steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -218,7 +218,7 @@ jobs: CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -276,7 +276,7 @@ jobs: PR_HOSTNAME: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/release-validation.yaml b/.github/workflows/release-validation.yaml index 54111aa876916..d71a02881d95b 100644 --- a/.github/workflows/release-validation.yaml +++ b/.github/workflows/release-validation.yaml @@ -14,7 +14,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 653912ae2dad2..94d7b6f9ae5e4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -134,7 +134,7 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -222,7 +222,7 @@ jobs: # Necessary for signing Windows binaries. - name: Setup Java - uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 with: distribution: "zulu" java-version: "11.0" @@ -737,7 +737,7 @@ jobs: # TODO: skip this if it's not a new release (i.e. a backport). This is # fine right now because it just makes a PR that we can close. - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -813,7 +813,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -903,7 +903,7 @@ jobs: if: ${{ !inputs.dry_run }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 08eea59f4c24e..417b626d063de 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -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@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 with: sarif_file: results.sarif diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 88e6b51771434..19b7a13fb3967 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -27,7 +27,7 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -38,7 +38,7 @@ jobs: uses: ./.github/actions/setup-go - name: Initialize CodeQL - uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 with: languages: go, javascript @@ -48,7 +48,7 @@ jobs: rm Makefile - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 - name: Send Slack notification on failure if: ${{ failure() }} @@ -67,7 +67,7 @@ jobs: runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -150,7 +150,7 @@ jobs: severity: "CRITICAL,HIGH" - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 + uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15 with: sarif_file: trivy-results.sarif category: "Trivy" diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 33b667eee0a8d..558631224220d 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -18,7 +18,7 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -96,7 +96,7 @@ jobs: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -118,7 +118,7 @@ jobs: actions: write steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit diff --git a/.github/workflows/start-workspace.yaml b/.github/workflows/start-workspace.yaml index b7d618e7b0cf0..17e24241d6272 100644 --- a/.github/workflows/start-workspace.yaml +++ b/.github/workflows/start-workspace.yaml @@ -16,7 +16,7 @@ jobs: timeout-minutes: 5 steps: - name: Start Coder workspace - uses: coder/start-workspace-action@26d3600161d67901f24d8612793d3b82771cde2d + uses: coder/start-workspace-action@35a4608cefc7e8cc56573cae7c3b85304575cb72 with: github-token: ${{ secrets.GITHUB_TOKEN }} trigger-phrase: "@coder" diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index fffd2afbd20a1..6a9b07b475111 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -1,3 +1,6 @@ +[default] +extend-ignore-identifiers-re = ["gho_.*"] + [default.extend-identifiers] alog = "alog" Jetbrains = "JetBrains" @@ -24,6 +27,7 @@ EDE = "EDE" HELO = "HELO" LKE = "LKE" byt = "byt" +typ = "typ" [files] extend-exclude = [ diff --git a/.github/workflows/weekly-docs.yaml b/.github/workflows/weekly-docs.yaml index f7357306d6410..45306813ff66a 100644 --- a/.github/workflows/weekly-docs.yaml +++ b/.github/workflows/weekly-docs.yaml @@ -21,7 +21,7 @@ jobs: pull-requests: write # required to post PR review comments by the action steps: - name: Harden Runner - uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 with: egress-policy: audit @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check Markdown links - uses: umbrelladocs/action-linkspector@49cf4f8da82db70e691bb8284053add5028fa244 # v1.3.2 + uses: umbrelladocs/action-linkspector@a0567ce1c7c13de4a2358587492ed43cab5d0102 # v1.3.4 id: markdown-link-check # checks all markdown files from /docs including all subfolders with: From 2f99d70640ba4522f721c99c1f146afe8e55c8dd Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Tue, 15 Apr 2025 09:50:57 +0200 Subject: [PATCH 089/384] fix: configure start workspace action after version upgrade (#17398) Dependabot recently upgraded `coder/start-workspace-action` to the latest version. Compared to the version we were using previously, the new version expects a different configuration. --- .github/workflows/start-workspace.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/start-workspace.yaml b/.github/workflows/start-workspace.yaml index 17e24241d6272..41a5cd4b41d9f 100644 --- a/.github/workflows/start-workspace.yaml +++ b/.github/workflows/start-workspace.yaml @@ -12,6 +12,9 @@ permissions: jobs: comment: runs-on: ubuntu-latest + if: >- + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@coder')) || + (github.event_name == 'issues' && contains(github.event.issue.body, '@coder')) environment: aidev timeout-minutes: 5 steps: @@ -19,14 +22,16 @@ jobs: uses: coder/start-workspace-action@35a4608cefc7e8cc56573cae7c3b85304575cb72 with: github-token: ${{ secrets.GITHUB_TOKEN }} - trigger-phrase: "@coder" + github-username: >- + ${{ + (github.event_name == 'issue_comment' && github.event.comment.user.login) || + (github.event_name == 'issues' && github.event.issue.user.login) + }} coder-url: ${{ secrets.CODER_URL }} coder-token: ${{ secrets.CODER_TOKEN }} template-name: ${{ secrets.CODER_TEMPLATE_NAME }} - workspace-name: issue-${{ github.event.issue.number }} parameters: |- Coder Image: codercom/oss-dogfood:latest Coder Repository Base Directory: "~" AI Code Prompt: "Use the gh CLI tool to read the details of issue https://github.com/${{ github.repository }}/issues/${{ github.event.issue.number }} and then address it." Region: us-pittsburgh - user-mapping: ${{ secrets.CODER_USER_MAPPING }} From 0b18e458f4319b2522e852bc3f65a55db6928211 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 15 Apr 2025 10:55:30 +0200 Subject: [PATCH 090/384] fix: reduce excessive logging when database is unreachable (#17363) Fixes #17045 --------- Signed-off-by: Danny Kopping --- coderd/coderd.go | 9 ++--- coderd/tailnet.go | 16 ++++++++- coderd/tailnet_test.go | 41 ++++++++++++++++++++++ coderd/workspaceagents.go | 10 ++++++ codersdk/database.go | 7 ++++ codersdk/workspacesdk/dialer.go | 15 +++++--- codersdk/workspacesdk/workspacesdk_test.go | 35 ++++++++++++++++++ provisionerd/provisionerd.go | 30 +++++++++++----- site/src/api/typesGenerated.ts | 3 ++ tailnet/controllers.go | 14 ++++++-- 10 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 codersdk/database.go diff --git a/coderd/coderd.go b/coderd/coderd.go index 43caf8b344edc..a5886061ac4dc 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -675,10 +675,11 @@ func New(options *Options) *API { api.Auditor.Store(&options.Auditor) api.TailnetCoordinator.Store(&options.TailnetCoordinator) dialer := &InmemTailnetDialer{ - CoordPtr: &api.TailnetCoordinator, - DERPFn: api.DERPMap, - Logger: options.Logger, - ClientID: uuid.New(), + CoordPtr: &api.TailnetCoordinator, + DERPFn: api.DERPMap, + Logger: options.Logger, + ClientID: uuid.New(), + DatabaseHealthCheck: api.Database, } stn, err := NewServerTailnet(api.ctx, options.Logger, diff --git a/coderd/tailnet.go b/coderd/tailnet.go index b06219db40a78..cfdc667f4da0f 100644 --- a/coderd/tailnet.go +++ b/coderd/tailnet.go @@ -24,9 +24,11 @@ import ( "tailscale.com/tailcfg" "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/coderd/workspaceapps/appurl" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/site" "github.com/coder/coder/v2/tailnet" @@ -534,6 +536,10 @@ func NewMultiAgentController(ctx context.Context, logger slog.Logger, tracer tra return m } +type Pinger interface { + Ping(context.Context) (time.Duration, error) +} + // InmemTailnetDialer is a tailnet.ControlProtocolDialer that connects to a Coordinator and DERPMap // service running in the same memory space. type InmemTailnetDialer struct { @@ -541,9 +547,17 @@ type InmemTailnetDialer struct { DERPFn func() *tailcfg.DERPMap Logger slog.Logger ClientID uuid.UUID + // DatabaseHealthCheck is used to validate that the store is reachable. + DatabaseHealthCheck Pinger } -func (a *InmemTailnetDialer) Dial(_ context.Context, _ tailnet.ResumeTokenController) (tailnet.ControlProtocolClients, error) { +func (a *InmemTailnetDialer) Dial(ctx context.Context, _ tailnet.ResumeTokenController) (tailnet.ControlProtocolClients, error) { + if a.DatabaseHealthCheck != nil { + if _, err := a.DatabaseHealthCheck.Ping(ctx); err != nil { + return tailnet.ControlProtocolClients{}, xerrors.Errorf("%w: %v", codersdk.ErrDatabaseNotReachable, err) + } + } + coord := a.CoordPtr.Load() if coord == nil { return tailnet.ControlProtocolClients{}, xerrors.Errorf("tailnet coordinator not initialized") diff --git a/coderd/tailnet_test.go b/coderd/tailnet_test.go index b0aaaedc769c0..28265404c3eae 100644 --- a/coderd/tailnet_test.go +++ b/coderd/tailnet_test.go @@ -11,6 +11,7 @@ import ( "strconv" "sync/atomic" "testing" + "time" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" @@ -18,6 +19,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" + "golang.org/x/xerrors" "tailscale.com/tailcfg" "github.com/coder/coder/v2/agent" @@ -25,6 +27,7 @@ import ( "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/workspaceapps/appurl" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/coder/v2/tailnet" @@ -365,6 +368,44 @@ func TestServerTailnet_ReverseProxy(t *testing.T) { }) } +func TestDialFailure(t *testing.T) { + t.Parallel() + + // Setup. + ctx := testutil.Context(t, testutil.WaitShort) + logger := testutil.Logger(t) + + // Given: a tailnet coordinator. + coord := tailnet.NewCoordinator(logger) + t.Cleanup(func() { + _ = coord.Close() + }) + coordPtr := atomic.Pointer[tailnet.Coordinator]{} + coordPtr.Store(&coord) + + // Given: a fake DB healthchecker which will always fail. + fch := &failingHealthcheck{} + + // When: dialing the in-memory coordinator. + dialer := &coderd.InmemTailnetDialer{ + CoordPtr: &coordPtr, + Logger: logger, + ClientID: uuid.UUID{5}, + DatabaseHealthCheck: fch, + } + _, err := dialer.Dial(ctx, nil) + + // Then: the error returned reflects the database has failed its healthcheck. + require.ErrorIs(t, err, codersdk.ErrDatabaseNotReachable) +} + +type failingHealthcheck struct{} + +func (failingHealthcheck) Ping(context.Context) (time.Duration, error) { + // Simulate a database connection error. + return 0, xerrors.New("oops") +} + type wrappedListener struct { net.Listener dials int32 diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index a4f8ed297b77a..4af12fa228713 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -997,6 +997,16 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) { func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() + // Ensure the database is reachable before proceeding. + _, err := api.Database.Ping(ctx) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: codersdk.DatabaseNotReachable, + Detail: err.Error(), + }) + return + } + // This route accepts user API key auth and workspace proxy auth. The moon actor has // full permissions so should be able to pass this authz check. workspace := httpmw.WorkspaceParam(r) diff --git a/codersdk/database.go b/codersdk/database.go new file mode 100644 index 0000000000000..1a33da6362e0d --- /dev/null +++ b/codersdk/database.go @@ -0,0 +1,7 @@ +package codersdk + +import "golang.org/x/xerrors" + +const DatabaseNotReachable = "database not reachable" + +var ErrDatabaseNotReachable = xerrors.New(DatabaseNotReachable) diff --git a/codersdk/workspacesdk/dialer.go b/codersdk/workspacesdk/dialer.go index 23d618761b807..71cac0c5f04b1 100644 --- a/codersdk/workspacesdk/dialer.go +++ b/codersdk/workspacesdk/dialer.go @@ -11,17 +11,19 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/websocket" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" - "github.com/coder/websocket" ) var permanentErrorStatuses = []int{ - http.StatusConflict, // returned if client/agent connections disabled (browser only) - http.StatusBadRequest, // returned if API mismatch - http.StatusNotFound, // returned if user doesn't have permission or agent doesn't exist + http.StatusConflict, // returned if client/agent connections disabled (browser only) + http.StatusBadRequest, // returned if API mismatch + http.StatusNotFound, // returned if user doesn't have permission or agent doesn't exist + http.StatusInternalServerError, // returned if database is not reachable, } type WebsocketDialer struct { @@ -89,6 +91,11 @@ func (w *WebsocketDialer) Dial(ctx context.Context, r tailnet.ResumeTokenControl "Ensure your client release version (%s, different than the API version) matches the server release version", buildinfo.Version()) } + + if sdkErr.Message == codersdk.DatabaseNotReachable && + sdkErr.StatusCode() == http.StatusInternalServerError { + err = xerrors.Errorf("%w: %v", codersdk.ErrDatabaseNotReachable, err) + } } w.connected <- err return tailnet.ControlProtocolClients{}, err diff --git a/codersdk/workspacesdk/workspacesdk_test.go b/codersdk/workspacesdk/workspacesdk_test.go index 317db4471319f..e7ccd96e208fa 100644 --- a/codersdk/workspacesdk/workspacesdk_test.go +++ b/codersdk/workspacesdk/workspacesdk_test.go @@ -1,13 +1,21 @@ package workspacesdk_test import ( + "net/http" + "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/require" "tailscale.com/tailcfg" + "github.com/coder/websocket" + + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" + "github.com/coder/coder/v2/codersdk/workspacesdk" + "github.com/coder/coder/v2/testutil" ) func TestWorkspaceRewriteDERPMap(t *testing.T) { @@ -37,3 +45,30 @@ func TestWorkspaceRewriteDERPMap(t *testing.T) { require.Equal(t, "coconuts.org", node.HostName) require.Equal(t, 44558, node.DERPPort) } + +func TestWorkspaceDialerFailure(t *testing.T) { + t.Parallel() + + // Setup. + ctx := testutil.Context(t, testutil.WaitShort) + logger := testutil.Logger(t) + + // Given: a mock HTTP server which mimicks an unreachable database when calling the coordination endpoint. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ + Message: codersdk.DatabaseNotReachable, + Detail: "oops", + }) + })) + t.Cleanup(srv.Close) + + u, err := url.Parse(srv.URL) + require.NoError(t, err) + + // When: calling the coordination endpoint. + dialer := workspacesdk.NewWebsocketDialer(logger, u, &websocket.DialOptions{}) + _, err = dialer.Dial(ctx, nil) + + // Then: an error indicating a database issue is returned, to conditionalize the behavior of the caller. + require.ErrorIs(t, err, codersdk.ErrDatabaseNotReachable) +} diff --git a/provisionerd/provisionerd.go b/provisionerd/provisionerd.go index 8e9df48b9a1e8..6635495a2553a 100644 --- a/provisionerd/provisionerd.go +++ b/provisionerd/provisionerd.go @@ -20,12 +20,13 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/retry" + "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionerd/runner" sdkproto "github.com/coder/coder/v2/provisionersdk/proto" - "github.com/coder/retry" ) // Dialer represents the function to create a daemon client connection. @@ -290,7 +291,7 @@ func (p *Server) acquireLoop() { defer p.wg.Done() defer func() { close(p.acquireDoneCh) }() ctx := p.closeContext - for { + for retrier := retry.New(10*time.Millisecond, 1*time.Second); retrier.Wait(ctx); { if p.acquireExit() { return } @@ -299,7 +300,17 @@ func (p *Server) acquireLoop() { p.opts.Logger.Debug(ctx, "shut down before client (re) connected") return } - p.acquireAndRunOne(client) + err := p.acquireAndRunOne(client) + if err != nil && ctx.Err() == nil { // Only log if context is not done. + // Short-circuit: don't wait for the retry delay to exit, if required. + if p.acquireExit() { + return + } + p.opts.Logger.Warn(ctx, "failed to acquire job, retrying", slog.F("delay", fmt.Sprintf("%vms", retrier.Delay.Milliseconds())), slog.Error(err)) + } else { + // Reset the retrier after each successful acquisition. + retrier.Reset() + } } } @@ -318,7 +329,7 @@ func (p *Server) acquireExit() bool { return false } -func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) { +func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) error { ctx := p.closeContext p.opts.Logger.Debug(ctx, "start of acquireAndRunOne") job, err := p.acquireGraceful(client) @@ -327,15 +338,15 @@ func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) { if errors.Is(err, context.Canceled) || errors.Is(err, yamux.ErrSessionShutdown) || errors.Is(err, fasthttputil.ErrInmemoryListenerClosed) { - return + return err } p.opts.Logger.Warn(ctx, "provisionerd was unable to acquire job", slog.Error(err)) - return + return xerrors.Errorf("failed to acquire job: %w", err) } if job.JobId == "" { p.opts.Logger.Debug(ctx, "acquire job successfully canceled") - return + return nil } if len(job.TraceMetadata) > 0 { @@ -392,9 +403,9 @@ func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) { Error: fmt.Sprintf("failed to connect to provisioner: %s", resp.Error), }) if err != nil { - p.opts.Logger.Error(ctx, "provisioner job failed", slog.F("job_id", job.JobId), slog.Error(err)) + p.opts.Logger.Error(ctx, "failed to report provisioner job failed", slog.F("job_id", job.JobId), slog.Error(err)) } - return + return xerrors.Errorf("failed to report provisioner job failed: %w", err) } p.mutex.Lock() @@ -418,6 +429,7 @@ func (p *Server) acquireAndRunOne(client proto.DRPCProvisionerDaemonClient) { p.mutex.Lock() p.activeJob = nil p.mutex.Unlock() + return nil } // acquireGraceful attempts to acquire a job from the server, handling canceling the acquisition if we gracefully shut diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 24562dab7c04a..1768a207a4b41 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -591,6 +591,9 @@ export interface DangerousConfig { readonly allow_all_cors: boolean; } +// From codersdk/database.go +export const DatabaseNotReachable = "database not reachable"; + // From healthsdk/healthsdk.go export interface DatabaseReport extends BaseReport { readonly healthy: boolean; diff --git a/tailnet/controllers.go b/tailnet/controllers.go index 1d2a348b985f3..a257667fbe7a9 100644 --- a/tailnet/controllers.go +++ b/tailnet/controllers.go @@ -2,6 +2,7 @@ package tailnet import ( "context" + "errors" "fmt" "io" "maps" @@ -21,11 +22,12 @@ import ( "tailscale.com/util/dnsname" "cdr.dev/slog" + "github.com/coder/quartz" + "github.com/coder/retry" + "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet/proto" - "github.com/coder/quartz" - "github.com/coder/retry" ) // A Controller connects to the tailnet control plane, and then uses the control protocols to @@ -1381,6 +1383,14 @@ func (c *Controller) Run(ctx context.Context) { if xerrors.Is(err, context.Canceled) || xerrors.Is(err, context.DeadlineExceeded) { return } + + // If the database is unreachable by the control plane, there's not much we can do, so we'll just retry later. + if errors.Is(err, codersdk.ErrDatabaseNotReachable) { + c.logger.Warn(c.ctx, "control plane lost connection to database, retrying", + slog.Error(err), slog.F("delay", fmt.Sprintf("%vms", retrier.Delay.Milliseconds()))) + continue + } + errF := slog.Error(err) var sdkErr *codersdk.Error if xerrors.As(err, &sdkErr) { From 95f03c561facf2f48621cd9f304dccd2d473cdb9 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Tue, 15 Apr 2025 11:39:23 +0200 Subject: [PATCH 091/384] fix: increase context timeout in `TestProvisionerd/MaliciousTar` to avoid flake (#17400) Fixing a flake seen here: https://github.com/coder/coder/actions/runs/14465389766/job/40566518088 Signed-off-by: Danny Kopping --- provisionerd/provisionerd_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go index fae8d073fbfd0..8d5ba1621b8b7 100644 --- a/provisionerd/provisionerd_test.go +++ b/provisionerd/provisionerd_test.go @@ -174,7 +174,7 @@ func TestProvisionerd(t *testing.T) { }, provisionerd.LocalProvisioners{ "someprovisioner": createProvisionerClient(t, done, provisionerTestServer{}), }) - require.Condition(t, closedWithin(completeChan, testutil.WaitShort)) + require.Condition(t, closedWithin(completeChan, testutil.WaitMedium)) require.NoError(t, closer.Close()) }) From 979687c37fded8fd7f73a2cb3e36190902be3522 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 15 Apr 2025 10:47:42 +0100 Subject: [PATCH 092/384] chore(codersdk): deprecate WorkspaceAppStatus.{NeedsUserAttention,Icon} (#17358) https://github.com/coder/coder/pull/17163 introduced the `workspace_app_statuses` table. Two of these fields (`needs_user_attention`, `icon`) turned out to be surplus to requirements. - Removes columns `needs_user_attention` and `icon` from `workspace_app_statuses` - Marks the corresponding fields of `codersdk.WorkspaceAppStatus` as deprecated. --- coderd/apidoc/docs.go | 5 ++- coderd/apidoc/swagger.json | 5 ++- coderd/database/db2sdk/db2sdk.go | 18 ++++----- coderd/database/dbmem/dbmem.go | 18 ++++----- coderd/database/dump.sql | 4 +- ..._workspace_app_status_drop_fields.down.sql | 3 ++ ...17_workspace_app_status_drop_fields.up.sql | 3 ++ coderd/database/models.go | 18 ++++----- coderd/database/queries.sql.go | 38 +++++++----------- coderd/database/queries/workspaceapps.sql | 6 +-- coderd/workspaceagents.go | 5 --- coderd/workspaceagents_test.go | 7 +++- codersdk/agentsdk/agentsdk.go | 14 ++++--- codersdk/toolsdk/toolsdk.go | 20 +++------- codersdk/toolsdk/toolsdk_test.go | 1 - codersdk/workspaceapps.go | 20 ++++++---- docs/reference/api/builds.md | 8 ++-- docs/reference/api/schemas.md | 40 +++++++++---------- docs/reference/api/templates.md | 8 ++-- site/src/api/typesGenerated.ts | 2 +- site/src/testHelpers/entities.ts | 5 ++- 21 files changed, 119 insertions(+), 129 deletions(-) create mode 100644 coderd/database/migrations/000317_workspace_app_status_drop_fields.down.sql create mode 100644 coderd/database/migrations/000317_workspace_app_status_drop_fields.up.sql diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6ad75b2d65a26..04b0a93cfb12e 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -10242,12 +10242,14 @@ const docTemplate = `{ "type": "string" }, "icon": { + "description": "Deprecated: this field is unused and will be removed in a future version.", "type": "string" }, "message": { "type": "string" }, "needs_user_attention": { + "description": "Deprecated: this field is unused and will be removed in a future version.", "type": "boolean" }, "state": { @@ -16925,7 +16927,7 @@ const docTemplate = `{ "format": "date-time" }, "icon": { - "description": "Icon is an external URL to an icon that will be rendered in the UI.", + "description": "Deprecated: This field is unused and will be removed in a future version.\nIcon is an external URL to an icon that will be rendered in the UI.", "type": "string" }, "id": { @@ -16936,6 +16938,7 @@ const docTemplate = `{ "type": "string" }, "needs_user_attention": { + "description": "Deprecated: This field is unused and will be removed in a future version.\nNeedsUserAttention specifies whether the status needs user attention.", "type": "boolean" }, "state": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 77758feb75c70..1cea2c58f7255 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -9079,12 +9079,14 @@ "type": "string" }, "icon": { + "description": "Deprecated: this field is unused and will be removed in a future version.", "type": "string" }, "message": { "type": "string" }, "needs_user_attention": { + "description": "Deprecated: this field is unused and will be removed in a future version.", "type": "boolean" }, "state": { @@ -15444,7 +15446,7 @@ "format": "date-time" }, "icon": { - "description": "Icon is an external URL to an icon that will be rendered in the UI.", + "description": "Deprecated: This field is unused and will be removed in a future version.\nIcon is an external URL to an icon that will be rendered in the UI.", "type": "string" }, "id": { @@ -15455,6 +15457,7 @@ "type": "string" }, "needs_user_attention": { + "description": "Deprecated: This field is unused and will be removed in a future version.\nNeedsUserAttention specifies whether the status needs user attention.", "type": "boolean" }, "state": { diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index e6d529ddadbfe..7efcd009c6ef9 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -537,16 +537,14 @@ func WorkspaceAppStatuses(statuses []database.WorkspaceAppStatus) []codersdk.Wor func WorkspaceAppStatus(status database.WorkspaceAppStatus) codersdk.WorkspaceAppStatus { return codersdk.WorkspaceAppStatus{ - ID: status.ID, - CreatedAt: status.CreatedAt, - WorkspaceID: status.WorkspaceID, - AgentID: status.AgentID, - AppID: status.AppID, - NeedsUserAttention: status.NeedsUserAttention, - URI: status.Uri.String, - Icon: status.Icon.String, - Message: status.Message, - State: codersdk.WorkspaceAppStatusState(status.State), + ID: status.ID, + CreatedAt: status.CreatedAt, + WorkspaceID: status.WorkspaceID, + AgentID: status.AgentID, + AppID: status.AppID, + URI: status.Uri.String, + Message: status.Message, + State: codersdk.WorkspaceAppStatusState(status.State), } } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 7fa583489a32e..ed9f098c00e3c 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9764,16 +9764,14 @@ func (q *FakeQuerier) InsertWorkspaceAppStatus(_ context.Context, arg database.I defer q.mutex.Unlock() status := database.WorkspaceAppStatus{ - ID: arg.ID, - CreatedAt: arg.CreatedAt, - WorkspaceID: arg.WorkspaceID, - AgentID: arg.AgentID, - AppID: arg.AppID, - NeedsUserAttention: arg.NeedsUserAttention, - State: arg.State, - Message: arg.Message, - Uri: arg.Uri, - Icon: arg.Icon, + ID: arg.ID, + CreatedAt: arg.CreatedAt, + WorkspaceID: arg.WorkspaceID, + AgentID: arg.AgentID, + AppID: arg.AppID, + State: arg.State, + Message: arg.Message, + Uri: arg.Uri, } q.workspaceAppStatuses = append(q.workspaceAppStatuses, status) return status, nil diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 8d9ac8186be85..83d998b2b9a3e 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1911,10 +1911,8 @@ CREATE TABLE workspace_app_statuses ( app_id uuid NOT NULL, workspace_id uuid NOT NULL, state workspace_app_status_state NOT NULL, - needs_user_attention boolean NOT NULL, message text NOT NULL, - uri text, - icon text + uri text ); CREATE TABLE workspace_apps ( diff --git a/coderd/database/migrations/000317_workspace_app_status_drop_fields.down.sql b/coderd/database/migrations/000317_workspace_app_status_drop_fields.down.sql new file mode 100644 index 0000000000000..169cafe5830db --- /dev/null +++ b/coderd/database/migrations/000317_workspace_app_status_drop_fields.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE ONLY workspace_app_statuses + ADD COLUMN IF NOT EXISTS needs_user_attention BOOLEAN NOT NULL DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS icon TEXT; diff --git a/coderd/database/migrations/000317_workspace_app_status_drop_fields.up.sql b/coderd/database/migrations/000317_workspace_app_status_drop_fields.up.sql new file mode 100644 index 0000000000000..135f89d7c4f3c --- /dev/null +++ b/coderd/database/migrations/000317_workspace_app_status_drop_fields.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE ONLY workspace_app_statuses + DROP COLUMN IF EXISTS needs_user_attention, + DROP COLUMN IF EXISTS icon; diff --git a/coderd/database/models.go b/coderd/database/models.go index 208b11cb26e71..f817ff2712d54 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3579,16 +3579,14 @@ type WorkspaceAppStat struct { } type WorkspaceAppStatus struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - AppID uuid.UUID `db:"app_id" json:"app_id"` - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - State WorkspaceAppStatusState `db:"state" json:"state"` - NeedsUserAttention bool `db:"needs_user_attention" json:"needs_user_attention"` - Message string `db:"message" json:"message"` - Uri sql.NullString `db:"uri" json:"uri"` - Icon sql.NullString `db:"icon" json:"icon"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + AppID uuid.UUID `db:"app_id" json:"app_id"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + State WorkspaceAppStatusState `db:"state" json:"state"` + Message string `db:"message" json:"message"` + Uri sql.NullString `db:"uri" json:"uri"` } // Joins in the username + avatar url of the initiated by user. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0d5fa1bb7f060..ab5f27892749f 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -15598,8 +15598,8 @@ func (q *sqlQuerier) UpsertWorkspaceAppAuditSession(ctx context.Context, arg Ups const getLatestWorkspaceAppStatusesByWorkspaceIDs = `-- name: GetLatestWorkspaceAppStatusesByWorkspaceIDs :many SELECT DISTINCT ON (workspace_id) - id, created_at, agent_id, app_id, workspace_id, state, needs_user_attention, message, uri, icon -FROM workspace_app_statuses + id, created_at, agent_id, app_id, workspace_id, state, message, uri +FROM workspace_app_statuses WHERE workspace_id = ANY($1 :: uuid[]) ORDER BY workspace_id, created_at DESC ` @@ -15620,10 +15620,8 @@ func (q *sqlQuerier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(ctx context.Con &i.AppID, &i.WorkspaceID, &i.State, - &i.NeedsUserAttention, &i.Message, &i.Uri, - &i.Icon, ); err != nil { return nil, err } @@ -15674,7 +15672,7 @@ func (q *sqlQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg Ge } const getWorkspaceAppStatusesByAppIDs = `-- name: GetWorkspaceAppStatusesByAppIDs :many -SELECT id, created_at, agent_id, app_id, workspace_id, state, needs_user_attention, message, uri, icon FROM workspace_app_statuses WHERE app_id = ANY($1 :: uuid [ ]) +SELECT id, created_at, agent_id, app_id, workspace_id, state, message, uri FROM workspace_app_statuses WHERE app_id = ANY($1 :: uuid [ ]) ` func (q *sqlQuerier) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAppStatus, error) { @@ -15693,10 +15691,8 @@ func (q *sqlQuerier) GetWorkspaceAppStatusesByAppIDs(ctx context.Context, ids [] &i.AppID, &i.WorkspaceID, &i.State, - &i.NeedsUserAttention, &i.Message, &i.Uri, - &i.Icon, ); err != nil { return nil, err } @@ -15942,22 +15938,20 @@ func (q *sqlQuerier) InsertWorkspaceApp(ctx context.Context, arg InsertWorkspace } const insertWorkspaceAppStatus = `-- name: InsertWorkspaceAppStatus :one -INSERT INTO workspace_app_statuses (id, created_at, workspace_id, agent_id, app_id, state, message, needs_user_attention, uri, icon) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) -RETURNING id, created_at, agent_id, app_id, workspace_id, state, needs_user_attention, message, uri, icon +INSERT INTO workspace_app_statuses (id, created_at, workspace_id, agent_id, app_id, state, message, uri) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) +RETURNING id, created_at, agent_id, app_id, workspace_id, state, message, uri ` type InsertWorkspaceAppStatusParams struct { - ID uuid.UUID `db:"id" json:"id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` - AgentID uuid.UUID `db:"agent_id" json:"agent_id"` - AppID uuid.UUID `db:"app_id" json:"app_id"` - State WorkspaceAppStatusState `db:"state" json:"state"` - Message string `db:"message" json:"message"` - NeedsUserAttention bool `db:"needs_user_attention" json:"needs_user_attention"` - Uri sql.NullString `db:"uri" json:"uri"` - Icon sql.NullString `db:"icon" json:"icon"` + ID uuid.UUID `db:"id" json:"id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"` + AgentID uuid.UUID `db:"agent_id" json:"agent_id"` + AppID uuid.UUID `db:"app_id" json:"app_id"` + State WorkspaceAppStatusState `db:"state" json:"state"` + Message string `db:"message" json:"message"` + Uri sql.NullString `db:"uri" json:"uri"` } func (q *sqlQuerier) InsertWorkspaceAppStatus(ctx context.Context, arg InsertWorkspaceAppStatusParams) (WorkspaceAppStatus, error) { @@ -15969,9 +15963,7 @@ func (q *sqlQuerier) InsertWorkspaceAppStatus(ctx context.Context, arg InsertWor arg.AppID, arg.State, arg.Message, - arg.NeedsUserAttention, arg.Uri, - arg.Icon, ) var i WorkspaceAppStatus err := row.Scan( @@ -15981,10 +15973,8 @@ func (q *sqlQuerier) InsertWorkspaceAppStatus(ctx context.Context, arg InsertWor &i.AppID, &i.WorkspaceID, &i.State, - &i.NeedsUserAttention, &i.Message, &i.Uri, - &i.Icon, ) return i, err } diff --git a/coderd/database/queries/workspaceapps.sql b/coderd/database/queries/workspaceapps.sql index e402ee1402922..cd1cddb454b88 100644 --- a/coderd/database/queries/workspaceapps.sql +++ b/coderd/database/queries/workspaceapps.sql @@ -44,8 +44,8 @@ WHERE id = $1; -- name: InsertWorkspaceAppStatus :one -INSERT INTO workspace_app_statuses (id, created_at, workspace_id, agent_id, app_id, state, message, needs_user_attention, uri, icon) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) +INSERT INTO workspace_app_statuses (id, created_at, workspace_id, agent_id, app_id, state, message, uri) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; -- name: GetWorkspaceAppStatusesByAppIDs :many @@ -54,6 +54,6 @@ SELECT * FROM workspace_app_statuses WHERE app_id = ANY(@ids :: uuid [ ]); -- name: GetLatestWorkspaceAppStatusesByWorkspaceIDs :many SELECT DISTINCT ON (workspace_id) * -FROM workspace_app_statuses +FROM workspace_app_statuses WHERE workspace_id = ANY(@ids :: uuid[]) ORDER BY workspace_id, created_at DESC; diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index 4af12fa228713..cf47514c7f0eb 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -366,11 +366,6 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req String: req.URI, Valid: req.URI != "", }, - Icon: sql.NullString{ - String: req.Icon, - Valid: req.Icon != "", - }, - NeedsUserAttention: req.NeedsUserAttention, }) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index a8fe7718f4385..de935176f22ac 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -366,8 +366,10 @@ func TestWorkspaceAgentAppStatus(t *testing.T) { AppSlug: "vscode", Message: "testing", URI: "https://example.com", - Icon: "https://example.com/icon.png", State: codersdk.WorkspaceAppStatusStateComplete, + // Ensure deprecated fields are ignored. + Icon: "https://example.com/icon.png", + NeedsUserAttention: true, }) require.NoError(t, err) @@ -376,6 +378,9 @@ func TestWorkspaceAgentAppStatus(t *testing.T) { agent, err := client.WorkspaceAgent(ctx, workspace.LatestBuild.Resources[0].Agents[0].ID) require.NoError(t, err) require.Len(t, agent.Apps[0].Statuses, 1) + // Deprecated fields should be ignored. + require.Empty(t, agent.Apps[0].Statuses[0].Icon) + require.False(t, agent.Apps[0].Statuses[0].NeedsUserAttention) }) } diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 4f7d0a8baef31..109d14b84d050 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -583,12 +583,14 @@ func (c *Client) PatchLogs(ctx context.Context, req PatchLogs) error { // PatchAppStatus updates the status of a workspace app. type PatchAppStatus struct { - AppSlug string `json:"app_slug"` - NeedsUserAttention bool `json:"needs_user_attention"` - State codersdk.WorkspaceAppStatusState `json:"state"` - Message string `json:"message"` - URI string `json:"uri"` - Icon string `json:"icon"` + AppSlug string `json:"app_slug"` + State codersdk.WorkspaceAppStatusState `json:"state"` + Message string `json:"message"` + URI string `json:"uri"` + // Deprecated: this field is unused and will be removed in a future version. + Icon string `json:"icon"` + // Deprecated: this field is unused and will be removed in a future version. + NeedsUserAttention bool `json:"needs_user_attention"` } func (c *Client) PatchAppStatus(ctx context.Context, req PatchAppStatus) error { diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index 6cadbe611f335..73dee8e748575 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -67,10 +67,6 @@ var ( "type": "string", "description": "A link to a relevant resource, such as a PR or issue.", }, - "emoji": map[string]any{ - "type": "string", - "description": "An emoji that visually represents your current progress. Choose an emoji that helps the user understand your current status at a glance.", - }, "state": map[string]any{ "type": "string", "description": "The state of your task. This can be one of the following: working, complete, or failure. Select the state that best represents your current progress.", @@ -81,7 +77,7 @@ var ( }, }, }, - Required: []string{"summary", "link", "emoji", "state"}, + Required: []string{"summary", "link", "state"}, }, }, Handler: func(ctx context.Context, args map[string]any) (string, error) { @@ -104,22 +100,16 @@ var ( if !ok { return "", xerrors.New("link must be a string") } - emoji, ok := args["emoji"].(string) - if !ok { - return "", xerrors.New("emoji must be a string") - } state, ok := args["state"].(string) if !ok { return "", xerrors.New("state must be a string") } if err := agentClient.PatchAppStatus(ctx, agentsdk.PatchAppStatus{ - AppSlug: appSlug, - Message: summary, - URI: link, - Icon: emoji, - NeedsUserAttention: false, // deprecated, to be removed later - State: codersdk.WorkspaceAppStatusState(state), + AppSlug: appSlug, + Message: summary, + URI: link, + State: codersdk.WorkspaceAppStatusState(state), }); err != nil { return "", err } diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index aca4045f36e8e..1504e956f6bd4 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -75,7 +75,6 @@ func TestTools(t *testing.T) { "summary": "test summary", "state": "complete", "link": "https://example.com", - "emoji": "✅", }) require.NoError(t, err) }) diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go index ec5a7c4414f76..a55db1911101e 100644 --- a/codersdk/workspaceapps.go +++ b/codersdk/workspaceapps.go @@ -100,18 +100,22 @@ type Healthcheck struct { } type WorkspaceAppStatus struct { - ID uuid.UUID `json:"id" format:"uuid"` - CreatedAt time.Time `json:"created_at" format:"date-time"` - WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` - AgentID uuid.UUID `json:"agent_id" format:"uuid"` - AppID uuid.UUID `json:"app_id" format:"uuid"` - State WorkspaceAppStatusState `json:"state"` - NeedsUserAttention bool `json:"needs_user_attention"` - Message string `json:"message"` + ID uuid.UUID `json:"id" format:"uuid"` + CreatedAt time.Time `json:"created_at" format:"date-time"` + WorkspaceID uuid.UUID `json:"workspace_id" format:"uuid"` + AgentID uuid.UUID `json:"agent_id" format:"uuid"` + AppID uuid.UUID `json:"app_id" format:"uuid"` + State WorkspaceAppStatusState `json:"state"` + Message string `json:"message"` // URI is the URI of the resource that the status is for. // e.g. https://github.com/org/repo/pull/123 // e.g. file:///path/to/file URI string `json:"uri"` + + // Deprecated: This field is unused and will be removed in a future version. // Icon is an external URL to an icon that will be rendered in the UI. Icon string `json:"icon"` + // Deprecated: This field is unused and will be removed in a future version. + // NeedsUserAttention specifies whether the status needs user attention. + NeedsUserAttention bool `json:"needs_user_attention"` } diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 1e5ff95026eaf..1f795c3d7d313 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -818,10 +818,10 @@ Status Code **200** | `»»»» agent_id` | string(uuid) | false | | | | `»»»» app_id` | string(uuid) | false | | | | `»»»» created_at` | string(date-time) | false | | | -| `»»»» icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | | `»»»» id` | string(uuid) | false | | | | `»»»» message` | string | false | | | -| `»»»» needs_user_attention` | boolean | false | | | +| `»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | | `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | | `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | | `»»»» workspace_id` | string(uuid) | false | | | @@ -1532,10 +1532,10 @@ Status Code **200** | `»»»»» agent_id` | string(uuid) | false | | | | `»»»»» app_id` | string(uuid) | false | | | | `»»»»» created_at` | string(date-time) | false | | | -| `»»»»» icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | | `»»»»» id` | string(uuid) | false | | | | `»»»»» message` | string | false | | | -| `»»»»» needs_user_attention` | boolean | false | | | +| `»»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | | `»»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | | `»»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | | `»»»»» workspace_id` | string(uuid) | false | | | diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index e5fa809ef23f0..85b6e65a545aa 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -133,14 +133,14 @@ ### Properties -| Name | Type | Required | Restrictions | Description | -|------------------------|----------------------------------------------------------------------|----------|--------------|-------------| -| `app_slug` | string | false | | | -| `icon` | string | false | | | -| `message` | string | false | | | -| `needs_user_attention` | boolean | false | | | -| `state` | [codersdk.WorkspaceAppStatusState](#codersdkworkspaceappstatusstate) | false | | | -| `uri` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------|----------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------| +| `app_slug` | string | false | | | +| `icon` | string | false | | Deprecated: this field is unused and will be removed in a future version. | +| `message` | string | false | | | +| `needs_user_attention` | boolean | false | | Deprecated: this field is unused and will be removed in a future version. | +| `state` | [codersdk.WorkspaceAppStatusState](#codersdkworkspaceappstatusstate) | false | | | +| `uri` | string | false | | | ## agentsdk.PatchLogs @@ -8499,18 +8499,18 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -|------------------------|----------------------------------------------------------------------|----------|--------------|----------------------------------------------------------------------------------------------------------------------------| -| `agent_id` | string | false | | | -| `app_id` | string | false | | | -| `created_at` | string | false | | | -| `icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | -| `id` | string | false | | | -| `message` | string | false | | | -| `needs_user_attention` | boolean | false | | | -| `state` | [codersdk.WorkspaceAppStatusState](#codersdkworkspaceappstatusstate) | false | | | -| `uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | -| `workspace_id` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|------------------------|----------------------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| `agent_id` | string | false | | | +| `app_id` | string | false | | | +| `created_at` | string | false | | | +| `icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | +| `id` | string | false | | | +| `message` | string | false | | | +| `needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | +| `state` | [codersdk.WorkspaceAppStatusState](#codersdkworkspaceappstatusstate) | false | | | +| `uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | +| `workspace_id` | string | false | | | ## codersdk.WorkspaceAppStatusState diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index f48a9482fa695..0f21cfccac670 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -2429,10 +2429,10 @@ Status Code **200** | `»»»» agent_id` | string(uuid) | false | | | | `»»»» app_id` | string(uuid) | false | | | | `»»»» created_at` | string(date-time) | false | | | -| `»»»» icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | | `»»»» id` | string(uuid) | false | | | | `»»»» message` | string | false | | | -| `»»»» needs_user_attention` | boolean | false | | | +| `»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | | `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | | `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | | `»»»» workspace_id` | string(uuid) | false | | | @@ -2976,10 +2976,10 @@ Status Code **200** | `»»»» agent_id` | string(uuid) | false | | | | `»»»» app_id` | string(uuid) | false | | | | `»»»» created_at` | string(date-time) | false | | | -| `»»»» icon` | string | false | | Icon is an external URL to an icon that will be rendered in the UI. | +| `»»»» icon` | string | false | | Deprecated: This field is unused and will be removed in a future version. Icon is an external URL to an icon that will be rendered in the UI. | | `»»»» id` | string(uuid) | false | | | | `»»»» message` | string | false | | | -| `»»»» needs_user_attention` | boolean | false | | | +| `»»»» needs_user_attention` | boolean | false | | Deprecated: This field is unused and will be removed in a future version. NeedsUserAttention specifies whether the status needs user attention. | | `»»»» state` | [codersdk.WorkspaceAppStatusState](schemas.md#codersdkworkspaceappstatusstate) | false | | | | `»»»» uri` | string | false | | Uri is the URI of the resource that the status is for. e.g. https://github.com/org/repo/pull/123 e.g. file:///path/to/file | | `»»»» workspace_id` | string(uuid) | false | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 1768a207a4b41..c3109139ba300 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3448,10 +3448,10 @@ export interface WorkspaceAppStatus { readonly agent_id: string; readonly app_id: string; readonly state: WorkspaceAppStatusState; - readonly needs_user_attention: boolean; readonly message: string; readonly uri: string; readonly icon: string; + readonly needs_user_attention: boolean; } // From codersdk/workspaceapps.go diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index a434c56200a87..8b19905286a22 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -984,11 +984,12 @@ export const MockWorkspaceAppStatus: TypesGen.WorkspaceAppStatus = { agent_id: "test-workspace-agent", workspace_id: "test-workspace", app_id: MockWorkspaceApp.id, - needs_user_attention: false, - icon: "/emojis/1f957.png", uri: "https://github.com/coder/coder/pull/1234", message: "Your competitors page is completed!", state: "complete", + // Deprecated fields + needs_user_attention: false, + icon: "", }; export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = { From 06d39151dc1aa55fddb846cbfde9ff526769c3e0 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:27:23 +0200 Subject: [PATCH 093/384] feat: extend request logs with auth & DB info (#17304) Closes #16903 --- Makefile | 8 +- coderd/coderd.go | 3 +- coderd/database/dbauthz/dbauthz.go | 34 +++-- coderd/database/queries.sql.go | 6 +- coderd/database/queries/users.sql | 4 +- coderd/httpmw/apikey.go | 2 + coderd/httpmw/{ => loggermw}/logger.go | 78 +++++++++- .../{ => loggermw}/logger_internal_test.go | 141 +++++++++++++++++- .../{ => loggermw}/loggermock/loggermock.go | 15 +- coderd/httpmw/workspaceagentparam.go | 11 ++ coderd/httpmw/workspaceparam.go | 15 ++ coderd/inboxnotifications.go | 3 +- coderd/provisionerjobs.go | 3 +- coderd/provisionerjobs_internal_test.go | 6 +- coderd/rbac/authz.go | 25 ++++ coderd/workspaceagents.go | 7 +- enterprise/coderd/provisionerdaemons.go | 3 +- enterprise/wsproxy/wsproxy.go | 3 +- tailnet/test/integration/integration.go | 4 +- 19 files changed, 336 insertions(+), 35 deletions(-) rename coderd/httpmw/{ => loggermw}/logger.go (62%) rename coderd/httpmw/{ => loggermw}/logger_internal_test.go (56%) rename coderd/httpmw/{ => loggermw}/loggermock/loggermock.go (77%) diff --git a/Makefile b/Makefile index 6486f5cbed5fa..4ada1cd6d488c 100644 --- a/Makefile +++ b/Makefile @@ -582,7 +582,7 @@ GEN_FILES := \ coderd/database/pubsub/psmock/psmock.go \ agent/agentcontainers/acmock/acmock.go \ agent/agentcontainers/dcspec/dcspec_gen.go \ - coderd/httpmw/loggermock/loggermock.go + coderd/httpmw/loggermw/loggermock/loggermock.go # all gen targets should be added here and to gen/mark-fresh gen: gen/db gen/golden-files $(GEN_FILES) @@ -631,7 +631,7 @@ gen/mark-fresh: coderd/database/pubsub/psmock/psmock.go \ agent/agentcontainers/acmock/acmock.go \ agent/agentcontainers/dcspec/dcspec_gen.go \ - coderd/httpmw/loggermock/loggermock.go \ + coderd/httpmw/loggermw/loggermock/loggermock.go \ " for file in $$files; do @@ -671,8 +671,8 @@ agent/agentcontainers/acmock/acmock.go: agent/agentcontainers/containers.go go generate ./agent/agentcontainers/acmock/ touch "$@" -coderd/httpmw/loggermock/loggermock.go: coderd/httpmw/logger.go - go generate ./coderd/httpmw/loggermock/ +coderd/httpmw/loggermw/loggermock/loggermock.go: coderd/httpmw/loggermw/logger.go + go generate ./coderd/httpmw/loggermw/loggermock/ touch "$@" agent/agentcontainers/dcspec/dcspec_gen.go: \ diff --git a/coderd/coderd.go b/coderd/coderd.go index a5886061ac4dc..d8e9d96ff7106 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -65,6 +65,7 @@ import ( "github.com/coder/coder/v2/coderd/healthcheck/derphealth" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/metricscache" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/portsharing" @@ -811,7 +812,7 @@ func New(options *Options) *API { tracing.Middleware(api.TracerProvider), httpmw.AttachRequestID, httpmw.ExtractRealIP(api.RealIPConfig), - httpmw.Logger(api.Logger), + loggermw.Logger(api.Logger), singleSlashMW, rolestore.CustomRoleMW, prometheusMW, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index b9eb8b05e171e..ceb5ba7f2a15a 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -25,6 +25,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/provisionersdk" @@ -163,6 +164,7 @@ func ActorFromContext(ctx context.Context) (rbac.Subject, bool) { var ( subjectProvisionerd = rbac.Subject{ + Type: rbac.SubjectTypeProvisionerd, FriendlyName: "Provisioner Daemon", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -197,6 +199,7 @@ var ( }.WithCachedASTValue() subjectAutostart = rbac.Subject{ + Type: rbac.SubjectTypeAutostart, FriendlyName: "Autostart", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -220,6 +223,7 @@ var ( // See unhanger package. subjectHangDetector = rbac.Subject{ + Type: rbac.SubjectTypeHangDetector, FriendlyName: "Hang Detector", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -240,6 +244,7 @@ var ( // See cryptokeys package. subjectCryptoKeyRotator = rbac.Subject{ + Type: rbac.SubjectTypeCryptoKeyRotator, FriendlyName: "Crypto Key Rotator", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -258,6 +263,7 @@ var ( // See cryptokeys package. subjectCryptoKeyReader = rbac.Subject{ + Type: rbac.SubjectTypeCryptoKeyReader, FriendlyName: "Crypto Key Reader", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -275,6 +281,7 @@ var ( }.WithCachedASTValue() subjectNotifier = rbac.Subject{ + Type: rbac.SubjectTypeNotifier, FriendlyName: "Notifier", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -295,6 +302,7 @@ var ( }.WithCachedASTValue() subjectResourceMonitor = rbac.Subject{ + Type: rbac.SubjectTypeResourceMonitor, FriendlyName: "Resource Monitor", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -313,6 +321,7 @@ var ( }.WithCachedASTValue() subjectSystemRestricted = rbac.Subject{ + Type: rbac.SubjectTypeSystemRestricted, FriendlyName: "System", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -347,6 +356,7 @@ var ( }.WithCachedASTValue() subjectSystemReadProvisionerDaemons = rbac.Subject{ + Type: rbac.SubjectTypeSystemReadProvisionerDaemons, FriendlyName: "Provisioner Daemons Reader", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -364,6 +374,7 @@ var ( }.WithCachedASTValue() subjectPrebuildsOrchestrator = rbac.Subject{ + Type: rbac.SubjectTypePrebuildsOrchestrator, FriendlyName: "Prebuilds Orchestrator", ID: prebuilds.SystemUserID.String(), Roles: rbac.Roles([]rbac.Role{ @@ -388,59 +399,59 @@ var ( // AsProvisionerd returns a context with an actor that has permissions required // for provisionerd to function. func AsProvisionerd(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectProvisionerd) + return As(ctx, subjectProvisionerd) } // AsAutostart returns a context with an actor that has permissions required // for autostart to function. func AsAutostart(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectAutostart) + return As(ctx, subjectAutostart) } // AsHangDetector returns a context with an actor that has permissions required // for unhanger.Detector to function. func AsHangDetector(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectHangDetector) + return As(ctx, subjectHangDetector) } // AsKeyRotator returns a context with an actor that has permissions required for rotating crypto keys. func AsKeyRotator(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyRotator) + return As(ctx, subjectCryptoKeyRotator) } // AsKeyReader returns a context with an actor that has permissions required for reading crypto keys. func AsKeyReader(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyReader) + return As(ctx, subjectCryptoKeyReader) } // AsNotifier returns a context with an actor that has permissions required for // creating/reading/updating/deleting notifications. func AsNotifier(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectNotifier) + return As(ctx, subjectNotifier) } // AsResourceMonitor returns a context with an actor that has permissions required for // updating resource monitors. func AsResourceMonitor(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectResourceMonitor) + return As(ctx, subjectResourceMonitor) } // AsSystemRestricted returns a context with an actor that has permissions // required for various system operations (login, logout, metrics cache). func AsSystemRestricted(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectSystemRestricted) + return As(ctx, subjectSystemRestricted) } // AsSystemReadProvisionerDaemons returns a context with an actor that has permissions // to read provisioner daemons. func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectSystemReadProvisionerDaemons) + return As(ctx, subjectSystemReadProvisionerDaemons) } // AsPrebuildsOrchestrator returns a context with an actor that has permissions // to read orchestrator workspace prebuilds. func AsPrebuildsOrchestrator(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectPrebuildsOrchestrator) + return As(ctx, subjectPrebuildsOrchestrator) } var AsRemoveActor = rbac.Subject{ @@ -458,6 +469,9 @@ func As(ctx context.Context, actor rbac.Subject) context.Context { // should be removed from the context. return context.WithValue(ctx, authContextKey{}, nil) } + if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { + rlogger.WithAuthContext(actor) + } return context.WithValue(ctx, authContextKey{}, actor) } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ab5f27892749f..c1738589d37ae 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12064,10 +12064,10 @@ func (q *sqlQuerier) GetActiveUserCount(ctx context.Context, includeSystem bool) const getAuthorizationUserRoles = `-- name: GetAuthorizationUserRoles :one SELECT - -- username is returned just to help for logging purposes + -- username and email are returned just to help for logging purposes -- status is used to enforce 'suspended' users, as all roles are ignored -- when suspended. - id, username, status, + id, username, status, email, -- All user roles, including their org roles. array_cat( -- All users are members @@ -12108,6 +12108,7 @@ type GetAuthorizationUserRolesRow struct { ID uuid.UUID `db:"id" json:"id"` Username string `db:"username" json:"username"` Status UserStatus `db:"status" json:"status"` + Email string `db:"email" json:"email"` Roles []string `db:"roles" json:"roles"` Groups []string `db:"groups" json:"groups"` } @@ -12121,6 +12122,7 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid. &i.ID, &i.Username, &i.Status, + &i.Email, pq.Array(&i.Roles), pq.Array(&i.Groups), ) diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 8757b377728a3..eece2f96512ea 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -300,10 +300,10 @@ WHERE -- This function returns roles for authorization purposes. Implied member roles -- are included. SELECT - -- username is returned just to help for logging purposes + -- username and email are returned just to help for logging purposes -- status is used to enforce 'suspended' users, as all roles are ignored -- when suspended. - id, username, status, + id, username, status, email, -- All user roles, including their org roles. array_cat( -- All users are members diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 1574affa30b65..d614b37a3d897 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -465,7 +465,9 @@ func UserRBACSubject(ctx context.Context, db database.Store, userID uuid.UUID, s } actor := rbac.Subject{ + Type: rbac.SubjectTypeUser, FriendlyName: roles.Username, + Email: roles.Email, ID: userID.String(), Roles: rbacRoles, Groups: roles.Groups, diff --git a/coderd/httpmw/logger.go b/coderd/httpmw/loggermw/logger.go similarity index 62% rename from coderd/httpmw/logger.go rename to coderd/httpmw/loggermw/logger.go index 0da964407b3e4..9eeb07a5f10e5 100644 --- a/coderd/httpmw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -1,13 +1,17 @@ -package httpmw +package loggermw import ( "context" "fmt" "net/http" + "sync" "time" + "github.com/go-chi/chi/v5" + "cdr.dev/slog" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/tracing" ) @@ -62,6 +66,7 @@ func Logger(log slog.Logger) func(next http.Handler) http.Handler { type RequestLogger interface { WithFields(fields ...slog.Field) WriteLog(ctx context.Context, status int) + WithAuthContext(actor rbac.Subject) } type SlogRequestLogger struct { @@ -69,6 +74,9 @@ type SlogRequestLogger struct { written bool message string start time.Time + // Protects actors map for concurrent writes. + mu sync.RWMutex + actors map[rbac.SubjectType]rbac.Subject } var _ RequestLogger = &SlogRequestLogger{} @@ -79,6 +87,7 @@ func NewRequestLogger(log slog.Logger, message string, start time.Time) RequestL written: false, message: message, start: start, + actors: make(map[rbac.SubjectType]rbac.Subject), } } @@ -86,6 +95,52 @@ func (c *SlogRequestLogger) WithFields(fields ...slog.Field) { c.log = c.log.With(fields...) } +func (c *SlogRequestLogger) WithAuthContext(actor rbac.Subject) { + c.mu.Lock() + defer c.mu.Unlock() + c.actors[actor.Type] = actor +} + +func (c *SlogRequestLogger) addAuthContextFields() { + c.mu.RLock() + defer c.mu.RUnlock() + + usr, ok := c.actors[rbac.SubjectTypeUser] + if ok { + c.log = c.log.With( + slog.F("requestor_id", usr.ID), + slog.F("requestor_name", usr.FriendlyName), + slog.F("requestor_email", usr.Email), + ) + } else { + // If there is no user, we log the requestor name for the first + // actor in a defined order. + for _, v := range actorLogOrder { + subj, ok := c.actors[v] + if !ok { + continue + } + c.log = c.log.With( + slog.F("requestor_name", subj.FriendlyName), + ) + break + } + } +} + +var actorLogOrder = []rbac.SubjectType{ + rbac.SubjectTypeAutostart, + rbac.SubjectTypeCryptoKeyReader, + rbac.SubjectTypeCryptoKeyRotator, + rbac.SubjectTypeHangDetector, + rbac.SubjectTypeNotifier, + rbac.SubjectTypePrebuildsOrchestrator, + rbac.SubjectTypeProvisionerd, + rbac.SubjectTypeResourceMonitor, + rbac.SubjectTypeSystemReadProvisionerDaemons, + rbac.SubjectTypeSystemRestricted, +} + func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { if c.written { return @@ -93,11 +148,32 @@ func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { c.written = true end := time.Now() + // Right before we write the log, we try to find the user in the actors + // and add the fields to the log. + c.addAuthContextFields() + logger := c.log.With( slog.F("took", end.Sub(c.start)), slog.F("status_code", status), slog.F("latency_ms", float64(end.Sub(c.start)/time.Millisecond)), ) + + // If the request is routed, add the route parameters to the log. + if chiCtx := chi.RouteContext(ctx); chiCtx != nil { + urlParams := chiCtx.URLParams + routeParamsFields := make([]slog.Field, 0, len(urlParams.Keys)) + + for k, v := range urlParams.Keys { + if urlParams.Values[k] != "" { + routeParamsFields = append(routeParamsFields, slog.F("params_"+v, urlParams.Values[k])) + } + } + + if len(routeParamsFields) > 0 { + logger = logger.With(routeParamsFields...) + } + } + // We already capture most of this information in the span (minus // the response body which we don't want to capture anyways). tracing.RunWithoutSpan(ctx, func(ctx context.Context) { diff --git a/coderd/httpmw/logger_internal_test.go b/coderd/httpmw/loggermw/logger_internal_test.go similarity index 56% rename from coderd/httpmw/logger_internal_test.go rename to coderd/httpmw/loggermw/logger_internal_test.go index d3035e50d98c9..e88f8a69c178e 100644 --- a/coderd/httpmw/logger_internal_test.go +++ b/coderd/httpmw/loggermw/logger_internal_test.go @@ -1,13 +1,16 @@ -package httpmw +package loggermw import ( "context" "net/http" "net/http/httptest" + "slices" + "strings" "sync" "testing" "time" + "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -79,7 +82,7 @@ func TestLoggerMiddleware_SingleRequest(t *testing.T) { require.Equal(t, sink.entries[0].Message, "GET") - fieldsMap := make(map[string]interface{}) + fieldsMap := make(map[string]any) for _, field := range sink.entries[0].Fields { fieldsMap[field.Name] = field.Value } @@ -156,6 +159,140 @@ func TestLoggerMiddleware_WebSocket(t *testing.T) { require.Len(t, sink.entries, 1, "log was written twice") } +func TestRequestLogger_HTTPRouteParams(t *testing.T) { + t.Parallel() + + sink := &fakeSink{} + logger := slog.Make(sink) + logger = logger.Leveled(slog.LevelDebug) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("workspace", "test-workspace") + chiCtx.URLParams.Add("agent", "test-agent") + + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + + // Create a test handler to simulate an HTTP request + testHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte("OK")) + }) + + // Wrap the test handler with the Logger middleware + loggerMiddleware := Logger(logger) + wrappedHandler := loggerMiddleware(testHandler) + + // Create a test HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/test-path/}", nil) + require.NoError(t, err, "failed to create request") + + sw := &tracing.StatusWriter{ResponseWriter: httptest.NewRecorder()} + + // Serve the request + wrappedHandler.ServeHTTP(sw, req) + + fieldsMap := make(map[string]any) + for _, field := range sink.entries[0].Fields { + fieldsMap[field.Name] = field.Value + } + + // Check that the log contains the expected fields + requiredFields := []string{"workspace", "agent"} + for _, field := range requiredFields { + _, exists := fieldsMap["params_"+field] + require.True(t, exists, "field %q is missing in log fields", field) + } +} + +func TestRequestLogger_RouteParamsLogging(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + params map[string]string + expectedFields []string + }{ + { + name: "EmptyParams", + params: map[string]string{}, + expectedFields: []string{}, + }, + { + name: "SingleParam", + params: map[string]string{ + "workspace": "test-workspace", + }, + expectedFields: []string{"params_workspace"}, + }, + { + name: "MultipleParams", + params: map[string]string{ + "workspace": "test-workspace", + "agent": "test-agent", + "user": "test-user", + }, + expectedFields: []string{"params_workspace", "params_agent", "params_user"}, + }, + { + name: "EmptyValueParam", + params: map[string]string{ + "workspace": "test-workspace", + "agent": "", + }, + expectedFields: []string{"params_workspace"}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + sink := &fakeSink{} + logger := slog.Make(sink) + logger = logger.Leveled(slog.LevelDebug) + + // Create a route context with the test parameters + chiCtx := chi.NewRouteContext() + for key, value := range tt.params { + chiCtx.URLParams.Add(key, value) + } + + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + logCtx := NewRequestLogger(logger, "GET", time.Now()) + + // Write the log + logCtx.WriteLog(ctx, http.StatusOK) + + require.Len(t, sink.entries, 1, "expected exactly one log entry") + + // Convert fields to map for easier checking + fieldsMap := make(map[string]any) + for _, field := range sink.entries[0].Fields { + fieldsMap[field.Name] = field.Value + } + + // Verify expected fields are present + for _, field := range tt.expectedFields { + value, exists := fieldsMap[field] + require.True(t, exists, "field %q should be present in log", field) + require.Equal(t, tt.params[strings.TrimPrefix(field, "params_")], value, "field %q has incorrect value", field) + } + + // Verify no unexpected fields are present + for field := range fieldsMap { + if field == "took" || field == "status_code" || field == "latency_ms" { + continue // Skip standard fields + } + require.True(t, slices.Contains(tt.expectedFields, field), "unexpected field %q in log", field) + } + }) + } +} + type fakeSink struct { entries []slog.SinkEntry newEntries chan slog.SinkEntry diff --git a/coderd/httpmw/loggermock/loggermock.go b/coderd/httpmw/loggermw/loggermock/loggermock.go similarity index 77% rename from coderd/httpmw/loggermock/loggermock.go rename to coderd/httpmw/loggermw/loggermock/loggermock.go index 47818ca11d9e6..008f862107ae6 100644 --- a/coderd/httpmw/loggermock/loggermock.go +++ b/coderd/httpmw/loggermw/loggermock/loggermock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/coder/coder/v2/coderd/httpmw (interfaces: RequestLogger) +// Source: github.com/coder/coder/v2/coderd/httpmw/loggermw (interfaces: RequestLogger) // // Generated by this command: // @@ -14,6 +14,7 @@ import ( reflect "reflect" slog "cdr.dev/slog" + rbac "github.com/coder/coder/v2/coderd/rbac" gomock "go.uber.org/mock/gomock" ) @@ -41,6 +42,18 @@ func (m *MockRequestLogger) EXPECT() *MockRequestLoggerMockRecorder { return m.recorder } +// WithAuthContext mocks base method. +func (m *MockRequestLogger) WithAuthContext(actor rbac.Subject) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "WithAuthContext", actor) +} + +// WithAuthContext indicates an expected call of WithAuthContext. +func (mr *MockRequestLoggerMockRecorder) WithAuthContext(actor any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithAuthContext", reflect.TypeOf((*MockRequestLogger)(nil).WithAuthContext), actor) +} + // WithFields mocks base method. func (m *MockRequestLogger) WithFields(fields ...slog.Field) { m.ctrl.T.Helper() diff --git a/coderd/httpmw/workspaceagentparam.go b/coderd/httpmw/workspaceagentparam.go index a47ce3c377ae0..434e057c0eccc 100644 --- a/coderd/httpmw/workspaceagentparam.go +++ b/coderd/httpmw/workspaceagentparam.go @@ -6,8 +6,11 @@ import ( "github.com/go-chi/chi/v5" + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/codersdk" ) @@ -81,6 +84,14 @@ func ExtractWorkspaceAgentParam(db database.Store) func(http.Handler) http.Handl ctx = context.WithValue(ctx, workspaceAgentParamContextKey{}, agent) chi.RouteContext(ctx).URLParams.Add("workspace", build.WorkspaceID.String()) + + if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { + rlogger.WithFields( + slog.F("workspace_name", resource.Name), + slog.F("agent_name", agent.Name), + ) + } + next.ServeHTTP(rw, r.WithContext(ctx)) }) } diff --git a/coderd/httpmw/workspaceparam.go b/coderd/httpmw/workspaceparam.go index 21e8dcfd62863..0c4e4f77354fc 100644 --- a/coderd/httpmw/workspaceparam.go +++ b/coderd/httpmw/workspaceparam.go @@ -9,8 +9,11 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/codersdk" ) @@ -48,6 +51,11 @@ func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler { } ctx = context.WithValue(ctx, workspaceParamContextKey{}, workspace) + + if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { + rlogger.WithFields(slog.F("workspace_name", workspace.Name)) + } + next.ServeHTTP(rw, r.WithContext(ctx)) }) } @@ -154,6 +162,13 @@ func ExtractWorkspaceAndAgentParam(db database.Store) func(http.Handler) http.Ha ctx = context.WithValue(ctx, workspaceParamContextKey{}, workspace) ctx = context.WithValue(ctx, workspaceAgentParamContextKey{}, agent) + + if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { + rlogger.WithFields( + slog.F("workspace_name", workspace.Name), + slog.F("agent_name", agent.Name), + ) + } next.ServeHTTP(rw, r.WithContext(ctx)) }) } diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index ea20c60de3cce..bc357bf2e35f2 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/pubsub" markdown "github.com/coder/coder/v2/coderd/render" @@ -220,7 +221,7 @@ func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) defer encoder.Close(websocket.StatusNormalClosure) // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) for { select { diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 335643390796f..6d75227a14ccd 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -20,6 +20,7 @@ import ( "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/util/slice" @@ -555,7 +556,7 @@ func (f *logFollower) follow() { } // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(f.ctx).WriteLog(f.ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(f.ctx).WriteLog(f.ctx, http.StatusAccepted) // no need to wait if the job is done if f.complete { diff --git a/coderd/provisionerjobs_internal_test.go b/coderd/provisionerjobs_internal_test.go index c2c0a60c75ba0..f3bc2eb1dea99 100644 --- a/coderd/provisionerjobs_internal_test.go +++ b/coderd/provisionerjobs_internal_test.go @@ -19,8 +19,8 @@ import ( "github.com/coder/coder/v2/coderd/database/dbmock" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/pubsub" - "github.com/coder/coder/v2/coderd/httpmw" - "github.com/coder/coder/v2/coderd/httpmw/loggermock" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw/loggermock" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/testutil" @@ -309,7 +309,7 @@ func Test_logFollower_EndOfLogs(t *testing.T) { mockLogger := loggermock.NewMockRequestLogger(ctrl) mockLogger.EXPECT().WriteLog(gomock.Any(), http.StatusAccepted).Times(1) - ctx = httpmw.WithRequestLogger(ctx, mockLogger) + ctx = loggermw.WithRequestLogger(ctx, mockLogger) // we need an HTTP server to get a websocket srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 3239ea3c42dc5..d2c6d5d0675be 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -58,6 +58,23 @@ func hashAuthorizeCall(actor Subject, action policy.Action, object Object) [32]b return hashOut } +// SubjectType represents the type of subject in the RBAC system. +type SubjectType string + +const ( + SubjectTypeUser SubjectType = "user" + SubjectTypeProvisionerd SubjectType = "provisionerd" + SubjectTypeAutostart SubjectType = "autostart" + SubjectTypeHangDetector SubjectType = "hang_detector" + SubjectTypeResourceMonitor SubjectType = "resource_monitor" + SubjectTypeCryptoKeyRotator SubjectType = "crypto_key_rotator" + SubjectTypeCryptoKeyReader SubjectType = "crypto_key_reader" + SubjectTypePrebuildsOrchestrator SubjectType = "prebuilds_orchestrator" + SubjectTypeSystemReadProvisionerDaemons SubjectType = "system_read_provisioner_daemons" + SubjectTypeSystemRestricted SubjectType = "system_restricted" + SubjectTypeNotifier SubjectType = "notifier" +) + // Subject is a struct that contains all the elements of a subject in an rbac // authorize. type Subject struct { @@ -67,6 +84,14 @@ type Subject struct { // external workspace proxy or other service type actor. FriendlyName string + // Email is entirely optional and is used for logging and debugging + // It is not used in any functional way. + Email string + + // Type indicates what kind of subject this is (user, system, provisioner, etc.) + // It is not used in any functional way, only for logging. + Type SubjectType + ID string Roles ExpandableRoles Groups []string diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index cf47514c7f0eb..1388b61030d38 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -33,6 +33,7 @@ import ( "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/httpapi" "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/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -551,7 +552,7 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { defer t.Stop() // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) go func() { defer func() { @@ -929,7 +930,7 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) { defer encoder.Close(websocket.StatusGoingAway) // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) go func(ctx context.Context) { // TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout? @@ -1329,7 +1330,7 @@ func (api *API) watchWorkspaceAgentMetadata( defer sendTicker.Stop() // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) // Send initial metadata. sendMetadata() diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 15e3c3901ade3..6ffa15851214d 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -24,6 +24,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -378,7 +379,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) }) // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) err = server.Serve(ctx, session) srvCancel() diff --git a/enterprise/wsproxy/wsproxy.go b/enterprise/wsproxy/wsproxy.go index 5dbf8ab6ea24d..bce49417fcd35 100644 --- a/enterprise/wsproxy/wsproxy.go +++ b/enterprise/wsproxy/wsproxy.go @@ -32,6 +32,7 @@ import ( "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/codersdk" @@ -336,7 +337,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) { tracing.Middleware(s.TracerProvider), httpmw.AttachRequestID, httpmw.ExtractRealIP(s.Options.RealIPConfig), - httpmw.Logger(s.Logger), + loggermw.Logger(s.Logger), prometheusMW, corsMW, diff --git a/tailnet/test/integration/integration.go b/tailnet/test/integration/integration.go index 08c66b515cd53..1190a3aa98b0d 100644 --- a/tailnet/test/integration/integration.go +++ b/tailnet/test/integration/integration.go @@ -33,7 +33,7 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd/httpapi" - "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" @@ -200,7 +200,7 @@ func (o SimpleServerOptions) Router(t *testing.T, logger slog.Logger) *chi.Mux { }) }, tracing.StatusWriterMiddleware, - httpmw.Logger(logger), + loggermw.Logger(logger), ) r.Route("/derp", func(r chi.Router) { From c8c4de5f7a4a5fead34835ccc66782566bf7f5b4 Mon Sep 17 00:00:00 2001 From: Benjamin Peinhardt <61021968+bcpeinhardt@users.noreply.github.com> Date: Tue, 15 Apr 2025 08:26:13 -0500 Subject: [PATCH 094/384] chore(dogfood): add tmux (#17397) --- dogfood/coder/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dogfood/coder/Dockerfile b/dogfood/coder/Dockerfile index b17d4c49563d3..1559279e41aa9 100644 --- a/dogfood/coder/Dockerfile +++ b/dogfood/coder/Dockerfile @@ -85,7 +85,7 @@ RUN apt-get update && \ rm -rf /tmp/go/src # alpine:3.18 -FROM gcr.io/coder-dev-1/alpine@sha256:25fad2a32ad1f6f510e528448ae1ec69a28ef81916a004d3629874104f8a7f70 AS proto +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 && \ @@ -185,6 +185,7 @@ RUN apt-get update --quiet && apt-get install --yes \ sudo \ tcptraceroute \ termshark \ + tmux \ traceroute \ unzip \ vim \ From 00b5f56734b9f5ac33bb80625259cdd3c28d289d Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 15 Apr 2025 17:53:37 +0300 Subject: [PATCH 095/384] feat(agent/agentcontainers): add devcontainers list endpoint (#17389) This change allows listing both predefined and runtime-detected devcontainers, as well as showing whether or not the devcontainer is running and which container represents it. Fixes coder/internal#478 --- agent/agentcontainers/api.go | 134 ++++++++++-- agent/agentcontainers/api_test.go | 340 +++++++++++++++++++++++++++++- agent/api.go | 15 +- codersdk/workspaceagents.go | 10 + site/src/api/typesGenerated.ts | 7 + 5 files changed, 487 insertions(+), 19 deletions(-) diff --git a/agent/agentcontainers/api.go b/agent/agentcontainers/api.go index 81354457d0730..9a028e565b6ca 100644 --- a/agent/agentcontainers/api.go +++ b/agent/agentcontainers/api.go @@ -3,11 +3,15 @@ package agentcontainers import ( "context" "errors" + "fmt" "net/http" + "path" "slices" + "strings" "time" "github.com/go-chi/chi/v5" + "github.com/google/uuid" "golang.org/x/xerrors" "cdr.dev/slog" @@ -31,11 +35,13 @@ type API struct { dccli DevcontainerCLI clock quartz.Clock - // lockCh protects the below fields. We use a channel instead of a mutex so we - // can handle cancellation properly. - lockCh chan struct{} - containers codersdk.WorkspaceAgentListContainersResponse - mtime time.Time + // lockCh protects the below fields. We use a channel instead of a + // mutex so we can handle cancellation properly. + lockCh chan struct{} + containers codersdk.WorkspaceAgentListContainersResponse + mtime time.Time + devcontainerNames map[string]struct{} // Track devcontainer names to avoid duplicates. + knownDevcontainers []codersdk.WorkspaceAgentDevcontainer // Track predefined and runtime-detected devcontainers. } // Option is a functional option for API. @@ -55,12 +61,29 @@ 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 { + 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{}{} + } + } + } +} + // NewAPI returns a new API with the given options applied. func NewAPI(logger slog.Logger, options ...Option) *API { api := &API{ - clock: quartz.NewReal(), - cacheDuration: defaultGetContainersCacheDuration, - lockCh: make(chan struct{}, 1), + clock: quartz.NewReal(), + cacheDuration: defaultGetContainersCacheDuration, + lockCh: make(chan struct{}, 1), + devcontainerNames: make(map[string]struct{}), + knownDevcontainers: []codersdk.WorkspaceAgentDevcontainer{}, } for _, opt := range options { opt(api) @@ -79,6 +102,7 @@ func NewAPI(logger slog.Logger, options ...Option) *API { func (api *API) Routes() http.Handler { r := chi.NewRouter() r.Get("/", api.handleList) + r.Get("/devcontainers", api.handleListDevcontainers) r.Post("/{id}/recreate", api.handleRecreate) return r } @@ -121,12 +145,11 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC select { case <-ctx.Done(): return codersdk.WorkspaceAgentListContainersResponse{}, ctx.Err() - default: - api.lockCh <- struct{}{} + case api.lockCh <- struct{}{}: + defer func() { + <-api.lockCh + }() } - defer func() { - <-api.lockCh - }() now := api.clock.Now() if now.Sub(api.mtime) < api.cacheDuration { @@ -142,6 +165,53 @@ func (api *API) getContainers(ctx context.Context) (codersdk.WorkspaceAgentListC api.containers = updated api.mtime = now + // Reset all known devcontainers to not running. + for i := range api.knownDevcontainers { + api.knownDevcontainers[i].Running = false + api.knownDevcontainers[i].Container = nil + } + + // Check if the container is running and update the known devcontainers. + for _, container := range updated.Containers { + workspaceFolder := container.Labels[DevcontainerLocalFolderLabel] + if workspaceFolder != "" { + // Check if this is already in our known list. + if knownIndex := slices.IndexFunc(api.knownDevcontainers, func(dc codersdk.WorkspaceAgentDevcontainer) bool { + return dc.WorkspaceFolder == workspaceFolder + }); knownIndex != -1 { + // Update existing entry with runtime information. + if api.knownDevcontainers[knownIndex].ConfigPath == "" { + api.knownDevcontainers[knownIndex].ConfigPath = container.Labels[DevcontainerConfigFileLabel] + } + api.knownDevcontainers[knownIndex].Running = container.Running + api.knownDevcontainers[knownIndex].Container = &container + continue + } + + // If not in our known list, add as a runtime detected entry. + name := path.Base(workspaceFolder) + if _, ok := api.devcontainerNames[name]; ok { + // Try to find a unique name by appending a number. + for i := 2; ; i++ { + newName := fmt.Sprintf("%s-%d", name, i) + if _, ok := api.devcontainerNames[newName]; !ok { + name = newName + break + } + } + } + api.devcontainerNames[name] = struct{}{} + api.knownDevcontainers = append(api.knownDevcontainers, codersdk.WorkspaceAgentDevcontainer{ + ID: uuid.New(), + Name: name, + WorkspaceFolder: workspaceFolder, + ConfigPath: container.Labels[DevcontainerConfigFileLabel], + Running: container.Running, + Container: &container, + }) + } + } + return copyListContainersResponse(api.containers), nil } @@ -158,7 +228,7 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) { return } - containers, err := api.cl.List(ctx) + containers, err := api.getContainers(ctx) if err != nil { httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ Message: "Could not list containers", @@ -203,3 +273,39 @@ func (api *API) handleRecreate(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } + +// handleListDevcontainers handles the HTTP request to list known devcontainers. +func (api *API) handleListDevcontainers(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Run getContainers to detect the latest devcontainers and their state. + _, err := api.getContainers(ctx) + if err != nil { + httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{ + Message: "Could not list containers", + Detail: err.Error(), + }) + return + } + + select { + 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 { + return cmp + } + return strings.Compare(a.ConfigPath, b.ConfigPath) + }) + + response := codersdk.WorkspaceAgentDevcontainersResponse{ + Devcontainers: devcontainers, + } + + httpapi.Write(ctx, w, http.StatusOK, response) +} diff --git a/agent/agentcontainers/api_test.go b/agent/agentcontainers/api_test.go index 76a88f4fc1da4..6f2fe5ce84919 100644 --- a/agent/agentcontainers/api_test.go +++ b/agent/agentcontainers/api_test.go @@ -2,11 +2,13 @@ package agentcontainers_test import ( "context" + "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/xerrors" @@ -151,10 +153,10 @@ func TestAPI(t *testing.T) { agentcontainers.WithLister(tt.lister), agentcontainers.WithDevcontainerCLI(tt.devcontainerCLI), ) - r.Mount("/containers", api.Routes()) + r.Mount("/", api.Routes()) // Simulate HTTP request to the recreate endpoint. - req := httptest.NewRequest(http.MethodPost, "/containers/"+tt.containerID+"/recreate", nil) + req := httptest.NewRequest(http.MethodPost, "/"+tt.containerID+"/recreate", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) @@ -168,4 +170,338 @@ func TestAPI(t *testing.T) { }) } }) + + t.Run("List devcontainers", func(t *testing.T) { + t.Parallel() + + knownDevcontainerID1 := uuid.New() + knownDevcontainerID2 := uuid.New() + + knownDevcontainers := []codersdk.WorkspaceAgentDevcontainer{ + { + ID: knownDevcontainerID1, + Name: "known-devcontainer-1", + WorkspaceFolder: "/workspace/known1", + ConfigPath: "/workspace/known1/.devcontainer/devcontainer.json", + }, + { + ID: knownDevcontainerID2, + Name: "known-devcontainer-2", + WorkspaceFolder: "/workspace/known2", + // No config path intentionally. + }, + } + + tests := []struct { + name string + lister *fakeLister + knownDevcontainers []codersdk.WorkspaceAgentDevcontainer + wantStatus int + wantCount int + verify func(t *testing.T, devcontainers []codersdk.WorkspaceAgentDevcontainer) + }{ + { + name: "List error", + lister: &fakeLister{ + err: xerrors.New("list error"), + }, + wantStatus: http.StatusInternalServerError, + }, + { + name: "Empty containers", + lister: &fakeLister{}, + wantStatus: http.StatusOK, + wantCount: 0, + }, + { + name: "Only known devcontainers, no containers", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{}, + }, + }, + knownDevcontainers: knownDevcontainers, + wantStatus: http.StatusOK, + wantCount: 2, + verify: func(t *testing.T, devcontainers []codersdk.WorkspaceAgentDevcontainer) { + for _, dc := range devcontainers { + assert.False(t, dc.Running, "devcontainer should not be running") + assert.Nil(t, dc.Container, "devcontainer should not have container reference") + } + }, + }, + { + name: "Runtime-detected devcontainer", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{ + { + ID: "runtime-container-1", + FriendlyName: "runtime-container-1", + Running: true, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspace/runtime1", + agentcontainers.DevcontainerConfigFileLabel: "/workspace/runtime1/.devcontainer/devcontainer.json", + }, + }, + { + ID: "not-a-devcontainer", + FriendlyName: "not-a-devcontainer", + Running: true, + Labels: map[string]string{}, + }, + }, + }, + }, + wantStatus: http.StatusOK, + wantCount: 1, + verify: func(t *testing.T, devcontainers []codersdk.WorkspaceAgentDevcontainer) { + dc := devcontainers[0] + assert.Equal(t, "/workspace/runtime1", dc.WorkspaceFolder) + assert.True(t, dc.Running) + require.NotNil(t, dc.Container) + assert.Equal(t, "runtime-container-1", dc.Container.ID) + }, + }, + { + name: "Mixed known and runtime-detected devcontainers", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{ + { + ID: "known-container-1", + FriendlyName: "known-container-1", + Running: true, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspace/known1", + agentcontainers.DevcontainerConfigFileLabel: "/workspace/known1/.devcontainer/devcontainer.json", + }, + }, + { + ID: "runtime-container-1", + FriendlyName: "runtime-container-1", + Running: true, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspace/runtime1", + agentcontainers.DevcontainerConfigFileLabel: "/workspace/runtime1/.devcontainer/devcontainer.json", + }, + }, + }, + }, + }, + knownDevcontainers: knownDevcontainers, + wantStatus: http.StatusOK, + wantCount: 3, // 2 known + 1 runtime + verify: func(t *testing.T, devcontainers []codersdk.WorkspaceAgentDevcontainer) { + known1 := mustFindDevcontainerByPath(t, devcontainers, "/workspace/known1") + known2 := mustFindDevcontainerByPath(t, devcontainers, "/workspace/known2") + runtime1 := mustFindDevcontainerByPath(t, devcontainers, "/workspace/runtime1") + + assert.True(t, known1.Running) + assert.False(t, known2.Running) + assert.True(t, runtime1.Running) + + require.NotNil(t, known1.Container) + assert.Nil(t, known2.Container) + require.NotNil(t, runtime1.Container) + + assert.Equal(t, "known-container-1", known1.Container.ID) + assert.Equal(t, "runtime-container-1", runtime1.Container.ID) + }, + }, + { + name: "Both running and non-running containers have container references", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{ + { + ID: "running-container", + FriendlyName: "running-container", + Running: true, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspace/running", + agentcontainers.DevcontainerConfigFileLabel: "/workspace/running/.devcontainer/devcontainer.json", + }, + }, + { + ID: "non-running-container", + FriendlyName: "non-running-container", + Running: false, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspace/non-running", + agentcontainers.DevcontainerConfigFileLabel: "/workspace/non-running/.devcontainer/devcontainer.json", + }, + }, + }, + }, + }, + wantStatus: http.StatusOK, + wantCount: 2, + verify: func(t *testing.T, devcontainers []codersdk.WorkspaceAgentDevcontainer) { + running := mustFindDevcontainerByPath(t, devcontainers, "/workspace/running") + nonRunning := mustFindDevcontainerByPath(t, devcontainers, "/workspace/non-running") + + assert.True(t, running.Running) + assert.False(t, nonRunning.Running) + + require.NotNil(t, running.Container, "running container should have container reference") + require.NotNil(t, nonRunning.Container, "non-running container should have container reference") + + assert.Equal(t, "running-container", running.Container.ID) + assert.Equal(t, "non-running-container", nonRunning.Container.ID) + }, + }, + { + name: "Config path update", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{ + { + ID: "known-container-2", + FriendlyName: "known-container-2", + Running: true, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspace/known2", + agentcontainers.DevcontainerConfigFileLabel: "/workspace/known2/.devcontainer/devcontainer.json", + }, + }, + }, + }, + }, + knownDevcontainers: knownDevcontainers, + wantStatus: http.StatusOK, + wantCount: 2, + verify: func(t *testing.T, devcontainers []codersdk.WorkspaceAgentDevcontainer) { + var dc2 *codersdk.WorkspaceAgentDevcontainer + for i := range devcontainers { + if devcontainers[i].ID == knownDevcontainerID2 { + dc2 = &devcontainers[i] + break + } + } + require.NotNil(t, dc2, "missing devcontainer with ID %s", knownDevcontainerID2) + assert.True(t, dc2.Running) + assert.NotEmpty(t, dc2.ConfigPath) + require.NotNil(t, dc2.Container) + assert.Equal(t, "known-container-2", dc2.Container.ID) + }, + }, + { + name: "Name generation and uniqueness", + lister: &fakeLister{ + containers: codersdk.WorkspaceAgentListContainersResponse{ + Containers: []codersdk.WorkspaceAgentContainer{ + { + ID: "project1-container", + FriendlyName: "project1-container", + Running: true, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/workspace/project", + agentcontainers.DevcontainerConfigFileLabel: "/workspace/project/.devcontainer/devcontainer.json", + }, + }, + { + ID: "project2-container", + FriendlyName: "project2-container", + Running: true, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/home/user/project", + agentcontainers.DevcontainerConfigFileLabel: "/home/user/project/.devcontainer/devcontainer.json", + }, + }, + { + ID: "project3-container", + FriendlyName: "project3-container", + Running: true, + Labels: map[string]string{ + agentcontainers.DevcontainerLocalFolderLabel: "/var/lib/project", + agentcontainers.DevcontainerConfigFileLabel: "/var/lib/project/.devcontainer/devcontainer.json", + }, + }, + }, + }, + }, + knownDevcontainers: []codersdk.WorkspaceAgentDevcontainer{ + { + ID: uuid.New(), + Name: "project", // This will cause uniqueness conflicts. + WorkspaceFolder: "/usr/local/project", + ConfigPath: "/usr/local/project/.devcontainer/devcontainer.json", + }, + }, + wantStatus: http.StatusOK, + wantCount: 4, // 1 known + 3 runtime + verify: func(t *testing.T, devcontainers []codersdk.WorkspaceAgentDevcontainer) { + names := make(map[string]int) + for _, dc := range devcontainers { + names[dc.Name]++ + assert.NotEmpty(t, dc.Name, "devcontainer name should not be empty") + } + + for name, count := range names { + assert.Equal(t, 1, count, "name '%s' appears %d times, should be unique", name, count) + } + assert.Len(t, names, 4, "should have four unique devcontainer names") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug) + + // Setup router with the handler under test. + r := chi.NewRouter() + apiOptions := []agentcontainers.Option{ + agentcontainers.WithLister(tt.lister), + } + + if len(tt.knownDevcontainers) > 0 { + apiOptions = append(apiOptions, agentcontainers.WithDevcontainers(tt.knownDevcontainers)) + } + + api := agentcontainers.NewAPI(logger, apiOptions...) + r.Mount("/", api.Routes()) + + req := httptest.NewRequest(http.MethodGet, "/devcontainers", nil) + rec := httptest.NewRecorder() + r.ServeHTTP(rec, req) + + // Check the response status code. + require.Equal(t, tt.wantStatus, rec.Code, "status code mismatch") + if tt.wantStatus != http.StatusOK { + return + } + + var response codersdk.WorkspaceAgentDevcontainersResponse + err := json.NewDecoder(rec.Body).Decode(&response) + require.NoError(t, err, "unmarshal response failed") + + // Verify the number of devcontainers in the response. + assert.Len(t, response.Devcontainers, tt.wantCount, "wrong number of devcontainers") + + // Run custom verification if provided. + if tt.verify != nil && len(response.Devcontainers) > 0 { + tt.verify(t, response.Devcontainers) + } + }) + } + }) +} + +// mustFindDevcontainerByPath returns the devcontainer with the given workspace +// folder path. It fails the test if no matching devcontainer is found. +func mustFindDevcontainerByPath(t *testing.T, devcontainers []codersdk.WorkspaceAgentDevcontainer, path string) codersdk.WorkspaceAgentDevcontainer { + t.Helper() + + for i := range devcontainers { + if devcontainers[i].WorkspaceFolder == path { + return devcontainers[i] + } + } + + require.Failf(t, "no devcontainer found with workspace folder %q", path) + return codersdk.WorkspaceAgentDevcontainer{} // Unreachable, but required for compilation } diff --git a/agent/api.go b/agent/api.go index bb357d1b87da2..0813deb77a146 100644 --- a/agent/api.go +++ b/agent/api.go @@ -37,10 +37,19 @@ func (a *agent) apiHandler() http.Handler { cacheDuration: cacheDuration, } - containerAPI := agentcontainers.NewAPI( - a.logger.Named("containers"), + containerAPIOpts := []agentcontainers.Option{ agentcontainers.WithLister(a.lister), - ) + } + if a.experimentalDevcontainersEnabled { + manifest := a.manifest.Load() + if manifest != nil && len(manifest.Devcontainers) > 0 { + containerAPIOpts = append( + containerAPIOpts, + agentcontainers.WithDevcontainers(manifest.Devcontainers), + ) + } + } + containerAPI := agentcontainers.NewAPI(a.logger.Named("containers"), containerAPIOpts...) promHandler := PrometheusMetricsHandler(a.prometheusRegistry, a.logger) diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index ef770712c340a..6a72de5ae4ff3 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -392,6 +392,12 @@ func (c *Client) WorkspaceAgentListeningPorts(ctx context.Context, agentID uuid. return listeningPorts, json.NewDecoder(res.Body).Decode(&listeningPorts) } +// WorkspaceAgentDevcontainersResponse is the response to the devcontainers +// request. +type WorkspaceAgentDevcontainersResponse struct { + Devcontainers []WorkspaceAgentDevcontainer `json:"devcontainers"` +} + // WorkspaceAgentDevcontainer defines the location of a devcontainer // configuration in a workspace that is visible to the workspace agent. type WorkspaceAgentDevcontainer struct { @@ -399,6 +405,10 @@ type WorkspaceAgentDevcontainer struct { Name string `json:"name"` WorkspaceFolder string `json:"workspace_folder"` ConfigPath string `json:"config_path,omitempty"` + + // Additional runtime fields. + Running bool `json:"running"` + Container *WorkspaceAgentContainer `json:"container,omitempty"` } // WorkspaceAgentContainer describes a devcontainer of some sort diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index c3109139ba300..38e8e91ac8c1a 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3239,6 +3239,13 @@ export interface WorkspaceAgentDevcontainer { readonly name: string; readonly workspace_folder: string; readonly config_path?: string; + readonly running: boolean; + readonly container?: WorkspaceAgentContainer; +} + +// From codersdk/workspaceagents.go +export interface WorkspaceAgentDevcontainersResponse { + readonly devcontainers: readonly WorkspaceAgentDevcontainer[]; } // From codersdk/workspaceagents.go From b0fe62625042bc54dce75ee1e128c143c885e3d0 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 15 Apr 2025 13:52:32 -0300 Subject: [PATCH 096/384] refactor: update the workspace table design (#17404) Related to https://github.com/coder/coder/issues/17309 **Before:** Screenshot 2025-04-15 at 11 36 32 **After:** Screenshot 2025-04-15 at 11 36 22 --- site/src/hooks/useClickableTableRow.ts | 22 +- .../pages/TemplatesPage/TemplatesPageView.tsx | 4 +- site/src/pages/WorkspacesPage/LastUsed.tsx | 5 +- .../pages/WorkspacesPage/WorkspacesTable.tsx | 423 ++++++++---------- 4 files changed, 204 insertions(+), 250 deletions(-) diff --git a/site/src/hooks/useClickableTableRow.ts b/site/src/hooks/useClickableTableRow.ts index 1967762aa24dc..5f10c637b8de3 100644 --- a/site/src/hooks/useClickableTableRow.ts +++ b/site/src/hooks/useClickableTableRow.ts @@ -13,9 +13,9 @@ * It might not make sense to test this hook until the underlying design * problems are fixed. */ -import { type CSSObject, useTheme } from "@emotion/react"; import type { TableRowProps } from "@mui/material/TableRow"; import type { MouseEventHandler } from "react"; +import { cn } from "utils/cn"; import { type ClickableAriaRole, type UseClickableResult, @@ -26,7 +26,7 @@ type UseClickableTableRowResult< TRole extends ClickableAriaRole = ClickableAriaRole, > = UseClickableResult & TableRowProps & { - css: CSSObject; + className: string; hover: true; onAuxClick: MouseEventHandler; }; @@ -54,23 +54,13 @@ export const useClickableTableRow = < onAuxClick: externalOnAuxClick, }: UseClickableTableRowConfig): UseClickableTableRowResult => { const clickableProps = useClickable(onClick, (role ?? "button") as TRole); - const theme = useTheme(); return { ...clickableProps, - css: { - cursor: "pointer", - - "&:focus": { - outline: `1px solid ${theme.palette.primary.main}`, - outlineOffset: -1, - }, - - "&:last-of-type": { - borderBottomLeftRadius: 8, - borderBottomRightRadius: 8, - }, - }, + className: cn([ + "cursor-pointer hover:outline focus:outline outline-1 -outline-offset-1 outline-border-hover", + "first:rounded-t-md last:rounded-b-md", + ]), hover: true, onDoubleClick, onAuxClick: (event) => { diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index 3a4e5a7812f09..30b1cd5093185 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -102,7 +102,7 @@ const TemplateRow: FC = ({ ); const navigate = useNavigate(); - const { css: clickableCss, ...clickableRow } = useClickableTableRow({ + const clickableRow = useClickableTableRow({ onClick: () => navigate(templatePageLink), }); @@ -111,7 +111,7 @@ const TemplateRow: FC = ({ key={template.id} data-testid={`template-${template.id}`} {...clickableRow} - css={[clickableCss, styles.tableRow]} + css={styles.tableRow} > = ({ lastUsedAt }) => { - const theme = useTheme(); - const [circle, message] = useTime(() => { const t = dayjs(lastUsedAt); const now = dayjs(); @@ -40,7 +37,7 @@ export const LastUsed: FC = ({ lastUsedAt }) => { return ( = ({ templates, canCreateTemplate, }) => { - const theme = useTheme(); const dashboard = useDashboard(); const workspaceIDToAppByStatus = useMemo(() => { return ( @@ -96,213 +96,189 @@ export const WorkspacesTable: FC = ({ ); return ( - - - - - -
    - {canCheckWorkspaces && ( - { - if (!workspaces) { - return; - } +
    + + + +
    + {canCheckWorkspaces && ( + { + if (!workspaces) { + return; + } - if (!checked) { - onCheckChange([]); - } else { - onCheckChange(workspaces); - } - }} - /> - )} - Name -
    + if (!checked) { + onCheckChange([]); + } else { + onCheckChange(workspaces); + } + }} + /> + )} + Name + +
    + {hasAppStatus && Activity} + Template + Last used + Status + +
    +
    + + {!workspaces && } + {workspaces && workspaces.length === 0 && ( + + + - {hasAppStatus && Activity} - Template - Last used - Status - - - - {!workspaces && ( - - )} - {workspaces && workspaces.length === 0 && ( - - )} - {workspaces?.map((workspace) => { - const checked = checkedWorkspaces.some( - (w) => w.id === workspace.id, - ); - const activeOrg = dashboard.organizations.find( - (o) => o.id === workspace.organization_id, - ); + )} + {workspaces?.map((workspace) => { + const checked = checkedWorkspaces.some((w) => w.id === workspace.id); + const activeOrg = dashboard.organizations.find( + (o) => o.id === workspace.organization_id, + ); - return ( - - -
    - {canCheckWorkspaces && ( - { - e.stopPropagation(); - }} - onChange={(e) => { - if (e.currentTarget.checked) { - onCheckChange([...checkedWorkspaces, workspace]); - } else { - onCheckChange( - checkedWorkspaces.filter( - (w) => w.id !== workspace.id, - ), - ); - } - }} - /> - )} - - {workspace.name} - {workspace.favorite && ( - - )} - {workspace.outdated && ( - { - onUpdateWorkspace(workspace); - }} - /> - )} - - } - subtitle={ -
    - Owner: - {workspace.owner_name} -
    - } - avatar={ - - } - /> -
    -
    - - {hasAppStatus && ( - - - - )} - - -
    {getDisplayWorkspaceTemplateName(workspace)}
    - - {dashboard.showOrganizations && ( -
    + +
    + {canCheckWorkspaces && ( + { + e.stopPropagation(); + }} + onChange={(e) => { + if (e.currentTarget.checked) { + onCheckChange([...checkedWorkspaces, workspace]); + } else { + onCheckChange( + checkedWorkspaces.filter( + (w) => w.id !== workspace.id, + ), + ); + } }} - > - Organization: - {activeOrg?.display_name || workspace.organization_name} -
    + /> )} -
    + + {workspace.name} + {workspace.favorite && } + {workspace.outdated && ( + { + onUpdateWorkspace(workspace); + }} + /> + )} + + } + subtitle={ +
    + Owner: + {workspace.owner_name} +
    + } + avatar={ + + } + /> +
    +
    + {hasAppStatus && ( - + + )} - -
    - - {workspace.latest_build.status === "running" && - !workspace.health.healthy && ( - - )} - {workspace.dormant_at && ( - + + + Organization:{" "} + {activeOrg?.display_name || workspace.organization_name} + + ) + } + avatar={ + + } + /> + + + + + + + +
    + + {workspace.latest_build.status === "running" && + !workspace.health.healthy && ( + )} -
    -
    + {workspace.dormant_at && ( + + )} +
    +
    - -
    - -
    -
    -
    - ); - })} -
    -
    -
    + +
    + +
    +
    + + ); + })} + + ); }; @@ -318,7 +294,6 @@ const WorkspacesRow: FC = ({ checked, }) => { const navigate = useNavigate(); - const theme = useTheme(); const workspacePageLink = `/@${workspace.owner_name}/${workspace.name}`; const openLinkInNewTab = () => window.open(workspacePageLink, "_blank"); @@ -339,20 +314,14 @@ const WorkspacesRow: FC = ({ }, }); - const bgColor = checked ? theme.palette.action.hover : undefined; - return ( {children} @@ -367,25 +336,23 @@ const TableLoader: FC = ({ canCheckWorkspaces }) => { return ( - -
    - {canCheckWorkspaces && ( - - )} + +
    + {canCheckWorkspaces && }
    - - + + - - + + - - + + - - + + From 0cd531dd3386ef721e56423c99bdca066ba9fd5c Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Tue, 15 Apr 2025 14:11:05 -0400 Subject: [PATCH 097/384] docs: document workspace naming rules and restrictions (#17312) closes #12047 [preview](https://coder.com/docs/@12047-workspace-names/user-guides/workspace-management) --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- coderd/apidoc/docs.go | 2 +- coderd/apidoc/swagger.json | 2 +- codersdk/organizations.go | 7 +++++++ docs/reference/api/schemas.md | 2 +- docs/user-guides/workspace-management.md | 11 +++++++++++ 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 04b0a93cfb12e..dcb7eba98b653 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11433,7 +11433,7 @@ const docTemplate = `{ } }, "codersdk.CreateWorkspaceRequest": { - "description": "CreateWorkspaceRequest provides options for creating a new workspace. Only one of TemplateID or TemplateVersionID can be specified, not both. If TemplateID is specified, the active version of the template will be used.", + "description": "CreateWorkspaceRequest provides options for creating a new workspace. Only one of TemplateID or TemplateVersionID can be specified, not both. If TemplateID is specified, the active version of the template will be used. Workspace names: - Must start with a letter or number - Can only contain letters, numbers, and hyphens - Cannot contain spaces or special characters - Cannot be named ` + "`" + `new` + "`" + ` or ` + "`" + `create` + "`" + ` - Must be unique within your workspaces - Maximum length of 32 characters", "type": "object", "required": [ "name" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 1cea2c58f7255..0464733070ef3 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10193,7 +10193,7 @@ } }, "codersdk.CreateWorkspaceRequest": { - "description": "CreateWorkspaceRequest provides options for creating a new workspace. Only one of TemplateID or TemplateVersionID can be specified, not both. If TemplateID is specified, the active version of the template will be used.", + "description": "CreateWorkspaceRequest provides options for creating a new workspace. Only one of TemplateID or TemplateVersionID can be specified, not both. If TemplateID is specified, the active version of the template will be used. Workspace names: - Must start with a letter or number - Can only contain letters, numbers, and hyphens - Cannot contain spaces or special characters - Cannot be named `new` or `create` - Must be unique within your workspaces - Maximum length of 32 characters", "type": "object", "required": ["name"], "properties": { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index b981e3bed28fa..b880f25e15a2c 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -207,6 +207,13 @@ type CreateTemplateRequest struct { // @Description CreateWorkspaceRequest provides options for creating a new workspace. // @Description Only one of TemplateID or TemplateVersionID can be specified, not both. // @Description If TemplateID is specified, the active version of the template will be used. +// @Description Workspace names: +// @Description - Must start with a letter or number +// @Description - Can only contain letters, numbers, and hyphens +// @Description - Cannot contain spaces or special characters +// @Description - Cannot be named `new` or `create` +// @Description - Must be unique within your workspaces +// @Description - Maximum length of 32 characters type CreateWorkspaceRequest struct { // TemplateID specifies which template should be used for creating the workspace. TemplateID uuid.UUID `json:"template_id,omitempty" validate:"required_without=TemplateVersionID,excluded_with=TemplateVersionID" format:"uuid"` diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 85b6e65a545aa..79d7a411bf98c 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1476,7 +1476,7 @@ None } ``` -CreateWorkspaceRequest provides options for creating a new workspace. Only one of TemplateID or TemplateVersionID can be specified, not both. If TemplateID is specified, the active version of the template will be used. +CreateWorkspaceRequest provides options for creating a new workspace. Only one of TemplateID or TemplateVersionID can be specified, not both. If TemplateID is specified, the active version of the template will be used. Workspace names: - Must start with a letter or number - Can only contain letters, numbers, and hyphens - Cannot contain spaces or special characters - Cannot be named `new` or `create` - Must be unique within your workspaces - Maximum length of 32 characters ### Properties diff --git a/docs/user-guides/workspace-management.md b/docs/user-guides/workspace-management.md index 695b5de36fb79..ad9bd3466b99a 100644 --- a/docs/user-guides/workspace-management.md +++ b/docs/user-guides/workspace-management.md @@ -34,6 +34,17 @@ coder create --template="" coder show ``` +### Workspace name rules and restrictions + +| Constraint | Rule | +|------------------|--------------------------------------------| +| Start/end with | Must start and end with a letter or number | +| Character types | Letters, numbers, and hyphens only | +| Length | 1-32 characters | +| Case sensitivity | Case-insensitive (lowercase recommended) | +| Reserved names | Cannot use `new` or `create` | +| Uniqueness | Must be unique within your workspaces | + ## Workspace filtering In the Coder UI, you can filter your workspaces using pre-defined filters or From 362dcfefdd727a4c1973c44bfb16d8b660713b95 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 15 Apr 2025 15:10:30 -0400 Subject: [PATCH 098/384] fix: update start-workspace.yaml for dev.coder.com (#17407) I added the secrets and removed the aidev env secrets. --- .github/workflows/start-workspace.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/start-workspace.yaml b/.github/workflows/start-workspace.yaml index 41a5cd4b41d9f..ddc4d1a330707 100644 --- a/.github/workflows/start-workspace.yaml +++ b/.github/workflows/start-workspace.yaml @@ -15,7 +15,7 @@ jobs: if: >- (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@coder')) || (github.event_name == 'issues' && contains(github.event.issue.body, '@coder')) - environment: aidev + environment: dev.coder.com timeout-minutes: 5 steps: - name: Start Coder workspace From 57ddb3c61589f4bd3abdbe77cde15cae3f3da20c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 15 Apr 2025 15:15:00 -0400 Subject: [PATCH 099/384] fix: update ai code prompt parameter in start-workspace.yaml --- .github/workflows/start-workspace.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/start-workspace.yaml b/.github/workflows/start-workspace.yaml index ddc4d1a330707..975acd7e1d939 100644 --- a/.github/workflows/start-workspace.yaml +++ b/.github/workflows/start-workspace.yaml @@ -31,7 +31,5 @@ jobs: coder-token: ${{ secrets.CODER_TOKEN }} template-name: ${{ secrets.CODER_TEMPLATE_NAME }} parameters: |- - Coder Image: codercom/oss-dogfood:latest - Coder Repository Base Directory: "~" - AI Code Prompt: "Use the gh CLI tool to read the details of issue https://github.com/${{ github.repository }}/issues/${{ github.event.issue.number }} and then address it." + AI Prompt: "Use the gh CLI tool to read the details of issue https://github.com/${{ github.repository }}/issues/${{ github.event.issue.number }} and then address it." Region: us-pittsburgh From 70b113de7b234b84ef5419c7e4531809db144a35 Mon Sep 17 00:00:00 2001 From: brettkolodny Date: Tue, 15 Apr 2025 18:30:20 -0400 Subject: [PATCH 100/384] feat: add edit-role within user command (#17341) --- cli/testdata/coder_users_--help.golden | 19 ++-- .../coder_users_edit-roles_--help.golden | 18 ++++ cli/usereditroles.go | 90 +++++++++++++++++++ cli/usereditroles_test.go | 62 +++++++++++++ cli/users.go | 1 + docs/manifest.json | 5 ++ docs/reference/cli/users.md | 17 ++-- docs/reference/cli/users_edit-roles.md | 28 ++++++ 8 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 cli/testdata/coder_users_edit-roles_--help.golden create mode 100644 cli/usereditroles.go create mode 100644 cli/usereditroles_test.go create mode 100644 docs/reference/cli/users_edit-roles.md diff --git a/cli/testdata/coder_users_--help.golden b/cli/testdata/coder_users_--help.golden index 338fea4febc86..585588cbc6e18 100644 --- a/cli/testdata/coder_users_--help.golden +++ b/cli/testdata/coder_users_--help.golden @@ -8,15 +8,16 @@ USAGE: Aliases: user SUBCOMMANDS: - activate Update a user's status to 'active'. Active users can fully - interact with the platform - create - delete Delete a user by username or user_id. - list - show Show a single user. Use 'me' to indicate the currently - authenticated user. - suspend Update a user's status to 'suspended'. A suspended user cannot - log into the platform + activate Update a user's status to 'active'. Active users can fully + interact with the platform + create + delete Delete a user by username or user_id. + edit-roles Edit a user's roles by username or id + list + show Show a single user. Use 'me' to indicate the currently + authenticated user. + suspend Update a user's status to 'suspended'. A suspended user cannot + log into the platform ——— Run `coder --help` for a list of global options. diff --git a/cli/testdata/coder_users_edit-roles_--help.golden b/cli/testdata/coder_users_edit-roles_--help.golden new file mode 100644 index 0000000000000..02dd9155b4d4e --- /dev/null +++ b/cli/testdata/coder_users_edit-roles_--help.golden @@ -0,0 +1,18 @@ +coder v0.0.0-devel + +USAGE: + coder users edit-roles [flags] + + Edit a user's roles by username or id + +OPTIONS: + --roles string-array + A list of roles to give to the user. This removes any existing roles + the user may have. The available roles are: auditor, member, owner, + template-admin, user-admin. + + -y, --yes bool + Bypass prompts. + +——— +Run `coder --help` for a list of global options. diff --git a/cli/usereditroles.go b/cli/usereditroles.go new file mode 100644 index 0000000000000..815d8f47dc186 --- /dev/null +++ b/cli/usereditroles.go @@ -0,0 +1,90 @@ +package cli + +import ( + "fmt" + "slices" + "sort" + "strings" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/cli/cliui" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/serpent" +) + +func (r *RootCmd) userEditRoles() *serpent.Command { + client := new(codersdk.Client) + + roles := rbac.SiteRoles() + + siteRoles := make([]string, 0) + for _, role := range roles { + siteRoles = append(siteRoles, role.Identifier.Name) + } + sort.Strings(siteRoles) + + var givenRoles []string + + cmd := &serpent.Command{ + Use: "edit-roles ", + Short: "Edit a user's roles by username or id", + Options: []serpent.Option{ + cliui.SkipPromptOption(), + { + Name: "roles", + Description: fmt.Sprintf("A list of roles to give to the user. This removes any existing roles the user may have. The available roles are: %s.", strings.Join(siteRoles, ", ")), + Flag: "roles", + Value: serpent.StringArrayOf(&givenRoles), + }, + }, + Middleware: serpent.Chain(serpent.RequireNArgs(1), r.InitClient(client)), + Handler: func(inv *serpent.Invocation) error { + ctx := inv.Context() + + user, err := client.User(ctx, inv.Args[0]) + if err != nil { + return xerrors.Errorf("fetch user: %w", err) + } + + userRoles, err := client.UserRoles(ctx, user.Username) + if err != nil { + return xerrors.Errorf("fetch user roles: %w", err) + } + + var selectedRoles []string + if len(givenRoles) > 0 { + // Make sure all of the given roles are valid site roles + for _, givenRole := range givenRoles { + if !slices.Contains(siteRoles, givenRole) { + siteRolesPretty := strings.Join(siteRoles, ", ") + return xerrors.Errorf("The role %s is not valid. Please use one or more of the following roles: %s\n", givenRole, siteRolesPretty) + } + } + + selectedRoles = givenRoles + } else { + selectedRoles, err = cliui.MultiSelect(inv, cliui.MultiSelectOptions{ + Message: "Select the roles you'd like to assign to the user", + Options: siteRoles, + Defaults: userRoles.Roles, + }) + if err != nil { + return xerrors.Errorf("selecting roles for user: %w", err) + } + } + + _, err = client.UpdateUserRoles(ctx, user.Username, codersdk.UpdateRoles{ + Roles: selectedRoles, + }) + if err != nil { + return xerrors.Errorf("update user roles: %w", err) + } + + return nil + }, + } + + return cmd +} diff --git a/cli/usereditroles_test.go b/cli/usereditroles_test.go new file mode 100644 index 0000000000000..bd12092501808 --- /dev/null +++ b/cli/usereditroles_test.go @@ -0,0 +1,62 @@ +package cli_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/testutil" +) + +var roles = []string{"auditor", "user-admin"} + +func TestUserEditRoles(t *testing.T) { + t.Parallel() + + t.Run("UpdateUserRoles", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + userAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleOwner()) + _, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) + + inv, root := clitest.New(t, "users", "edit-roles", member.Username, fmt.Sprintf("--roles=%s", strings.Join(roles, ","))) + clitest.SetupConfig(t, userAdmin, root) + + // Create context with timeout + ctx := testutil.Context(t, testutil.WaitShort) + + err := inv.WithContext(ctx).Run() + require.NoError(t, err) + + memberRoles, err := client.UserRoles(ctx, member.Username) + require.NoError(t, err) + + require.ElementsMatch(t, memberRoles.Roles, roles) + }) + + t.Run("UserNotFound", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + userAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleUserAdmin()) + + // Setup command with non-existent user + inv, root := clitest.New(t, "users", "edit-roles", "nonexistentuser") + clitest.SetupConfig(t, userAdmin, root) + + // Create context with timeout + ctx := testutil.Context(t, testutil.WaitShort) + + err := inv.WithContext(ctx).Run() + require.Error(t, err) + require.Contains(t, err.Error(), "fetch user") + }) +} diff --git a/cli/users.go b/cli/users.go index 3e6173880c0a3..fa15fcddad0ee 100644 --- a/cli/users.go +++ b/cli/users.go @@ -18,6 +18,7 @@ func (r *RootCmd) users() *serpent.Command { r.userList(), r.userSingle(), r.userDelete(), + r.userEditRoles(), r.createUserStatusCommand(codersdk.UserStatusActive), r.createUserStatusCommand(codersdk.UserStatusSuspended), }, diff --git a/docs/manifest.json b/docs/manifest.json index c3858dfd486ea..ea1d19561593f 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1605,6 +1605,11 @@ "description": "Delete a user by username or user_id.", "path": "reference/cli/users_delete.md" }, + { + "title": "users edit-roles", + "description": "Edit a user's roles by username or id", + "path": "reference/cli/users_edit-roles.md" + }, { "title": "users list", "path": "reference/cli/users_list.md" diff --git a/docs/reference/cli/users.md b/docs/reference/cli/users.md index 174e08fe9f3a0..d942699d6ee31 100644 --- a/docs/reference/cli/users.md +++ b/docs/reference/cli/users.md @@ -15,11 +15,12 @@ coder users [subcommand] ## Subcommands -| Name | Purpose | -|----------------------------------------------|---------------------------------------------------------------------------------------| -| [create](./users_create.md) | | -| [list](./users_list.md) | | -| [show](./users_show.md) | Show a single user. Use 'me' to indicate the currently authenticated user. | -| [delete](./users_delete.md) | Delete a user by username or user_id. | -| [activate](./users_activate.md) | Update a user's status to 'active'. Active users can fully interact with the platform | -| [suspend](./users_suspend.md) | Update a user's status to 'suspended'. A suspended user cannot log into the platform | +| Name | Purpose | +|--------------------------------------------------|---------------------------------------------------------------------------------------| +| [create](./users_create.md) | | +| [list](./users_list.md) | | +| [show](./users_show.md) | Show a single user. Use 'me' to indicate the currently authenticated user. | +| [delete](./users_delete.md) | Delete a user by username or user_id. | +| [edit-roles](./users_edit-roles.md) | Edit a user's roles by username or id | +| [activate](./users_activate.md) | Update a user's status to 'active'. Active users can fully interact with the platform | +| [suspend](./users_suspend.md) | Update a user's status to 'suspended'. A suspended user cannot log into the platform | diff --git a/docs/reference/cli/users_edit-roles.md b/docs/reference/cli/users_edit-roles.md new file mode 100644 index 0000000000000..23e0baa42afff --- /dev/null +++ b/docs/reference/cli/users_edit-roles.md @@ -0,0 +1,28 @@ + +# users edit-roles + +Edit a user's roles by username or id + +## Usage + +```console +coder users edit-roles [flags] +``` + +## Options + +### -y, --yes + +| | | +|------|-------------------| +| Type | bool | + +Bypass prompts. + +### --roles + +| | | +|------|---------------------------| +| Type | string-array | + +A list of roles to give to the user. This removes any existing roles the user may have. The available roles are: auditor, member, owner, template-admin, user-admin. From a7646d152481f64f4b6ab141b2266c8ff15fc25e Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 15 Apr 2025 20:22:21 -0500 Subject: [PATCH 101/384] chore: disable authz-header in all builds (#17409) Header payload being large is causing some issues in dev builds. Another method of opting in needs to be determined --- coderd/coderd.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index d8e9d96ff7106..72ebce81120fa 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -464,8 +464,16 @@ func New(options *Options) *API { r := chi.NewRouter() // We add this middleware early, to make sure that authorization checks made // by other middleware get recorded. + //nolint:revive,staticcheck // This block will be re-enabled, not going to remove it if buildinfo.IsDev() { - r.Use(httpmw.RecordAuthzChecks) + // TODO: Find another solution to opt into these checks. + // If the header grows too large, it breaks `fetch()` requests. + // Temporarily disabling this until we can find a better solution. + // One idea is to include checking the request for `X-Authz-Record=true` + // header. To opt in on a per-request basis. + // Some authz calls (like filtering lists) might be able to be + // summarized better to condense the header payload. + // r.Use(httpmw.RecordAuthzChecks) } ctx, cancel := context.WithCancel(context.Background()) From 1db70bef5df8f4d88faad4ef9ad9afa06b4e5e2f Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 16 Apr 2025 10:00:25 +0100 Subject: [PATCH 102/384] feat: create dynamic parameter component (#17351) - Create DynamicParameter component and test with locally run preview websocket. - Adapt CreateWorkspacePageExperimental to work with PreviewParameter instead of TemplateVersionParameter - Small changes to checkbox, multi-select combobox and radiogroup The websocket implementation is temporary for testing purpose with a locally run preview websocket --- site/src/components/Checkbox/Checkbox.tsx | 3 + .../MultiSelectCombobox.stories.tsx | 2 +- .../MultiSelectCombobox.tsx | 6 +- site/src/components/RadioGroup/RadioGroup.tsx | 2 +- .../DynamicParameter/DynamicParameter.tsx | 579 ++++++++++++++++++ .../CreateWorkspacePageExperimental.tsx | 97 ++- .../CreateWorkspacePageViewExperimental.tsx | 182 ++++-- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 2 +- .../IdpSyncPage/IdpGroupSyncForm.tsx | 2 +- .../IdpSyncPage/IdpRoleSyncForm.tsx | 2 +- 10 files changed, 760 insertions(+), 117 deletions(-) create mode 100644 site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx diff --git a/site/src/components/Checkbox/Checkbox.tsx b/site/src/components/Checkbox/Checkbox.tsx index 304a04ad5b4ca..6bc1338955122 100644 --- a/site/src/components/Checkbox/Checkbox.tsx +++ b/site/src/components/Checkbox/Checkbox.tsx @@ -8,6 +8,9 @@ import * as React from "react"; import { cn } from "utils/cn"; +/** + * To allow for an indeterminate state the checkbox must be controlled, otherwise the checked prop would remain undefined + */ export const Checkbox = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.stories.tsx b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.stories.tsx index fd35842e0fddc..109a60e60448d 100644 --- a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.stories.tsx +++ b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.stories.tsx @@ -16,7 +16,7 @@ const meta: Meta = { All organizations selected

    ), - defaultOptions: organizations.map((org) => ({ + options: organizations.map((org) => ({ label: org.display_name, value: org.id, })), diff --git a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx index 83f2aeed41cd4..249af7918df28 100644 --- a/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx +++ b/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx @@ -203,9 +203,11 @@ export const MultiSelectCombobox = forwardRef< const [open, setOpen] = useState(false); const [onScrollbar, setOnScrollbar] = useState(false); const [isLoading, setIsLoading] = useState(false); - const dropdownRef = useRef(null); // Added this + const dropdownRef = useRef(null); - const [selected, setSelected] = useState(value || []); + const [selected, setSelected] = useState( + arrayDefaultOptions ?? [], + ); const [options, setOptions] = useState( transitionToGroupOption(arrayDefaultOptions, groupBy), ); diff --git a/site/src/components/RadioGroup/RadioGroup.tsx b/site/src/components/RadioGroup/RadioGroup.tsx index 9be24d6e26f33..3b63a91f40087 100644 --- a/site/src/components/RadioGroup/RadioGroup.tsx +++ b/site/src/components/RadioGroup/RadioGroup.tsx @@ -34,7 +34,7 @@ export const RadioGroupItem = React.forwardRef< focus:outline-none focus-visible:ring-2 focus-visible:ring-content-link focus-visible:ring-offset-4 focus-visible:ring-offset-surface-primary disabled:cursor-not-allowed disabled:opacity-25 disabled:border-surface-invert-primary - hover:border-border-hover`, + hover:border-border-hover data-[state=checked]:border-border-hover`, className, )} {...props} diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx new file mode 100644 index 0000000000000..d3f2cbbd69fa6 --- /dev/null +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -0,0 +1,579 @@ +import type { + PreviewParameter, + PreviewParameterOption, + WorkspaceBuildParameter, +} from "api/typesGenerated"; +import { Badge } from "components/Badge/Badge"; +import { Checkbox } from "components/Checkbox/Checkbox"; +import { ExternalImage } from "components/ExternalImage/ExternalImage"; +import { Input } from "components/Input/Input"; +import { Label } from "components/Label/Label"; +import { MemoizedMarkdown } from "components/Markdown/Markdown"; +import { + MultiSelectCombobox, + type Option, +} from "components/MultiSelectCombobox/MultiSelectCombobox"; +import { RadioGroup, RadioGroupItem } from "components/RadioGroup/RadioGroup"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "components/Select/Select"; +import { Switch } from "components/Switch/Switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { Info, Settings, TriangleAlert } from "lucide-react"; +import { type FC, useId } from "react"; +import type { AutofillBuildParameter } from "utils/richParameters"; +import * as Yup from "yup"; + +export interface DynamicParameterProps { + parameter: PreviewParameter; + onChange: (value: string) => void; + disabled?: boolean; + isPreset?: boolean; +} + +export const DynamicParameter: FC = ({ + parameter, + onChange, + disabled, + isPreset, +}) => { + const id = useId(); + + return ( +
    + + + {parameter.diagnostics.length > 0 && ( + + )} +
    + ); +}; + +interface ParameterLabelProps { + parameter: PreviewParameter; + isPreset?: boolean; +} + +const ParameterLabel: FC = ({ parameter, isPreset }) => { + const hasDescription = parameter.description && parameter.description !== ""; + const displayName = parameter.display_name + ? parameter.display_name + : parameter.name; + + return ( +
    + {parameter.icon && ( + + + + )} + +
    + + + {hasDescription && ( +
    + + {parameter.description} + +
    + )} +
    +
    + ); +}; + +interface ParameterFieldProps { + parameter: PreviewParameter; + onChange: (value: string) => void; + disabled?: boolean; + id: string; +} + +const ParameterField: FC = ({ + parameter, + onChange, + disabled, + id, +}) => { + const value = parameter.value.valid ? parameter.value.value : ""; + const defaultValue = parameter.default_value.valid + ? parameter.default_value.value + : ""; + + switch (parameter.form_type) { + case "dropdown": + return ( + + ); + + case "multi-select": { + // Map parameter options to MultiSelectCombobox options format + const comboboxOptions: Option[] = parameter.options.map((opt) => ({ + value: opt.value.value, + label: opt.name, + disable: false, + })); + + const defaultOptions: Option[] = JSON.parse(defaultValue).map( + (val: string) => { + const option = parameter.options.find((o) => o.value.value === val); + return { + value: val, + label: option?.name || val, + disable: false, + }; + }, + ); + + return ( + { + const values = newValues.map((option) => option.value); + onChange(JSON.stringify(values)); + }} + hidePlaceholderWhenSelected + placeholder="Select option" + emptyIndicator={ +

    + No results found +

    + } + disabled={disabled} + /> + ); + } + + case "switch": + return ( + { + onChange(checked ? "true" : "false"); + }} + disabled={disabled} + /> + ); + + case "radio": + return ( + + {parameter.options.map((option) => ( +
    + + +
    + ))} +
    + ); + + case "checkbox": + return ( +
    + { + onChange(checked ? "true" : "false"); + }} + disabled={disabled} + /> + +
    + ); + case "input": { + const inputType = parameter.type === "number" ? "number" : "text"; + const inputProps: Record = {}; + + if (parameter.type === "number") { + const validations = parameter.validations[0] || {}; + const { validation_min, validation_max } = validations; + + if (validation_min !== null) { + inputProps.min = validation_min; + } + + if (validation_max !== null) { + inputProps.max = validation_max; + } + } + + return ( + onChange(e.target.value)} + disabled={disabled} + placeholder={ + (parameter.styling as { placehholder?: string })?.placehholder + } + {...inputProps} + /> + ); + } + } +}; + +interface OptionDisplayProps { + option: PreviewParameterOption; +} + +const OptionDisplay: FC = ({ option }) => { + return ( +
    + {option.icon && ( + + )} + {option.name} + {option.description && ( + + + + + + + {option.description} + + + + )} +
    + ); +}; + +interface ParameterDiagnosticsProps { + diagnostics: PreviewParameter["diagnostics"]; +} + +const ParameterDiagnostics: FC = ({ + diagnostics, +}) => { + return ( +
    + {diagnostics.map((diagnostic, index) => ( +
    +
    {diagnostic.summary}
    + {diagnostic.detail &&
    {diagnostic.detail}
    } +
    + ))} +
    + ); +}; + +export const getInitialParameterValues = ( + params: PreviewParameter[], + autofillParams?: AutofillBuildParameter[], +): WorkspaceBuildParameter[] => { + return params.map((parameter) => { + // Short-circuit for ephemeral parameters, which are always reset to + // the template-defined default. + if (parameter.ephemeral) { + return { + name: parameter.name, + value: parameter.default_value.valid + ? parameter.default_value.value + : "", + }; + } + + const autofillParam = autofillParams?.find( + ({ name }) => name === parameter.name, + ); + + return { + name: parameter.name, + value: + autofillParam && + isValidValue(parameter, autofillParam) && + autofillParam.value + ? autofillParam.value + : "", + }; + }); +}; + +const isValidValue = ( + previewParam: PreviewParameter, + buildParam: WorkspaceBuildParameter, +) => { + if (previewParam.options.length > 0) { + const validValues = previewParam.options.map( + (option) => option.value.value, + ); + return validValues.includes(buildParam.value); + } + + return true; +}; + +export const useValidationSchemaForDynamicParameters = ( + parameters?: PreviewParameter[], + lastBuildParameters?: WorkspaceBuildParameter[], +): Yup.AnySchema => { + if (!parameters) { + return Yup.object(); + } + + return Yup.array() + .of( + Yup.object().shape({ + name: Yup.string().required(), + value: Yup.string() + .test("verify with template", (val, ctx) => { + const name = ctx.parent.name; + const parameter = parameters.find( + (parameter) => parameter.name === name, + ); + if (parameter) { + switch (parameter.type) { + case "number": { + const minValidation = parameter.validations.find( + (v) => v.validation_min !== null, + ); + const maxValidation = parameter.validations.find( + (v) => v.validation_max !== null, + ); + + if ( + minValidation && + minValidation.validation_min !== null && + !maxValidation && + Number(val) < minValidation.validation_min + ) { + return ctx.createError({ + path: ctx.path, + message: + parameterError(parameter, val) ?? + `Value must be greater than ${minValidation.validation_min}.`, + }); + } + + if ( + !minValidation && + maxValidation && + maxValidation.validation_max !== null && + Number(val) > maxValidation.validation_max + ) { + return ctx.createError({ + path: ctx.path, + message: + parameterError(parameter, val) ?? + `Value must be less than ${maxValidation.validation_max}.`, + }); + } + + if ( + minValidation && + minValidation.validation_min !== null && + maxValidation && + maxValidation.validation_max !== null && + (Number(val) < minValidation.validation_min || + Number(val) > maxValidation.validation_max) + ) { + return ctx.createError({ + path: ctx.path, + message: + parameterError(parameter, val) ?? + `Value must be between ${minValidation.validation_min} and ${maxValidation.validation_max}.`, + }); + } + + const monotonic = parameter.validations.find( + (v) => + v.validation_monotonic !== null && + v.validation_monotonic !== "", + ); + + if (monotonic && lastBuildParameters) { + const lastBuildParameter = lastBuildParameters.find( + (last: { name: string }) => last.name === name, + ); + if (lastBuildParameter) { + switch (monotonic.validation_monotonic) { + case "increasing": + if (Number(lastBuildParameter.value) > Number(val)) { + return ctx.createError({ + path: ctx.path, + message: `Value must only ever increase (last value was ${lastBuildParameter.value})`, + }); + } + break; + case "decreasing": + if (Number(lastBuildParameter.value) < Number(val)) { + return ctx.createError({ + path: ctx.path, + message: `Value must only ever decrease (last value was ${lastBuildParameter.value})`, + }); + } + break; + } + } + } + break; + } + case "string": { + const regex = parameter.validations.find( + (v) => + v.validation_regex !== null && v.validation_regex !== "", + ); + if (!regex || !regex.validation_regex) { + return true; + } + + if (val && !new RegExp(regex.validation_regex).test(val)) { + return ctx.createError({ + path: ctx.path, + message: parameterError(parameter, val), + }); + } + break; + } + } + } + return true; + }), + }), + ) + .required(); +}; + +const parameterError = ( + parameter: PreviewParameter, + value?: string, +): string | undefined => { + const validation_error = parameter.validations.find( + (v) => v.validation_error !== null, + ); + const minValidation = parameter.validations.find( + (v) => v.validation_min !== null, + ); + const maxValidation = parameter.validations.find( + (v) => v.validation_max !== null, + ); + + if (!validation_error || !value) { + return; + } + + const r = new Map([ + [ + "{min}", + minValidation ? (minValidation.validation_min?.toString() ?? "") : "", + ], + [ + "{max}", + maxValidation ? (maxValidation.validation_max?.toString() ?? "") : "", + ], + ["{value}", value], + ]); + return validation_error.validation_error.replace( + /{min}|{max}|{value}/g, + (match) => r.get(match) || "", + ); +}; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx index 8598085c948e5..14f34a2e29f0b 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx @@ -1,30 +1,34 @@ -import { API } from "api/api"; import type { ApiErrorResponse } from "api/errors"; import { checkAuthorization } from "api/queries/authCheck"; import { - richParameters, templateByName, templateVersionExternalAuth, templateVersionPresets, } from "api/queries/templates"; import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces"; import type { - TemplateVersionParameter, - UserParameter, + DynamicParametersRequest, + DynamicParametersResponse, + Template, Workspace, } from "api/typesGenerated"; import { Loader } from "components/Loader/Loader"; import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useEffectEvent } from "hooks/hookPolyfills"; -import { useDashboard } from "modules/dashboard/useDashboard"; import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; -import { type FC, useCallback, useEffect, useRef, useState } from "react"; +import { + type FC, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { pageTitle } from "utils/page"; import type { AutofillBuildParameter } from "utils/richParameters"; -import { paramsUsedToCreateWorkspace } from "utils/workspace"; import { CreateWorkspacePageViewExperimental } from "./CreateWorkspacePageViewExperimental"; export const createWorkspaceModes = ["form", "auto", "duplicate"] as const; export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number]; @@ -32,7 +36,6 @@ import { type CreateWorkspacePermissions, createWorkspaceChecks, } from "./permissions"; - export type ExternalAuthPollingState = "idle" | "polling" | "abandoned"; const CreateWorkspacePageExperimental: FC = () => { @@ -41,7 +44,11 @@ const CreateWorkspacePageExperimental: FC = () => { const { user: me } = useAuthenticated(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const { experiments } = useDashboard(); + + const [currentResponse, setCurrentResponse] = + useState(null); + const [wsResponseId, setWSResponseId] = useState(0); + const sendMessage = (message: DynamicParametersRequest) => {}; const customVersionId = searchParams.get("version") ?? undefined; const defaultName = searchParams.get("name"); @@ -72,14 +79,8 @@ const CreateWorkspacePageExperimental: FC = () => { ); const realizedVersionId = customVersionId ?? templateQuery.data?.active_version_id; + const organizationId = templateQuery.data?.organization_id; - const richParametersQuery = useQuery({ - ...richParameters(realizedVersionId ?? ""), - enabled: realizedVersionId !== undefined, - }); - const realizedParameters = richParametersQuery.data - ? richParametersQuery.data.filter(paramsUsedToCreateWorkspace) - : undefined; const { externalAuth, @@ -89,11 +90,8 @@ const CreateWorkspacePageExperimental: FC = () => { } = useExternalAuth(realizedVersionId); const isLoadingFormData = - templateQuery.isLoading || - permissionsQuery.isLoading || - richParametersQuery.isLoading; - const loadFormDataError = - templateQuery.error ?? permissionsQuery.error ?? richParametersQuery.error; + templateQuery.isLoading || permissionsQuery.isLoading; + const loadFormDataError = templateQuery.error ?? permissionsQuery.error; const title = autoCreateWorkspaceMutation.isLoading ? "Creating workspace..." @@ -107,16 +105,7 @@ const CreateWorkspacePageExperimental: FC = () => { ); // Auto fill parameters - const autofillEnabled = experiments.includes("auto-fill-parameters"); - const userParametersQuery = useQuery({ - queryKey: ["userParameters"], - queryFn: () => API.getUserParameters(templateQuery.data!.id), - enabled: autofillEnabled && templateQuery.isSuccess, - }); - const autofillParameters = getAutofillParameters( - searchParams, - userParametersQuery.data ? userParametersQuery.data : [], - ); + const autofillParameters = getAutofillParameters(searchParams); const autoCreationStartedRef = useRef(false); const automateWorkspaceCreation = useEffectEvent(async () => { @@ -146,10 +135,7 @@ const CreateWorkspacePageExperimental: FC = () => { externalAuth?.every((auth) => auth.optional || auth.authenticated), ); - let autoCreateReady = - mode === "auto" && - (!autofillEnabled || userParametersQuery.isSuccess) && - hasAllRequiredExternalAuth; + let autoCreateReady = mode === "auto" && hasAllRequiredExternalAuth; // `mode=auto` was set, but a prerequisite has failed, and so auto-mode should be abandoned. if ( @@ -181,17 +167,29 @@ const CreateWorkspacePageExperimental: FC = () => { } }, [automateWorkspaceCreation, autoCreateReady]); + const sortedParams = useMemo(() => { + if (!currentResponse?.parameters) { + return []; + } + return [...currentResponse.parameters].sort((a, b) => a.order - b.order); + }, [currentResponse?.parameters]); + return ( <> Codestin Search App - {isLoadingFormData || isLoadingExternalAuth || autoCreateReady ? ( + {!currentResponse || + !templateQuery.data || + isLoadingFormData || + isLoadingExternalAuth || + autoCreateReady ? ( ) : ( { autoCreateWorkspaceMutation.error } resetMutation={createWorkspaceMutation.reset} - template={templateQuery.data!} + template={templateQuery.data} versionId={realizedVersionId} externalAuth={externalAuth ?? []} externalAuthPollingState={externalAuthPollingState} startPollingExternalAuth={startPollingExternalAuth} hasAllRequiredExternalAuth={hasAllRequiredExternalAuth} permissions={permissionsQuery.data as CreateWorkspacePermissions} - parameters={realizedParameters as TemplateVersionParameter[]} + parameters={sortedParams} presets={templateVersionPresetsQuery.data ?? []} creatingWorkspace={createWorkspaceMutation.isLoading} + setWSResponseId={setWSResponseId} + sendMessage={sendMessage} onCancel={() => { navigate(-1); }} onSubmit={async (request, owner) => { + let workspaceRequest = request; if (realizedVersionId) { - request = { + workspaceRequest = { ...request, template_id: undefined, template_version_id: realizedVersionId, @@ -225,7 +226,7 @@ const CreateWorkspacePageExperimental: FC = () => { } const workspace = await createWorkspaceMutation.mutateAsync({ - ...request, + ...workspaceRequest, userId: owner.id, }); onCreateWorkspace(workspace); @@ -286,13 +287,7 @@ const useExternalAuth = (versionId: string | undefined) => { const getAutofillParameters = ( urlSearchParams: URLSearchParams, - userParameters: UserParameter[], ): AutofillBuildParameter[] => { - const userParamMap = userParameters.reduce((acc, param) => { - acc.set(param.name, param); - return acc; - }, new Map()); - const buildValues: AutofillBuildParameter[] = Array.from( urlSearchParams.keys(), ) @@ -300,18 +295,8 @@ const getAutofillParameters = ( .map((key) => { const name = key.replace("param.", ""); const value = urlSearchParams.get(key) ?? ""; - // URL should take precedence over user parameters - userParamMap.delete(name); return { name, value, source: "url" }; }); - - for (const param of userParamMap.values()) { - buildValues.push({ - name: param.name, - value: param.value, - source: "user_history", - }); - } return buildValues; }; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index ff8c2836be311..49fd6e9188960 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -1,5 +1,9 @@ -import type { Interpolation, Theme } from "@emotion/react"; import type * as TypesGen from "api/typesGenerated"; +import type { + DynamicParametersRequest, + PreviewDiagnostics, + PreviewParameter, +} from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; @@ -9,12 +13,18 @@ import { SelectFilter } from "components/Filter/SelectFilter"; import { Input } from "components/Input/Input"; import { Label } from "components/Label/Label"; import { Pill } from "components/Pill/Pill"; -import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"; import { Spinner } from "components/Spinner/Spinner"; import { Stack } from "components/Stack/Stack"; +import { Switch } from "components/Switch/Switch"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import { type FormikContextType, useFormik } from "formik"; +import { useDebouncedFunction } from "hooks/debounce"; import { ArrowLeft } from "lucide-react"; +import { + DynamicParameter, + getInitialParameterValues, + useValidationSchemaForDynamicParameters, +} from "modules/workspaces/DynamicParameter/DynamicParameter"; import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; import { type FC, @@ -25,11 +35,7 @@ import { useState, } from "react"; import { getFormHelpers, nameValidator } from "utils/formUtils"; -import { - type AutofillBuildParameter, - getInitialRichParameterValues, - useValidationSchemaForRichParameters, -} from "utils/richParameters"; +import type { AutofillBuildParameter } from "utils/richParameters"; import * as Yup from "yup"; import type { CreateWorkspaceMode, @@ -37,65 +43,67 @@ import type { } from "./CreateWorkspacePage"; import { ExternalAuthButton } from "./ExternalAuthButton"; import type { CreateWorkspacePermissions } from "./permissions"; -export const Language = { - duplicationWarning: - "Duplicating a workspace only copies its parameters. No state from the old workspace is copied over.", -} as const; export interface CreateWorkspacePageViewExperimentalProps { - mode: CreateWorkspaceMode; + autofillParameters: AutofillBuildParameter[]; + creatingWorkspace: boolean; defaultName?: string | null; + defaultOwner: TypesGen.User; + diagnostics: PreviewDiagnostics; disabledParams?: string[]; error: unknown; - resetMutation: () => void; - defaultOwner: TypesGen.User; - template: TypesGen.Template; - versionId?: string; externalAuth: TypesGen.TemplateVersionExternalAuth[]; externalAuthPollingState: ExternalAuthPollingState; - startPollingExternalAuth: () => void; hasAllRequiredExternalAuth: boolean; - parameters: TypesGen.TemplateVersionParameter[]; - autofillParameters: AutofillBuildParameter[]; - presets: TypesGen.Preset[]; + mode: CreateWorkspaceMode; + parameters: PreviewParameter[]; permissions: CreateWorkspacePermissions; - creatingWorkspace: boolean; + presets: TypesGen.Preset[]; + template: TypesGen.Template; + versionId?: string; onCancel: () => void; onSubmit: ( req: TypesGen.CreateWorkspaceRequest, owner: TypesGen.User, ) => void; + resetMutation: () => void; + sendMessage: (message: DynamicParametersRequest) => void; + setWSResponseId: (value: React.SetStateAction) => void; + startPollingExternalAuth: () => void; } export const CreateWorkspacePageViewExperimental: FC< CreateWorkspacePageViewExperimentalProps > = ({ - mode, + autofillParameters, + creatingWorkspace, defaultName, + defaultOwner, + diagnostics, disabledParams, error, - resetMutation, - defaultOwner, - template, - versionId, externalAuth, externalAuthPollingState, - startPollingExternalAuth, hasAllRequiredExternalAuth, + mode, parameters, - autofillParameters, - presets = [], permissions, - creatingWorkspace, + presets = [], + template, + versionId, onSubmit, onCancel, + resetMutation, + sendMessage, + setWSResponseId, + startPollingExternalAuth, }) => { const [owner, setOwner] = useState(defaultOwner); const [suggestedName, setSuggestedName] = useState(() => generateWorkspaceName(), ); + const [showPresetParameters, setShowPresetParameters] = useState(false); const id = useId(); - const rerollSuggestedName = useCallback(() => { setSuggestedName(() => generateWorkspaceName()); }, []); @@ -105,16 +113,19 @@ export const CreateWorkspacePageViewExperimental: FC< initialValues: { name: defaultName ?? "", template_id: template.id, - rich_parameter_values: getInitialRichParameterValues( + rich_parameter_values: getInitialParameterValues( parameters, autofillParameters, ), }, validationSchema: Yup.object({ name: nameValidator("Workspace Name"), - rich_parameter_values: useValidationSchemaForRichParameters(parameters), + rich_parameter_values: + useValidationSchemaForDynamicParameters(parameters), }), enableReinitialize: true, + validateOnChange: false, + validateOnBlur: true, onSubmit: (request) => { if (!hasAllRequiredExternalAuth) { return; @@ -195,10 +206,64 @@ export const CreateWorkspacePageViewExperimental: FC< presetOptions, selectedPresetIndex, presets, - parameters, form.setFieldValue, + parameters, ]); + const sendDynamicParamsRequest = ( + parameter: PreviewParameter, + value: string, + ) => { + const formInputs = Object.fromEntries( + form.values.rich_parameter_values?.map((value) => { + return [value.name, value.value]; + }) ?? [], + ); + // Update the input for the changed parameter + formInputs[parameter.name] = value; + + setWSResponseId((prevId) => { + const newId = prevId + 1; + const request: DynamicParametersRequest = { + id: newId, + inputs: formInputs, + }; + sendMessage(request); + return newId; + }); + }; + + const { debounced: handleChangeDebounced } = useDebouncedFunction( + async ( + parameter: PreviewParameter, + parameterField: string, + value: string, + ) => { + await form.setFieldValue(parameterField, { + name: parameter.form_type, + value, + }); + sendDynamicParamsRequest(parameter, value); + }, + 500, + ); + + const handleChange = async ( + parameter: PreviewParameter, + parameterField: string, + value: string, + ) => { + if (parameter.form_type === "input" || parameter.form_type === "textarea") { + handleChangeDebounced(parameter, parameterField, value); + } else { + await form.setFieldValue(parameterField, { + name: parameter.form_type, + value, + }); + sendDynamicParamsRequest(parameter, value); + } + }; + return ( <>
    @@ -244,7 +309,8 @@ export const CreateWorkspacePageViewExperimental: FC< dismissible data-testid="duplication-warning" > - {Language.duplicationWarning} + Duplicating a workspace only copies its parameters. No state from + the old workspace is copied over. )} @@ -353,9 +419,8 @@ export const CreateWorkspacePageViewExperimental: FC<

    Parameters

    - These are the settings used by your template. Please note that - immutable parameters cannot be modified once the workspace is - created. + These are the settings used by your template. Immutable + parameters cannot be modified once the workspace is created.

    {presets.length > 0 && ( @@ -382,6 +447,16 @@ export const CreateWorkspacePageViewExperimental: FC< selectedOption={presetOptions[selectedPresetIndex]} />
    + + + +
    )} @@ -390,26 +465,32 @@ export const CreateWorkspacePageViewExperimental: FC< {parameters.map((parameter, index) => { const parameterField = `rich_parameter_values.${index}`; const parameterInputName = `${parameterField}.value`; + const isPresetParameter = presetParameterNames.includes( + parameter.name, + ); const isDisabled = disabledParams?.includes( parameter.name.toLowerCase().replace(/ /g, "_"), ) || + (parameter.styling as { disabled?: boolean })?.disabled || creatingWorkspace || - presetParameterNames.includes(parameter.name); + isPresetParameter; + + // Hide preset parameters if showPresetParameters is false + if (!showPresetParameters && isPresetParameter) { + return null; + } return ( - { - await form.setFieldValue(parameterField, { - name: parameter.name, - value, - }); - }} key={parameter.name} parameter={parameter} - parameterAutofill={autofillByName[parameter.name]} + onChange={(value) => + handleChange(parameter, parameterField, value) + } disabled={isDisabled} + isPreset={isPresetParameter} /> ); })} @@ -431,10 +512,3 @@ export const CreateWorkspacePageViewExperimental: FC< ); }; - -const styles = { - description: (theme) => ({ - fontSize: 13, - color: theme.palette.text.secondary, - }), -} satisfies Record>; diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index aa39906f09370..f99c1d04fee14 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -257,7 +257,7 @@ export const IdpOrgSyncPageView: FC = ({ className="min-w-60 max-w-3xl" value={coderOrgs} onChange={setCoderOrgs} - defaultOptions={organizations.map((org) => ({ + options={organizations.map((org) => ({ label: org.display_name, value: org.id, }))} diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index 5340ec99dda79..284267f4487e1 100644 --- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -259,7 +259,7 @@ export const IdpGroupSyncForm: FC = ({ className="min-w-60 max-w-3xl" value={coderGroups} onChange={setCoderGroups} - defaultOptions={groups.map((group) => ({ + options={groups.map((group) => ({ label: group.display_name || group.name, value: group.id, }))} diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx index faeaf0773dffd..0825ab4217395 100644 --- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx +++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpRoleSyncForm.tsx @@ -200,7 +200,7 @@ export const IdpRoleSyncForm: FC = ({ className="min-w-60 max-w-3xl" value={coderRoles} onChange={setCoderRoles} - defaultOptions={roles.map((role) => ({ + options={roles.map((role) => ({ label: role.display_name || role.name, value: role.name, }))} From f8971bb3cc01d81b3085b2b3c9253d8d340d125c Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 16 Apr 2025 11:10:39 +0200 Subject: [PATCH 103/384] feat: add path & method labels to prometheus metrics for current requests (#17362) Closes: #17212 --- coderd/httpmw/prometheus.go | 52 ++++++++++++++---- coderd/httpmw/prometheus_test.go | 91 ++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 10 deletions(-) diff --git a/coderd/httpmw/prometheus.go b/coderd/httpmw/prometheus.go index b96be84e879e3..8b7b33381c74d 100644 --- a/coderd/httpmw/prometheus.go +++ b/coderd/httpmw/prometheus.go @@ -3,6 +3,7 @@ package httpmw import ( "net/http" "strconv" + "strings" "time" "github.com/go-chi/chi/v5" @@ -22,18 +23,18 @@ func Prometheus(register prometheus.Registerer) func(http.Handler) http.Handler Name: "requests_processed_total", Help: "The total number of processed API requests", }, []string{"code", "method", "path"}) - requestsConcurrent := factory.NewGauge(prometheus.GaugeOpts{ + requestsConcurrent := factory.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "coderd", Subsystem: "api", Name: "concurrent_requests", Help: "The number of concurrent API requests.", - }) - websocketsConcurrent := factory.NewGauge(prometheus.GaugeOpts{ + }, []string{"method", "path"}) + websocketsConcurrent := factory.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "coderd", Subsystem: "api", Name: "concurrent_websockets", Help: "The total number of concurrent API websockets.", - }) + }, []string{"path"}) websocketsDist := factory.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "coderd", Subsystem: "api", @@ -61,7 +62,6 @@ func Prometheus(register prometheus.Registerer) func(http.Handler) http.Handler var ( start = time.Now() method = r.Method - rctx = chi.RouteContext(r.Context()) ) sw, ok := w.(*tracing.StatusWriter) @@ -72,16 +72,18 @@ func Prometheus(register prometheus.Registerer) func(http.Handler) http.Handler var ( dist *prometheus.HistogramVec distOpts []string + path = getRoutePattern(r) ) + // We want to count WebSockets separately. if httpapi.IsWebsocketUpgrade(r) { - websocketsConcurrent.Inc() - defer websocketsConcurrent.Dec() + websocketsConcurrent.WithLabelValues(path).Inc() + defer websocketsConcurrent.WithLabelValues(path).Dec() dist = websocketsDist } else { - requestsConcurrent.Inc() - defer requestsConcurrent.Dec() + requestsConcurrent.WithLabelValues(method, path).Inc() + defer requestsConcurrent.WithLabelValues(method, path).Dec() dist = requestsDist distOpts = []string{method} @@ -89,7 +91,6 @@ func Prometheus(register prometheus.Registerer) func(http.Handler) http.Handler next.ServeHTTP(w, r) - path := rctx.RoutePattern() distOpts = append(distOpts, path) statusStr := strconv.Itoa(sw.Status) @@ -98,3 +99,34 @@ func Prometheus(register prometheus.Registerer) func(http.Handler) http.Handler }) } } + +func getRoutePattern(r *http.Request) string { + rctx := chi.RouteContext(r.Context()) + if rctx == nil { + return "" + } + + if pattern := rctx.RoutePattern(); pattern != "" { + // Pattern is already available + return pattern + } + + routePath := r.URL.Path + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } + + tctx := chi.NewRouteContext() + routes := rctx.Routes + if routes != nil && !routes.Match(tctx, r.Method, routePath) { + // No matching pattern. /api/* requests will be matched as "UNKNOWN" + // All other ones will be matched as "STATIC". + if strings.HasPrefix(routePath, "/api/") { + return "UNKNOWN" + } + return "STATIC" + } + + // tctx has the updated pattern, since Match mutates it + return tctx.RoutePattern() +} diff --git a/coderd/httpmw/prometheus_test.go b/coderd/httpmw/prometheus_test.go index a51eea5d00312..d40558f5ca5e7 100644 --- a/coderd/httpmw/prometheus_test.go +++ b/coderd/httpmw/prometheus_test.go @@ -8,14 +8,19 @@ import ( "github.com/go-chi/chi/v5" "github.com/prometheus/client_golang/prometheus" + cm "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/tracing" + "github.com/coder/coder/v2/testutil" + "github.com/coder/websocket" ) func TestPrometheus(t *testing.T) { t.Parallel() + t.Run("All", func(t *testing.T) { t.Parallel() req := httptest.NewRequest("GET", "/", nil) @@ -29,4 +34,90 @@ func TestPrometheus(t *testing.T) { require.NoError(t, err) require.Greater(t, len(metrics), 0) }) + + t.Run("Concurrent", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + reg := prometheus.NewRegistry() + promMW := httpmw.Prometheus(reg) + + // Create a test handler to simulate a WebSocket connection + testHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + conn, err := websocket.Accept(rw, r, nil) + if !assert.NoError(t, err, "failed to accept websocket") { + return + } + defer conn.Close(websocket.StatusGoingAway, "") + }) + + wrappedHandler := promMW(testHandler) + + r := chi.NewRouter() + r.Use(tracing.StatusWriterMiddleware, promMW) + r.Get("/api/v2/build/{build}/logs", func(rw http.ResponseWriter, r *http.Request) { + wrappedHandler.ServeHTTP(rw, r) + }) + + srv := httptest.NewServer(r) + defer srv.Close() + // nolint: bodyclose + conn, _, err := websocket.Dial(ctx, srv.URL+"/api/v2/build/1/logs", nil) + require.NoError(t, err, "failed to dial WebSocket") + defer conn.Close(websocket.StatusNormalClosure, "") + + metrics, err := reg.Gather() + require.NoError(t, err) + require.Greater(t, len(metrics), 0) + metricLabels := getMetricLabels(metrics) + + concurrentWebsockets, ok := metricLabels["coderd_api_concurrent_websockets"] + require.True(t, ok, "coderd_api_concurrent_websockets metric not found") + require.Equal(t, "/api/v2/build/{build}/logs", concurrentWebsockets["path"]) + }) + + t.Run("UserRoute", func(t *testing.T) { + t.Parallel() + reg := prometheus.NewRegistry() + promMW := httpmw.Prometheus(reg) + + r := chi.NewRouter() + r.With(promMW).Get("/api/v2/users/{user}", func(w http.ResponseWriter, r *http.Request) {}) + + req := httptest.NewRequest("GET", "/api/v2/users/john", nil) + + sw := &tracing.StatusWriter{ResponseWriter: httptest.NewRecorder()} + + r.ServeHTTP(sw, req) + + metrics, err := reg.Gather() + require.NoError(t, err) + require.Greater(t, len(metrics), 0) + metricLabels := getMetricLabels(metrics) + + reqProcessed, ok := metricLabels["coderd_api_requests_processed_total"] + require.True(t, ok, "coderd_api_requests_processed_total metric not found") + require.Equal(t, "/api/v2/users/{user}", reqProcessed["path"]) + require.Equal(t, "GET", reqProcessed["method"]) + + concurrentRequests, ok := metricLabels["coderd_api_concurrent_requests"] + require.True(t, ok, "coderd_api_concurrent_requests metric not found") + require.Equal(t, "/api/v2/users/{user}", concurrentRequests["path"]) + require.Equal(t, "GET", concurrentRequests["method"]) + }) +} + +func getMetricLabels(metrics []*cm.MetricFamily) map[string]map[string]string { + metricLabels := map[string]map[string]string{} + for _, metricFamily := range metrics { + metricName := metricFamily.GetName() + metricLabels[metricName] = map[string]string{} + for _, metric := range metricFamily.GetMetric() { + for _, labelPair := range metric.GetLabel() { + metricLabels[metricName][labelPair.GetName()] = labelPair.GetValue() + } + } + } + return metricLabels } From 8cc743a812baa29ad52af2aea2440a8e7b97429f Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 16 Apr 2025 02:44:33 -0700 Subject: [PATCH 104/384] chore: clarify error variable name in doAttach (#17284) --- agent/reconnectingpty/screen.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agent/reconnectingpty/screen.go b/agent/reconnectingpty/screen.go index 533c11a06bf4a..04e1861eade94 100644 --- a/agent/reconnectingpty/screen.go +++ b/agent/reconnectingpty/screen.go @@ -307,9 +307,9 @@ func (rpty *screenReconnectingPTY) doAttach(ctx context.Context, conn net.Conn, if closeErr != nil { logger.Debug(ctx, "closed ptty with error", slog.Error(closeErr)) } - closeErr = process.Kill() - if closeErr != nil { - logger.Debug(ctx, "killed process with error", slog.Error(closeErr)) + killErr := process.Kill() + if killErr != nil { + logger.Debug(ctx, "killed process with error", slog.Error(killErr)) } rpty.metrics.WithLabelValues("screen_wait").Add(1) return nil, nil, err From b7cd545d0a8d1bc879395c587b7b284f717db4a4 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Wed, 16 Apr 2025 14:29:45 +0400 Subject: [PATCH 105/384] test: fix TestConfigSSH_FileWriteAndOptionsFlow on Windows 11 24H2 (#17410) Fixes tests on Windows 11 due to `printf` not being a recognized command name. --- cli/configssh_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/configssh_test.go b/cli/configssh_test.go index 638e38a3fee1b..b42241b6b3aad 100644 --- a/cli/configssh_test.go +++ b/cli/configssh_test.go @@ -435,7 +435,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { "# :hostname-suffix=coder-suffix", "# :header=X-Test-Header=foo", "# :header=X-Test-Header2=bar", - "# :header-command=printf h1=v1 h2=\"v2\" h3='v3'", + "# :header-command=echo h1=v1 h2=\"v2\" h3='v3'", "#", }, "\n"), strings.Join([]string{ @@ -451,7 +451,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { "--hostname-suffix", "coder-suffix", "--header", "X-Test-Header=foo", "--header", "X-Test-Header2=bar", - "--header-command", "printf h1=v1 h2=\"v2\" h3='v3'", + "--header-command", "echo h1=v1 h2=\"v2\" h3='v3'", }, }, { @@ -566,36 +566,36 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { name: "Header command", args: []string{ "--yes", - "--header-command", "printf h1=v1", + "--header-command", "echo h1=v1", }, wantErr: false, hasAgent: true, wantConfig: wantConfig{ - regexMatch: `ProxyCommand .* --header-command "printf h1=v1" ssh .* --ssh-host-prefix coder. %h`, + regexMatch: `ProxyCommand .* --header-command "echo h1=v1" ssh .* --ssh-host-prefix coder. %h`, }, }, { name: "Header command with double quotes", args: []string{ "--yes", - "--header-command", "printf h1=v1 h2=\"v2\"", + "--header-command", "echo h1=v1 h2=\"v2\"", }, wantErr: false, hasAgent: true, wantConfig: wantConfig{ - regexMatch: `ProxyCommand .* --header-command "printf h1=v1 h2=\\\"v2\\\"" ssh .* --ssh-host-prefix coder. %h`, + regexMatch: `ProxyCommand .* --header-command "echo h1=v1 h2=\\\"v2\\\"" ssh .* --ssh-host-prefix coder. %h`, }, }, { name: "Header command with single quotes", args: []string{ "--yes", - "--header-command", "printf h1=v1 h2='v2'", + "--header-command", "echo h1=v1 h2='v2'", }, wantErr: false, hasAgent: true, wantConfig: wantConfig{ - regexMatch: `ProxyCommand .* --header-command "printf h1=v1 h2='v2'" ssh .* --ssh-host-prefix coder. %h`, + regexMatch: `ProxyCommand .* --header-command "echo h1=v1 h2='v2'" ssh .* --ssh-host-prefix coder. %h`, }, }, { From d78215cdcb2d43c69d6e4ecb370bb264070320f8 Mon Sep 17 00:00:00 2001 From: Borg93 <48671678+Borg93@users.noreply.github.com> Date: Wed, 16 Apr 2025 13:25:02 +0200 Subject: [PATCH 106/384] chore(site): add mlflow, lakefs and argo logos (#17332) --- site/src/theme/icons.json | 3 +++ site/static/icon/argo-workflows.svg | 1 + site/static/icon/lakefs.svg | 8 ++++++++ site/static/icon/mlflow.svg | 11 +++++++++++ 4 files changed, 23 insertions(+) create mode 100644 site/static/icon/argo-workflows.svg create mode 100644 site/static/icon/lakefs.svg create mode 100644 site/static/icon/mlflow.svg diff --git a/site/src/theme/icons.json b/site/src/theme/icons.json index b83a3320c67df..7c7d8dac9e9db 100644 --- a/site/src/theme/icons.json +++ b/site/src/theme/icons.json @@ -4,6 +4,7 @@ "apache-guacamole.svg", "apple-black.svg", "apple-grey.svg", + "argo-workflows.svg", "aws-dark.svg", "aws-light.svg", "aws-monochrome.svg", @@ -63,11 +64,13 @@ "kasmvnc.svg", "keycloak.svg", "kotlin.svg", + "lakefs.svg", "lxc.svg", "matlab.svg", "memory.svg", "microsoft-teams.svg", "microsoft.svg", + "mlflow.svg", "nix.svg", "node.svg", "nodejs.svg", diff --git a/site/static/icon/argo-workflows.svg b/site/static/icon/argo-workflows.svg new file mode 100644 index 0000000000000..580f6d0a3100f --- /dev/null +++ b/site/static/icon/argo-workflows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/static/icon/lakefs.svg b/site/static/icon/lakefs.svg new file mode 100644 index 0000000000000..ebd0a2f5f53fa --- /dev/null +++ b/site/static/icon/lakefs.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/site/static/icon/mlflow.svg b/site/static/icon/mlflow.svg new file mode 100644 index 0000000000000..6dd3cde27236c --- /dev/null +++ b/site/static/icon/mlflow.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + From 64172d374f00e616f6bceebd9d895164855344b7 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 16 Apr 2025 15:54:06 +0200 Subject: [PATCH 107/384] fix: set preset parameters in the API rather than the frontend (#17403) Follow-up from a [previous Pull Request](https://github.com/coder/coder/pull/16965) required some additional testing of Presets from the API perspective. In the process of adding the new tests, I updated the API to enforce preset parameter values based on the selected preset instead of trusting whichever frontend makes the request. This avoids errors scenarios in prebuilds where a prebuild might expect a certain preset but find a different set of actual parameter values. --- coderd/util/slice/slice.go | 13 + coderd/workspaces_test.go | 368 ++++++++++++++++++++++++++--- coderd/wsbuilder/wsbuilder.go | 42 +++- coderd/wsbuilder/wsbuilder_test.go | 10 + 4 files changed, 387 insertions(+), 46 deletions(-) diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index 508827dfaae81..b4ee79291d73f 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -66,6 +66,19 @@ func Contains[T comparable](haystack []T, needle T) bool { }) } +func CountMatchingPairs[A, B any](a []A, b []B, match func(A, B) bool) int { + count := 0 + for _, a := range a { + for _, b := range b { + if match(a, b) { + count++ + break + } + } + } + return count +} + // Find returns the first element that satisfies the condition. func Find[T any](haystack []T, cond func(T) bool) (T, bool) { for _, hay := range haystack { diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 136e259d541f9..3101346f5b43a 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -36,6 +36,7 @@ import ( "github.com/coder/coder/v2/coderd/schedule" "github.com/coder/coder/v2/coderd/schedule/cron" "github.com/coder/coder/v2/coderd/util/ptr" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/cryptorand" "github.com/coder/coder/v2/provisioner/echo" @@ -426,47 +427,346 @@ func TestWorkspace(t *testing.T) { t.Run("TemplateVersionPreset", func(t *testing.T) { t.Parallel() - client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - user := coderdtest.CreateFirstUser(t, client) - authz := coderdtest.AssertRBAC(t, api, client) - 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", - }}, + + // Test Utility variables + templateVersionParameters := []*proto.RichParameter{ + {Name: "param1", Type: "string", Required: false}, + {Name: "param2", Type: "string", Required: false}, + {Name: "param3", Type: "string", Required: false}, + } + presetParameters := []*proto.PresetParameter{ + {Name: "param1", Value: "value1"}, + {Name: "param2", Value: "value2"}, + {Name: "param3", Value: "value3"}, + } + emptyPreset := &proto.Preset{ + Name: "Empty Preset", + } + presetWithParameters := &proto.Preset{ + Name: "Preset With Parameters", + Parameters: presetParameters, + } + + testCases := []struct { + name string + presets []*proto.Preset + templateVersionParameters []*proto.RichParameter + selectedPresetIndex *int + }{ + { + name: "No Presets - No Template Parameters", + presets: []*proto.Preset{}, + }, + { + name: "No Presets - With Template Parameters", + presets: []*proto.Preset{}, + templateVersionParameters: templateVersionParameters, + }, + { + name: "Single Preset - No Preset Parameters But With Template Parameters", + presets: []*proto.Preset{emptyPreset}, + templateVersionParameters: templateVersionParameters, + selectedPresetIndex: ptr.Ref(0), + }, + { + name: "Single Preset - No Preset Parameters And No Template Parameters", + presets: []*proto.Preset{emptyPreset}, + selectedPresetIndex: ptr.Ref(0), + }, + { + name: "Single Preset - With Preset Parameters But No Template Parameters", + presets: []*proto.Preset{presetWithParameters}, + selectedPresetIndex: ptr.Ref(0), + }, + { + name: "Single Preset - With Matching Parameters", + presets: []*proto.Preset{presetWithParameters}, + templateVersionParameters: templateVersionParameters, + selectedPresetIndex: ptr.Ref(0), + }, + { + name: "Single Preset - With Partial Matching Parameters", + presets: []*proto.Preset{{ + Name: "test", + Parameters: presetParameters, + }}, + templateVersionParameters: templateVersionParameters[:2], + selectedPresetIndex: ptr.Ref(0), + }, + { + name: "Multiple Presets - No Parameters", + presets: []*proto.Preset{ + {Name: "preset1"}, + {Name: "preset2"}, + {Name: "preset3"}, + }, + selectedPresetIndex: ptr.Ref(0), + }, + { + name: "Multiple Presets - First Has Parameters", + presets: []*proto.Preset{ + { + Name: "preset1", + Parameters: presetParameters, }, + {Name: "preset2"}, + {Name: "preset3"}, }, - }}, - ProvisionApply: echo.ApplyComplete, - }) - coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + selectedPresetIndex: ptr.Ref(0), + }, + { + name: "Multiple Presets - First Has Matching Parameters", + presets: []*proto.Preset{ + presetWithParameters, + {Name: "preset2"}, + {Name: "preset3"}, + }, + templateVersionParameters: templateVersionParameters, + selectedPresetIndex: ptr.Ref(0), + }, + { + name: "Multiple Presets - Middle Has Parameters", + presets: []*proto.Preset{ + {Name: "preset1"}, + presetWithParameters, + {Name: "preset3"}, + }, + selectedPresetIndex: ptr.Ref(1), + }, + { + name: "Multiple Presets - Middle Has Matching Parameters", + presets: []*proto.Preset{ + {Name: "preset1"}, + presetWithParameters, + {Name: "preset3"}, + }, + templateVersionParameters: templateVersionParameters, + selectedPresetIndex: ptr.Ref(1), + }, + { + name: "Multiple Presets - Last Has Parameters", + presets: []*proto.Preset{ + {Name: "preset1"}, + {Name: "preset2"}, + presetWithParameters, + }, + selectedPresetIndex: ptr.Ref(2), + }, + { + name: "Multiple Presets - Last Has Matching Parameters", + presets: []*proto.Preset{ + {Name: "preset1"}, + {Name: "preset2"}, + presetWithParameters, + }, + templateVersionParameters: templateVersionParameters, + selectedPresetIndex: ptr.Ref(2), + }, + { + name: "Multiple Presets - All Have Parameters", + presets: []*proto.Preset{ + { + Name: "preset1", + Parameters: presetParameters[:1], + }, + { + Name: "preset2", + Parameters: presetParameters[1:2], + }, + { + Name: "preset3", + Parameters: presetParameters[2:3], + }, + }, + selectedPresetIndex: ptr.Ref(1), + }, + { + name: "Multiple Presets - All Have Partially Matching Parameters", + presets: []*proto.Preset{ + { + Name: "preset1", + Parameters: presetParameters[:1], + }, + { + Name: "preset2", + Parameters: presetParameters[1:2], + }, + { + Name: "preset3", + Parameters: presetParameters[2:3], + }, + }, + templateVersionParameters: templateVersionParameters, + selectedPresetIndex: ptr.Ref(1), + }, + { + name: "Multiple presets - With Overlapping Matching Parameters", + presets: []*proto.Preset{ + { + Name: "preset1", + Parameters: []*proto.PresetParameter{ + {Name: "param1", Value: "expectedValue1"}, + {Name: "param2", Value: "expectedValue2"}, + }, + }, + { + Name: "preset2", + Parameters: []*proto.PresetParameter{ + {Name: "param1", Value: "incorrectValue1"}, + {Name: "param2", Value: "incorrectValue2"}, + }, + }, + }, + templateVersionParameters: templateVersionParameters, + selectedPresetIndex: ptr.Ref(0), + }, + { + name: "Multiple Presets - With Parameters But Not Used", + presets: []*proto.Preset{ + { + Name: "preset1", + Parameters: presetParameters[:1], + }, + { + Name: "preset2", + Parameters: presetParameters[1:2], + }, + }, + templateVersionParameters: templateVersionParameters, + }, + { + name: "Multiple Presets - With Matching Parameters But Not Used", + presets: []*proto.Preset{ + { + Name: "preset1", + Parameters: presetParameters[:1], + }, + { + Name: "preset2", + Parameters: presetParameters[1:2], + }, + }, + templateVersionParameters: templateVersionParameters[0:2], + }, + } - ctx := testutil.Context(t, testutil.WaitLong) + for _, tc := range testCases { + tc := tc // Capture range variable + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - presets, err := client.TemplateVersionPresets(ctx, version.ID) - require.NoError(t, err) - require.Equal(t, 1, len(presets)) - require.Equal(t, "test", presets[0].Name) + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + authz := coderdtest.AssertRBAC(t, api, client) - workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(request *codersdk.CreateWorkspaceRequest) { - request.TemplateVersionPresetID = presets[0].ID - }) + // Create a plan response with the specified presets and parameters + planResponse := &proto.Response{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Presets: tc.presets, + Parameters: tc.templateVersionParameters, + }, + }, + } - authz.Reset() // Reset all previous checks done in setup. - ws, err := client.Workspace(ctx, workspace.ID) - authz.AssertChecked(t, policy.ActionRead, ws) - require.NoError(t, err) - require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID) - require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason) - require.Equal(t, presets[0].ID, *ws.LatestBuild.TemplateVersionPresetID) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{planResponse}, + ProvisionApply: echo.ApplyComplete, + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - org, err := client.Organization(ctx, ws.OrganizationID) - require.NoError(t, err) - require.Equal(t, ws.OrganizationName, org.Name) + ctx := testutil.Context(t, testutil.WaitLong) + + // Check createdPresets + createdPresets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Equal(t, len(tc.presets), len(createdPresets)) + + for _, createdPreset := range createdPresets { + presetIndex := slices.IndexFunc(tc.presets, func(expectedPreset *proto.Preset) bool { + return expectedPreset.Name == createdPreset.Name + }) + require.NotEqual(t, -1, presetIndex, "Preset %s should be present", createdPreset.Name) + + // Verify that the preset has the expected parameters + for _, expectedPresetParam := range tc.presets[presetIndex].Parameters { + paramFoundAtIndex := slices.IndexFunc(createdPreset.Parameters, func(createdPresetParam codersdk.PresetParameter) bool { + return expectedPresetParam.Name == createdPresetParam.Name && expectedPresetParam.Value == createdPresetParam.Value + }) + require.NotEqual(t, -1, paramFoundAtIndex, "Parameter %s should be present in preset", expectedPresetParam.Name) + } + } + + // Create workspace with or without preset + var workspace codersdk.Workspace + if tc.selectedPresetIndex != nil { + // Use the selected preset + workspace = coderdtest.CreateWorkspace(t, client, template.ID, func(request *codersdk.CreateWorkspaceRequest) { + request.TemplateVersionPresetID = createdPresets[*tc.selectedPresetIndex].ID + }) + } else { + workspace = coderdtest.CreateWorkspace(t, client, template.ID) + } + + // Verify workspace details + authz.Reset() // Reset all previous checks done in setup. + ws, err := client.Workspace(ctx, workspace.ID) + authz.AssertChecked(t, policy.ActionRead, ws) + require.NoError(t, err) + require.Equal(t, user.UserID, ws.LatestBuild.InitiatorID) + require.Equal(t, codersdk.BuildReasonInitiator, ws.LatestBuild.Reason) + + // Check that the preset ID is set if expected + require.Equal(t, tc.selectedPresetIndex == nil, ws.LatestBuild.TemplateVersionPresetID == nil) + + if tc.selectedPresetIndex == nil { + // No preset selected, so no further checks are needed + // Pre-preset tests cover this case sufficiently. + return + } + + // If we get here, we expect a preset to be selected. + // So we need to assert that selecting the preset had all the correct consequences. + require.Equal(t, createdPresets[*tc.selectedPresetIndex].ID, *ws.LatestBuild.TemplateVersionPresetID) + + selectedPresetParameters := tc.presets[*tc.selectedPresetIndex].Parameters + + // Get parameters that were applied to the latest workspace build + builds, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{ + WorkspaceID: ws.ID, + }) + require.NoError(t, err) + require.Equal(t, 1, len(builds)) + gotWorkspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, builds[0].ID) + require.NoError(t, err) + + // Count how many parameters were set by the preset + parametersSetByPreset := slice.CountMatchingPairs( + gotWorkspaceBuildParameters, + selectedPresetParameters, + func(gotParameter codersdk.WorkspaceBuildParameter, presetParameter *proto.PresetParameter) bool { + namesMatch := gotParameter.Name == presetParameter.Name + valuesMatch := gotParameter.Value == presetParameter.Value + return namesMatch && valuesMatch + }, + ) + + // Count how many parameters should have been set by the preset + expectedParamCount := slice.CountMatchingPairs( + selectedPresetParameters, + tc.templateVersionParameters, + func(presetParam *proto.PresetParameter, templateParam *proto.RichParameter) bool { + return presetParam.Name == templateParam.Name + }, + ) + + // Verify that only the expected number of parameters were set by the preset + require.Equal(t, expectedParamCount, parametersSetByPreset, + "Expected %d parameters to be set, but found %d", expectedParamCount, parametersSetByPreset) + }) + } }) } diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 469c8fbcfdd6d..fa7c00861202d 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -61,18 +61,19 @@ type Builder struct { store database.Store // cache of objects, so we only fetch once - template *database.Template - templateVersion *database.TemplateVersion - templateVersionJob *database.ProvisionerJob - templateVersionParameters *[]database.TemplateVersionParameter - templateVersionVariables *[]database.TemplateVersionVariable - templateVersionWorkspaceTags *[]database.TemplateVersionWorkspaceTag - lastBuild *database.WorkspaceBuild - lastBuildErr *error - lastBuildParameters *[]database.WorkspaceBuildParameter - lastBuildJob *database.ProvisionerJob - parameterNames *[]string - parameterValues *[]string + template *database.Template + templateVersion *database.TemplateVersion + templateVersionJob *database.ProvisionerJob + templateVersionParameters *[]database.TemplateVersionParameter + templateVersionVariables *[]database.TemplateVersionVariable + templateVersionWorkspaceTags *[]database.TemplateVersionWorkspaceTag + lastBuild *database.WorkspaceBuild + lastBuildErr *error + lastBuildParameters *[]database.WorkspaceBuildParameter + lastBuildJob *database.ProvisionerJob + parameterNames *[]string + parameterValues *[]string + templateVersionPresetParameterValues []database.TemplateVersionPresetParameter prebuild bool @@ -565,6 +566,14 @@ func (b *Builder) getParameters() (names, values []string, err error) { if err != nil { return nil, nil, BuildError{http.StatusInternalServerError, "failed to fetch last build parameters", err} } + if b.templateVersionPresetID != uuid.Nil { + // Fetch and cache these, since we'll need them to override requested values if a preset was chosen + presetParameters, err := b.store.GetPresetParametersByPresetID(b.ctx, b.templateVersionPresetID) + if err != nil { + return nil, nil, BuildError{http.StatusInternalServerError, "failed to get preset parameters", err} + } + b.templateVersionPresetParameterValues = presetParameters + } err = b.verifyNoLegacyParameters() if err != nil { return nil, nil, BuildError{http.StatusBadRequest, "Unable to build workspace with unsupported parameters", err} @@ -597,6 +606,15 @@ func (b *Builder) getParameters() (names, values []string, err error) { } func (b *Builder) findNewBuildParameterValue(name string) *codersdk.WorkspaceBuildParameter { + for _, v := range b.templateVersionPresetParameterValues { + if v.Name == name { + return &codersdk.WorkspaceBuildParameter{ + Name: v.Name, + Value: v.Value, + } + } + } + for _, v := range b.richParameterValues { if v.Name == name { return &v diff --git a/coderd/wsbuilder/wsbuilder_test.go b/coderd/wsbuilder/wsbuilder_test.go index bd6e64a60414a..00b7b5f0ae08b 100644 --- a/coderd/wsbuilder/wsbuilder_test.go +++ b/coderd/wsbuilder/wsbuilder_test.go @@ -789,6 +789,10 @@ func TestWorkspaceBuildWithPreset(t *testing.T) { // Inputs withTemplate, withActiveVersion(nil), + // building workspaces using presets with different combinations of parameters + // is tested at the API layer, in TestWorkspace. Here, it is sufficient to + // test that the preset is used when provided. + withTemplateVersionPresetParameters(presetID, nil), withLastBuildNotFound, withTemplateVersionVariables(activeVersionID, nil), withParameterSchemas(activeJobID, nil), @@ -960,6 +964,12 @@ func withInactiveVersion(params []database.TemplateVersionParameter) func(mTx *d } } +func withTemplateVersionPresetParameters(presetID uuid.UUID, params []database.TemplateVersionPresetParameter) func(mTx *dbmock.MockStore) { + return func(mTx *dbmock.MockStore) { + mTx.EXPECT().GetPresetParametersByPresetID(gomock.Any(), presetID).Return(params, nil) + } +} + func withLastBuildFound(mTx *dbmock.MockStore) { mTx.EXPECT().GetLatestWorkspaceBuildByWorkspaceID(gomock.Any(), workspaceID). Times(1). From 669e790df69e918863037671136b6757c2544f6f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 16 Apr 2025 09:27:35 -0500 Subject: [PATCH 108/384] test: add unit test to excercise bug when idp sync hits deleted orgs (#17405) Deleted organizations are still attempting to sync members. This causes an error on inserting the member, and would likely cause issues later in the sync process even if that member is inserted. Deleted orgs should be skipped. --- coderd/database/dbauthz/dbauthz_test.go | 5 +- coderd/database/dbfake/builder.go | 18 +++ coderd/database/dbmem/dbmem.go | 18 ++- coderd/database/queries.sql.go | 13 +- coderd/database/queries/organizations.sql | 9 +- coderd/idpsync/organization.go | 57 ++++++++- coderd/idpsync/organizations_test.go | 111 ++++++++++++++++++ coderd/users.go | 2 +- .../coderd/enidpsync/organizations_test.go | 42 ++++--- 9 files changed, 242 insertions(+), 33 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 711934a2c1146..e562bbd1f7160 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -886,7 +886,7 @@ func (s *MethodTestSuite) TestOrganization() { _ = dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{UserID: u.ID, OrganizationID: a.ID}) b := dbgen.Organization(s.T(), db, database.Organization{}) _ = dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{UserID: u.ID, OrganizationID: b.ID}) - check.Args(database.GetOrganizationsByUserIDParams{UserID: u.ID, Deleted: false}).Asserts(a, policy.ActionRead, b, policy.ActionRead).Returns(slice.New(a, b)) + check.Args(database.GetOrganizationsByUserIDParams{UserID: u.ID, Deleted: sql.NullBool{Valid: true, Bool: false}}).Asserts(a, policy.ActionRead, b, policy.ActionRead).Returns(slice.New(a, b)) })) s.Run("InsertOrganization", s.Subtest(func(db database.Store, check *expects) { check.Args(database.InsertOrganizationParams{ @@ -994,8 +994,7 @@ func (s *MethodTestSuite) TestOrganization() { member, policy.ActionRead, member, policy.ActionDelete). WithNotAuthorized("no rows"). - WithCancelled(cancelledErr). - ErrorsWithInMemDB(sql.ErrNoRows) + WithCancelled(cancelledErr) })) s.Run("UpdateOrganization", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{ diff --git a/coderd/database/dbfake/builder.go b/coderd/database/dbfake/builder.go index 67600c1856894..d916d2c7c533d 100644 --- a/coderd/database/dbfake/builder.go +++ b/coderd/database/dbfake/builder.go @@ -17,6 +17,7 @@ type OrganizationBuilder struct { t *testing.T db database.Store seed database.Organization + delete bool allUsersAllowance int32 members []uuid.UUID groups map[database.Group][]uuid.UUID @@ -45,6 +46,12 @@ func (b OrganizationBuilder) EveryoneAllowance(allowance int) OrganizationBuilde return b } +func (b OrganizationBuilder) Deleted(deleted bool) OrganizationBuilder { + //nolint: revive // returns modified struct + b.delete = deleted + return b +} + func (b OrganizationBuilder) Seed(seed database.Organization) OrganizationBuilder { //nolint: revive // returns modified struct b.seed = seed @@ -119,6 +126,17 @@ func (b OrganizationBuilder) Do() OrganizationResponse { } } + if b.delete { + now := dbtime.Now() + err = b.db.UpdateOrganizationDeletedByID(ctx, database.UpdateOrganizationDeletedByIDParams{ + UpdatedAt: now, + ID: org.ID, + }) + require.NoError(b.t, err) + org.Deleted = true + org.UpdatedAt = now + } + return OrganizationResponse{ Org: org, AllUsersGroup: everyone, diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index ed9f098c00e3c..1359d2e63484d 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -2357,10 +2357,13 @@ func (q *FakeQuerier) DeleteOrganizationMember(ctx context.Context, arg database q.mutex.Lock() defer q.mutex.Unlock() - deleted := slices.DeleteFunc(q.data.organizationMembers, func(member database.OrganizationMember) bool { - return member.OrganizationID == arg.OrganizationID && member.UserID == arg.UserID + deleted := false + q.data.organizationMembers = slices.DeleteFunc(q.data.organizationMembers, func(member database.OrganizationMember) bool { + match := member.OrganizationID == arg.OrganizationID && member.UserID == arg.UserID + deleted = deleted || match + return match }) - if len(deleted) == 0 { + if !deleted { return sql.ErrNoRows } @@ -4156,6 +4159,9 @@ func (q *FakeQuerier) GetOrganizations(_ context.Context, args database.GetOrgan if args.Name != "" && !strings.EqualFold(org.Name, args.Name) { continue } + if args.Deleted != org.Deleted { + continue + } tmp = append(tmp, org) } @@ -4172,7 +4178,11 @@ func (q *FakeQuerier) GetOrganizationsByUserID(_ context.Context, arg database.G continue } for _, organization := range q.organizations { - if organization.ID != organizationMember.OrganizationID || organization.Deleted != arg.Deleted { + if organization.ID != organizationMember.OrganizationID { + continue + } + + if arg.Deleted.Valid && organization.Deleted != arg.Deleted.Bool { continue } organizations = append(organizations, organization) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index c1738589d37ae..72f2c4a8fcb8e 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5680,8 +5680,13 @@ SELECT FROM organizations WHERE - -- Optionally include deleted organizations - deleted = $2 AND + -- Optionally provide a filter for deleted organizations. + CASE WHEN + $2 :: boolean IS NULL THEN + true + ELSE + deleted = $2 + END AND id = ANY( SELECT organization_id @@ -5693,8 +5698,8 @@ WHERE ` type GetOrganizationsByUserIDParams struct { - UserID uuid.UUID `db:"user_id" json:"user_id"` - Deleted bool `db:"deleted" json:"deleted"` + UserID uuid.UUID `db:"user_id" json:"user_id"` + Deleted sql.NullBool `db:"deleted" json:"deleted"` } func (q *sqlQuerier) GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error) { diff --git a/coderd/database/queries/organizations.sql b/coderd/database/queries/organizations.sql index d710a26ca9a46..d940fb1ad4dc6 100644 --- a/coderd/database/queries/organizations.sql +++ b/coderd/database/queries/organizations.sql @@ -55,8 +55,13 @@ SELECT FROM organizations WHERE - -- Optionally include deleted organizations - deleted = @deleted AND + -- Optionally provide a filter for deleted organizations. + CASE WHEN + sqlc.narg('deleted') :: boolean IS NULL THEN + true + ELSE + deleted = sqlc.narg('deleted') + END AND id = ANY( SELECT organization_id diff --git a/coderd/idpsync/organization.go b/coderd/idpsync/organization.go index 87fd9af5e935d..be65daba369df 100644 --- a/coderd/idpsync/organization.go +++ b/coderd/idpsync/organization.go @@ -92,14 +92,16 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u return nil // No sync configured, nothing to do } - expectedOrgs, err := orgSettings.ParseClaims(ctx, tx, params.MergedClaims) + expectedOrgIDs, err := orgSettings.ParseClaims(ctx, tx, params.MergedClaims) if err != nil { return xerrors.Errorf("organization claims: %w", err) } + // Fetch all organizations, even deleted ones. This is to remove a user + // from any deleted organizations they may be in. existingOrgs, err := tx.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ UserID: user.ID, - Deleted: false, + Deleted: sql.NullBool{}, }) if err != nil { return xerrors.Errorf("failed to get user organizations: %w", err) @@ -109,10 +111,35 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u return org.ID }) + // finalExpected is the final set of org ids the user is expected to be in. + // Deleted orgs are omitted from this set. + finalExpected := expectedOrgIDs + if len(expectedOrgIDs) > 0 { + // If you pass in an empty slice to the db arg, you get all orgs. So the slice + // has to be non-empty to get the expected set. Logically it also does not make + // sense to fetch an empty set from the db. + expectedOrganizations, err := tx.GetOrganizations(ctx, database.GetOrganizationsParams{ + IDs: expectedOrgIDs, + // Do not include deleted organizations. Omitting deleted orgs will remove the + // user from any deleted organizations they are a member of. + Deleted: false, + }) + if err != nil { + return xerrors.Errorf("failed to get expected organizations: %w", err) + } + finalExpected = db2sdk.List(expectedOrganizations, func(org database.Organization) uuid.UUID { + return org.ID + }) + } + // Find the difference in the expected and the existing orgs, and // correct the set of orgs the user is a member of. - add, remove := slice.SymmetricDifference(existingOrgIDs, expectedOrgs) - notExists := make([]uuid.UUID, 0) + add, remove := slice.SymmetricDifference(existingOrgIDs, finalExpected) + // notExists is purely for debugging. It logs when the settings want + // a user in an organization, but the organization does not exist. + notExists := slice.DifferenceFunc(expectedOrgIDs, finalExpected, func(a, b uuid.UUID) bool { + return a == b + }) for _, orgID := range add { _, err := tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ OrganizationID: orgID, @@ -123,9 +150,30 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u }) if err != nil { if xerrors.Is(err, sql.ErrNoRows) { + // This should not happen because we check the org existence + // beforehand. notExists = append(notExists, orgID) continue } + + if database.IsUniqueViolation(err, database.UniqueOrganizationMembersPkey) { + // If we hit this error we have a bug. The user already exists in the + // organization, but was not detected to be at the start of this function. + // Instead of failing the function, an error will be logged. This is to not bring + // down the entire syncing behavior from a single failed org. Failing this can + // prevent user logins, so only fatal non-recoverable errors should be returned. + // + // Inserting a user is privilege escalation. So skipping this instead of failing + // leaves the user with fewer permissions. So this is safe from a security + // perspective to continue. + s.Logger.Error(ctx, "syncing user to organization failed as they are already a member, please report this failure to Coder", + slog.F("user_id", user.ID), + slog.F("username", user.Username), + slog.F("organization_id", orgID), + slog.Error(err), + ) + continue + } return xerrors.Errorf("add user to organization: %w", err) } } @@ -141,6 +189,7 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u } if len(notExists) > 0 { + notExists = slice.Unique(notExists) // Remove duplicates s.Logger.Debug(ctx, "organizations do not exist but attempted to use in org sync", slog.F("not_found", notExists), slog.F("user_id", user.ID), diff --git a/coderd/idpsync/organizations_test.go b/coderd/idpsync/organizations_test.go index 51c8a7365d22b..3a00499bdbced 100644 --- a/coderd/idpsync/organizations_test.go +++ b/coderd/idpsync/organizations_test.go @@ -1,6 +1,7 @@ package idpsync_test import ( + "database/sql" "testing" "github.com/golang-jwt/jwt/v4" @@ -8,6 +9,11 @@ import ( "github.com/stretchr/testify/require" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/database/dbfake" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/idpsync" "github.com/coder/coder/v2/coderd/runtimeconfig" "github.com/coder/coder/v2/testutil" @@ -38,3 +44,108 @@ func TestParseOrganizationClaims(t *testing.T) { require.False(t, params.SyncEntitled) }) } + +func TestSyncOrganizations(t *testing.T) { + t.Parallel() + + // This test creates some deleted organizations and checks the behavior is + // correct. + t.Run("SyncUserToDeletedOrg", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + db, _ := dbtestutil.NewDB(t) + user := dbgen.User(t, db, database.User{}) + + // Create orgs for: + // - stays = User is a member, and stays + // - leaves = User is a member, and leaves + // - joins = User is not a member, and joins + // For deleted orgs, the user **should not** be a member of afterwards. + // - deletedStays = User is a member of deleted org, and wants to stay + // - deletedLeaves = User is a member of deleted org, and wants to leave + // - deletedJoins = User is not a member of deleted org, and wants to join + stays := dbfake.Organization(t, db).Members(user).Do() + leaves := dbfake.Organization(t, db).Members(user).Do() + joins := dbfake.Organization(t, db).Do() + + deletedStays := dbfake.Organization(t, db).Members(user).Deleted(true).Do() + deletedLeaves := dbfake.Organization(t, db).Members(user).Deleted(true).Do() + deletedJoins := dbfake.Organization(t, db).Deleted(true).Do() + + // Now sync the user to the deleted organization + s := idpsync.NewAGPLSync( + slogtest.Make(t, &slogtest.Options{}), + runtimeconfig.NewManager(), + idpsync.DeploymentSyncSettings{ + OrganizationField: "orgs", + OrganizationMapping: map[string][]uuid.UUID{ + "stay": {stays.Org.ID, deletedStays.Org.ID}, + "leave": {leaves.Org.ID, deletedLeaves.Org.ID}, + "join": {joins.Org.ID, deletedJoins.Org.ID}, + }, + OrganizationAssignDefault: false, + }, + ) + + err := s.SyncOrganizations(ctx, db, user, idpsync.OrganizationParams{ + SyncEntitled: true, + MergedClaims: map[string]interface{}{ + "orgs": []string{"stay", "join"}, + }, + }) + require.NoError(t, err) + + orgs, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ + UserID: user.ID, + Deleted: sql.NullBool{}, + }) + require.NoError(t, err) + require.Len(t, orgs, 2) + + // Verify the user only exists in 2 orgs. The one they stayed, and the one they + // joined. + inIDs := db2sdk.List(orgs, func(org database.Organization) uuid.UUID { + return org.ID + }) + require.ElementsMatch(t, []uuid.UUID{stays.Org.ID, joins.Org.ID}, inIDs) + }) + + t.Run("UserToZeroOrgs", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitMedium) + db, _ := dbtestutil.NewDB(t) + user := dbgen.User(t, db, database.User{}) + + deletedLeaves := dbfake.Organization(t, db).Members(user).Deleted(true).Do() + + // Now sync the user to the deleted organization + s := idpsync.NewAGPLSync( + slogtest.Make(t, &slogtest.Options{}), + runtimeconfig.NewManager(), + idpsync.DeploymentSyncSettings{ + OrganizationField: "orgs", + OrganizationMapping: map[string][]uuid.UUID{ + "leave": {deletedLeaves.Org.ID}, + }, + OrganizationAssignDefault: false, + }, + ) + + err := s.SyncOrganizations(ctx, db, user, idpsync.OrganizationParams{ + SyncEntitled: true, + MergedClaims: map[string]interface{}{ + "orgs": []string{}, + }, + }) + require.NoError(t, err) + + orgs, err := db.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ + UserID: user.ID, + Deleted: sql.NullBool{}, + }) + require.NoError(t, err) + require.Len(t, orgs, 0) + }) +} diff --git a/coderd/users.go b/coderd/users.go index d97abc82b2fd1..ad1ba8a018743 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -1340,7 +1340,7 @@ func (api *API) organizationsByUser(rw http.ResponseWriter, r *http.Request) { organizations, err := api.Database.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ UserID: user.ID, - Deleted: false, + Deleted: sql.NullBool{Bool: false, Valid: true}, }) if errors.Is(err, sql.ErrNoRows) { err = nil diff --git a/enterprise/coderd/enidpsync/organizations_test.go b/enterprise/coderd/enidpsync/organizations_test.go index 391535c9478d7..b2e120592b582 100644 --- a/enterprise/coderd/enidpsync/organizations_test.go +++ b/enterprise/coderd/enidpsync/organizations_test.go @@ -14,6 +14,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/entitlements" @@ -89,7 +90,8 @@ func TestOrganizationSync(t *testing.T) { Name: "SingleOrgDeployment", Case: func(t *testing.T, db database.Store) OrganizationSyncTestCase { def, _ := db.GetDefaultOrganization(context.Background()) - other := dbgen.Organization(t, db, database.Organization{}) + other := dbfake.Organization(t, db).Do() + deleted := dbfake.Organization(t, db).Deleted(true).Do() return OrganizationSyncTestCase{ Entitlements: entitled, Settings: idpsync.DeploymentSyncSettings{ @@ -123,11 +125,19 @@ func TestOrganizationSync(t *testing.T) { }) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: user.ID, - OrganizationID: other.ID, + OrganizationID: other.Org.ID, + }) + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + UserID: user.ID, + OrganizationID: deleted.Org.ID, }) }, Sync: ExpectedUser{ - Organizations: []uuid.UUID{def.ID, other.ID}, + Organizations: []uuid.UUID{ + def.ID, other.Org.ID, + // The user remains in the deleted org because no idp sync happens. + deleted.Org.ID, + }, }, }, }, @@ -138,17 +148,19 @@ func TestOrganizationSync(t *testing.T) { Name: "MultiOrgWithDefault", Case: func(t *testing.T, db database.Store) OrganizationSyncTestCase { def, _ := db.GetDefaultOrganization(context.Background()) - one := dbgen.Organization(t, db, database.Organization{}) - two := dbgen.Organization(t, db, database.Organization{}) - three := dbgen.Organization(t, db, database.Organization{}) + one := dbfake.Organization(t, db).Do() + two := dbfake.Organization(t, db).Do() + three := dbfake.Organization(t, db).Do() + deleted := dbfake.Organization(t, db).Deleted(true).Do() return OrganizationSyncTestCase{ Entitlements: entitled, Settings: idpsync.DeploymentSyncSettings{ OrganizationField: "organizations", OrganizationMapping: map[string][]uuid.UUID{ - "first": {one.ID}, - "second": {two.ID}, - "third": {three.ID}, + "first": {one.Org.ID}, + "second": {two.Org.ID}, + "third": {three.Org.ID}, + "deleted": {deleted.Org.ID}, }, OrganizationAssignDefault: true, }, @@ -167,7 +179,7 @@ func TestOrganizationSync(t *testing.T) { { Name: "AlreadyInOrgs", Claims: jwt.MapClaims{ - "organizations": []string{"second", "extra"}, + "organizations": []string{"second", "extra", "deleted"}, }, ExpectedParams: idpsync.OrganizationParams{ SyncEntitled: true, @@ -180,18 +192,18 @@ func TestOrganizationSync(t *testing.T) { }) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: user.ID, - OrganizationID: one.ID, + OrganizationID: one.Org.ID, }) }, Sync: ExpectedUser{ - Organizations: []uuid.UUID{def.ID, two.ID}, + Organizations: []uuid.UUID{def.ID, two.Org.ID}, }, }, { Name: "ManyClaims", Claims: jwt.MapClaims{ // Add some repeats - "organizations": []string{"second", "extra", "first", "third", "second", "second"}, + "organizations": []string{"second", "extra", "first", "third", "second", "second", "deleted"}, }, ExpectedParams: idpsync.OrganizationParams{ SyncEntitled: true, @@ -204,11 +216,11 @@ func TestOrganizationSync(t *testing.T) { }) dbgen.OrganizationMember(t, db, database.OrganizationMember{ UserID: user.ID, - OrganizationID: one.ID, + OrganizationID: one.Org.ID, }) }, Sync: ExpectedUser{ - Organizations: []uuid.UUID{def.ID, one.ID, two.ID, three.ID}, + Organizations: []uuid.UUID{def.ID, one.Org.ID, two.Org.ID, three.Org.ID}, }, }, }, From 99979a78f5a9fe71ff500bd79f8be0e7120c0b96 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Wed, 16 Apr 2025 19:48:26 +0500 Subject: [PATCH 109/384] docs: update jfrog-artifactory integration docs (#17413) --- docs/admin/integrations/jfrog-artifactory.md | 48 ++++++++------------ 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/docs/admin/integrations/jfrog-artifactory.md b/docs/admin/integrations/jfrog-artifactory.md index 8f27d687d7e00..3713bb1770f3d 100644 --- a/docs/admin/integrations/jfrog-artifactory.md +++ b/docs/admin/integrations/jfrog-artifactory.md @@ -1,15 +1,5 @@ # JFrog Artifactory Integration - -January 24, 2024 - ---- - Use Coder and JFrog Artifactory together to secure your development environments without disturbing your developers' existing workflows. @@ -60,8 +50,8 @@ To set this up, follow these steps: ``` 1. Create a new Application Integration by going to - `https://JFROG_URL/ui/admin/configuration/integrations/new` and select the - Application Type as the integration you created in step 1. + `https://JFROG_URL/ui/admin/configuration/integrations/app-integrations/new` and select the + Application Type as the integration you created in step 1 or `Custom Integration` if you are using SaaS instance i.e. example.jfrog.io. 1. Add a new [external authentication](../../admin/external-auth.md) to Coder by setting these environment variables in a manner consistent with your Coder deployment. Replace `JFROG_URL` with your JFrog Artifactory base URL: @@ -82,16 +72,18 @@ To set this up, follow these steps: ```tf module "jfrog" { - source = "registry.coder.com/modules/jfrog-oauth/coder" - version = "1.0.0" - agent_id = coder_agent.example.id - jfrog_url = "https://jfrog.example.com" - configure_code_server = true # this depends on the code-server + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jfrog-oauth/coder" + version = "1.0.19" + agent_id = coder_agent.example.id + jfrog_url = "https://example.jfrog.io" username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username" + package_managers = { - "npm": "npm", - "go": "go", - "pypi": "pypi" + npm = ["npm", "@scoped:npm-scoped"] + go = ["go", "another-go-repo"] + pypi = ["pypi", "extra-index-pypi"] + docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"] } } ``` @@ -117,16 +109,16 @@ To set this up, follow these steps: } module "jfrog" { - source = "registry.coder.com/modules/jfrog-token/coder" - version = "1.0.0" - agent_id = coder_agent.example.id - jfrog_url = "https://example.jfrog.io" - configure_code_server = true # this depends on the code-server + source = "registry.coder.com/modules/jfrog-token/coder" + version = "1.0.30" + agent_id = coder_agent.example.id + jfrog_url = "https://XXXX.jfrog.io" artifactory_access_token = var.artifactory_access_token package_managers = { - "npm": "npm", - "go": "go", - "pypi": "pypi" + npm = ["npm", "@scoped:npm-scoped"] + go = ["go", "another-go-repo"] + pypi = ["pypi", "extra-index-pypi"] + docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"] } } ``` From feb1a3dc02d260941e751f0033f69b9dff2fe464 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:56:12 +0000 Subject: [PATCH 110/384] chore: bump github.com/mark3labs/mcp-go from 0.17.0 to 0.20.0 (#17380) 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.17.0 to 0.20.0.
    Release notes

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

    Release v0.20.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/mark3labs/mcp-go/compare/v0.19.0...v0.20.0

    Release v0.19.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/mark3labs/mcp-go/compare/v0.18.0...v0.19.0

    Release v0.18.0

    What's Changed

    New Contributors

    Full Changelog: https://github.com/mark3labs/mcp-go/compare/v0.17.0...v0.18.0

    Commits
    • b8dc82d feat: Tool Handler Middleware (#123)
    • 6b923f6 fix(client): allow interface to be implemented (#135)
    • cc777fc feat: add ping for sse server (#80)
    • c7390fe Feature/pagination functionality (#107)
    • 62cdf71 feat: use defer processing error (#98)
    • 1b7e34c mcp-client should also include configurable http headers in the /sse request ...
    • d1e5f33 fix: make the default sse endpoint match the standard one used in the officia...
    • f3149bf fix: remove sse read timeout to avoid ignoring future sse messages (#88)
    • a0e968a feat: add context to hooks (#92)
    • 607d6c2 simplify required field handling in inputSchema (#82)
    • 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.17.0&new-version=0.20.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 c563050a6dba9..d3e9c55f3d937 100644 --- a/go.mod +++ b/go.mod @@ -490,7 +490,7 @@ require ( require ( github.com/coder/preview v0.0.0-20250409162646-62939c63c71a github.com/kylecarbs/aisdk-go v0.0.5 - github.com/mark3labs/mcp-go v0.17.0 + github.com/mark3labs/mcp-go v0.20.1 ) require ( diff --git a/go.sum b/go.sum index 69053b6525f4b..1943077cedafd 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.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930= -github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= +github.com/mark3labs/mcp-go v0.20.1 h1:E1Bbx9K8d8kQmDZ1QHblM38c7UU2evQ2LlkANk1U/zw= +github.com/mark3labs/mcp-go v0.20.1/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= 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 2a76f5028e457177267a3ca1a7f755b83a6972c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Wed, 16 Apr 2025 09:14:35 -0700 Subject: [PATCH 111/384] fix: don't attempt to insert empty terraform plans into the database (#17426) --- coderd/provisionerdserver/provisionerdserver.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 47fecfb4a1688..a4e28741ce988 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -1417,13 +1417,15 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob) return nil, xerrors.Errorf("update template version external auth providers: %w", err) } - err = s.Database.InsertTemplateVersionTerraformValuesByJobID(ctx, database.InsertTemplateVersionTerraformValuesByJobIDParams{ - JobID: jobID, - CachedPlan: jobType.TemplateImport.Plan, - UpdatedAt: now, - }) - if err != nil { - return nil, xerrors.Errorf("insert template version terraform data: %w", err) + if len(jobType.TemplateImport.Plan) > 0 { + err := s.Database.InsertTemplateVersionTerraformValuesByJobID(ctx, database.InsertTemplateVersionTerraformValuesByJobIDParams{ + JobID: jobID, + CachedPlan: jobType.TemplateImport.Plan, + UpdatedAt: now, + }) + if err != nil { + return nil, xerrors.Errorf("insert template version terraform data: %w", err) + } } err = s.Database.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{ From f670bc31f5ffcb1639a50586bd84e8e55cac3f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Wed, 16 Apr 2025 09:37:09 -0700 Subject: [PATCH 112/384] chore: update testutil chan helpers (#17408) --- agent/agent_test.go | 16 +- agent/agentscripts/agentscripts_test.go | 4 +- agent/apphealth_test.go | 8 +- agent/checkpoint_internal_test.go | 2 +- agent/stats_internal_test.go | 26 +- cli/cliui/prompt_test.go | 14 +- cli/portforward_test.go | 16 +- cli/ssh_internal_test.go | 14 +- cli/ssh_test.go | 10 +- cli/start_test.go | 6 +- cli/update_test.go | 20 +- cli/usercreate_test.go | 4 +- coderd/autobuild/lifecycle_executor_test.go | 4 +- .../database/pubsub/pubsub_internal_test.go | 8 +- coderd/database/pubsub/pubsub_test.go | 14 +- coderd/database/pubsub/watchdog_test.go | 14 +- coderd/entitlements/entitlements_test.go | 6 +- .../httpmw/loggermw/logger_internal_test.go | 4 +- coderd/notifications/manager_test.go | 2 +- coderd/notifications/metrics_test.go | 4 +- coderd/notifications/notifications_test.go | 10 +- .../provisionerdserver_test.go | 2 +- coderd/rbac/authz_test.go | 8 +- coderd/templateversions_test.go | 6 +- coderd/users_test.go | 4 +- coderd/workspaceagents_test.go | 22 +- coderd/workspaceagentsrpc_internal_test.go | 4 +- coderd/workspaceupdates_test.go | 8 +- codersdk/agentsdk/logs_internal_test.go | 60 +-- codersdk/workspacesdk/dialer_test.go | 32 +- enterprise/tailnet/pgcoord_internal_test.go | 2 +- enterprise/tailnet/pgcoord_test.go | 4 +- enterprise/wsproxy/wsproxy_test.go | 6 +- scaletest/createworkspaces/run_test.go | 2 +- tailnet/configmaps_internal_test.go | 136 +++---- tailnet/conn_test.go | 8 +- tailnet/controllers_test.go | 374 +++++++++--------- tailnet/coordinator_test.go | 4 +- tailnet/node_internal_test.go | 46 +-- tailnet/service_test.go | 22 +- testutil/chan.go | 57 +++ testutil/ctx.go | 34 -- vpn/client_test.go | 14 +- vpn/speaker_internal_test.go | 34 +- vpn/tunnel_internal_test.go | 46 +-- 45 files changed, 582 insertions(+), 559 deletions(-) create mode 100644 testutil/chan.go diff --git a/agent/agent_test.go b/agent/agent_test.go index 97790860ba70a..67fa203252ba7 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -110,7 +110,7 @@ func TestAgent_ImmediateClose(t *testing.T) { }) // wait until the agent has connected and is starting to find races in the startup code - _ = testutil.RequireRecvCtx(ctx, t, client.GetStartup()) + _ = testutil.TryReceive(ctx, t, client.GetStartup()) t.Log("Closing Agent") err := agentUnderTest.Close() require.NoError(t, err) @@ -1700,7 +1700,7 @@ func TestAgent_Lifecycle(t *testing.T) { // In order to avoid shutting down the agent before it is fully started and triggering // errors, we'll wait until the agent is fully up. It's a bit hokey, but among the last things the agent starts // is the stats reporting, so getting a stats report is a good indication the agent is fully up. - _ = testutil.RequireRecvCtx(ctx, t, statsCh) + _ = testutil.TryReceive(ctx, t, statsCh) err := agent.Close() require.NoError(t, err, "agent should be closed successfully") @@ -1730,7 +1730,7 @@ func TestAgent_Startup(t *testing.T) { _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ Directory: "", }, 0) - startup := testutil.RequireRecvCtx(ctx, t, client.GetStartup()) + startup := testutil.TryReceive(ctx, t, client.GetStartup()) require.Equal(t, "", startup.GetExpandedDirectory()) }) @@ -1741,7 +1741,7 @@ func TestAgent_Startup(t *testing.T) { _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ Directory: "~", }, 0) - startup := testutil.RequireRecvCtx(ctx, t, client.GetStartup()) + startup := testutil.TryReceive(ctx, t, client.GetStartup()) homeDir, err := os.UserHomeDir() require.NoError(t, err) require.Equal(t, homeDir, startup.GetExpandedDirectory()) @@ -1754,7 +1754,7 @@ func TestAgent_Startup(t *testing.T) { _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ Directory: "coder/coder", }, 0) - startup := testutil.RequireRecvCtx(ctx, t, client.GetStartup()) + startup := testutil.TryReceive(ctx, t, client.GetStartup()) homeDir, err := os.UserHomeDir() require.NoError(t, err) require.Equal(t, filepath.Join(homeDir, "coder/coder"), startup.GetExpandedDirectory()) @@ -1767,7 +1767,7 @@ func TestAgent_Startup(t *testing.T) { _, client, _, _, _ := setupAgent(t, agentsdk.Manifest{ Directory: "$HOME", }, 0) - startup := testutil.RequireRecvCtx(ctx, t, client.GetStartup()) + startup := testutil.TryReceive(ctx, t, client.GetStartup()) homeDir, err := os.UserHomeDir() require.NoError(t, err) require.Equal(t, homeDir, startup.GetExpandedDirectory()) @@ -2632,7 +2632,7 @@ done n := 1 for n <= 5 { - logs := testutil.RequireRecvCtx(ctx, t, logsCh) + logs := testutil.TryReceive(ctx, t, logsCh) require.NotNil(t, logs) for _, l := range logs.GetLogs() { require.Equal(t, fmt.Sprintf("start %d", n), l.GetOutput()) @@ -2645,7 +2645,7 @@ done n = 1 for n <= 3000 { - logs := testutil.RequireRecvCtx(ctx, t, logsCh) + logs := testutil.TryReceive(ctx, t, logsCh) require.NotNil(t, logs) for _, l := range logs.GetLogs() { require.Equal(t, fmt.Sprintf("stop %d", n), l.GetOutput()) diff --git a/agent/agentscripts/agentscripts_test.go b/agent/agentscripts/agentscripts_test.go index 0100f399c5eff..3104bb805a40c 100644 --- a/agent/agentscripts/agentscripts_test.go +++ b/agent/agentscripts/agentscripts_test.go @@ -44,7 +44,7 @@ func TestExecuteBasic(t *testing.T) { }}, aAPI.ScriptCompleted) require.NoError(t, err) require.NoError(t, runner.Execute(context.Background(), agentscripts.ExecuteAllScripts)) - log := testutil.RequireRecvCtx(ctx, t, fLogger.logs) + log := testutil.TryReceive(ctx, t, fLogger.logs) require.Equal(t, "hello", log.Output) } @@ -136,7 +136,7 @@ func TestScriptReportsTiming(t *testing.T) { require.NoError(t, runner.Execute(ctx, agentscripts.ExecuteAllScripts)) runner.Close() - log := testutil.RequireRecvCtx(ctx, t, fLogger.logs) + log := testutil.TryReceive(ctx, t, fLogger.logs) require.Equal(t, "hello", log.Output) timings := aAPI.GetTimings() diff --git a/agent/apphealth_test.go b/agent/apphealth_test.go index 4d83a889765ae..1d708b651d1f8 100644 --- a/agent/apphealth_test.go +++ b/agent/apphealth_test.go @@ -92,7 +92,7 @@ func TestAppHealth_Healthy(t *testing.T) { mClock.Advance(999 * time.Millisecond).MustWait(ctx) // app2 is now healthy mClock.Advance(time.Millisecond).MustWait(ctx) // report gets triggered - update := testutil.RequireRecvCtx(ctx, t, fakeAPI.AppHealthCh()) + update := testutil.TryReceive(ctx, t, fakeAPI.AppHealthCh()) require.Len(t, update.GetUpdates(), 2) applyUpdate(t, apps, update) require.Equal(t, codersdk.WorkspaceAppHealthHealthy, apps[1].Health) @@ -101,7 +101,7 @@ func TestAppHealth_Healthy(t *testing.T) { mClock.Advance(999 * time.Millisecond).MustWait(ctx) // app3 is now healthy mClock.Advance(time.Millisecond).MustWait(ctx) // report gets triggered - update = testutil.RequireRecvCtx(ctx, t, fakeAPI.AppHealthCh()) + update = testutil.TryReceive(ctx, t, fakeAPI.AppHealthCh()) require.Len(t, update.GetUpdates(), 2) applyUpdate(t, apps, update) require.Equal(t, codersdk.WorkspaceAppHealthHealthy, apps[1].Health) @@ -155,7 +155,7 @@ func TestAppHealth_500(t *testing.T) { mClock.Advance(999 * time.Millisecond).MustWait(ctx) // 2nd check, crosses threshold mClock.Advance(time.Millisecond).MustWait(ctx) // 2nd report, sends update - update := testutil.RequireRecvCtx(ctx, t, fakeAPI.AppHealthCh()) + update := testutil.TryReceive(ctx, t, fakeAPI.AppHealthCh()) require.Len(t, update.GetUpdates(), 1) applyUpdate(t, apps, update) require.Equal(t, codersdk.WorkspaceAppHealthUnhealthy, apps[0].Health) @@ -223,7 +223,7 @@ func TestAppHealth_Timeout(t *testing.T) { timeoutTrap.MustWait(ctx).Release() mClock.Set(ms(3001)).MustWait(ctx) // report tick, sends changes - update := testutil.RequireRecvCtx(ctx, t, fakeAPI.AppHealthCh()) + update := testutil.TryReceive(ctx, t, fakeAPI.AppHealthCh()) require.Len(t, update.GetUpdates(), 1) applyUpdate(t, apps, update) require.Equal(t, codersdk.WorkspaceAppHealthUnhealthy, apps[0].Health) diff --git a/agent/checkpoint_internal_test.go b/agent/checkpoint_internal_test.go index 5b8d16fc9706f..61cb2b7f564a0 100644 --- a/agent/checkpoint_internal_test.go +++ b/agent/checkpoint_internal_test.go @@ -44,6 +44,6 @@ func TestCheckpoint_WaitComplete(t *testing.T) { errCh <- uut.wait(ctx) }() uut.complete(err) - got := testutil.RequireRecvCtx(ctx, t, errCh) + got := testutil.TryReceive(ctx, t, errCh) require.Equal(t, err, got) } diff --git a/agent/stats_internal_test.go b/agent/stats_internal_test.go index 9fd6aa102a5aa..96ac687de070d 100644 --- a/agent/stats_internal_test.go +++ b/agent/stats_internal_test.go @@ -34,14 +34,14 @@ func TestStatsReporter(t *testing.T) { }() // initial request to get duration - req := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + req := testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, req) require.Nil(t, req.Stats) interval := time.Second * 34 - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.UpdateStatsResponse{ReportInterval: durationpb.New(interval)}) + testutil.RequireSend(ctx, t, fDest.resps, &proto.UpdateStatsResponse{ReportInterval: durationpb.New(interval)}) // call to source to set the callback and interval - gotInterval := testutil.RequireRecvCtx(ctx, t, fSource.period) + gotInterval := testutil.TryReceive(ctx, t, fSource.period) require.Equal(t, interval, gotInterval) // callback returning netstats @@ -60,7 +60,7 @@ func TestStatsReporter(t *testing.T) { fSource.callback(time.Now(), time.Now(), netStats, nil) // collector called to complete the stats - gotNetStats := testutil.RequireRecvCtx(ctx, t, fCollector.calls) + gotNetStats := testutil.TryReceive(ctx, t, fCollector.calls) require.Equal(t, netStats, gotNetStats) // while we are collecting the stats, send in two new netStats to simulate @@ -94,13 +94,13 @@ func TestStatsReporter(t *testing.T) { // complete first collection stats := &proto.Stats{SessionCountJetbrains: 55} - testutil.RequireSendCtx(ctx, t, fCollector.stats, stats) + testutil.RequireSend(ctx, t, fCollector.stats, stats) // destination called to report the first stats - update := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + update := testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, update) require.Equal(t, stats, update.Stats) - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.UpdateStatsResponse{ReportInterval: durationpb.New(interval)}) + testutil.RequireSend(ctx, t, fDest.resps, &proto.UpdateStatsResponse{ReportInterval: durationpb.New(interval)}) // second update -- netStat0 and netStats1 are accumulated and reported wantNetStats := map[netlogtype.Connection]netlogtype.Counts{ @@ -115,22 +115,22 @@ func TestStatsReporter(t *testing.T) { RxBytes: 21, }, } - gotNetStats = testutil.RequireRecvCtx(ctx, t, fCollector.calls) + gotNetStats = testutil.TryReceive(ctx, t, fCollector.calls) require.Equal(t, wantNetStats, gotNetStats) stats = &proto.Stats{SessionCountJetbrains: 66} - testutil.RequireSendCtx(ctx, t, fCollector.stats, stats) - update = testutil.RequireRecvCtx(ctx, t, fDest.reqs) + testutil.RequireSend(ctx, t, fCollector.stats, stats) + update = testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, update) require.Equal(t, stats, update.Stats) interval2 := 27 * time.Second - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.UpdateStatsResponse{ReportInterval: durationpb.New(interval2)}) + testutil.RequireSend(ctx, t, fDest.resps, &proto.UpdateStatsResponse{ReportInterval: durationpb.New(interval2)}) // set the new interval - gotInterval = testutil.RequireRecvCtx(ctx, t, fSource.period) + gotInterval = testutil.TryReceive(ctx, t, fSource.period) require.Equal(t, interval2, gotInterval) loopCancel() - err := testutil.RequireRecvCtx(ctx, t, loopErr) + err := testutil.TryReceive(ctx, t, loopErr) require.NoError(t, err) } diff --git a/cli/cliui/prompt_test.go b/cli/cliui/prompt_test.go index 58736ca8d16c8..5ac0d906caae8 100644 --- a/cli/cliui/prompt_test.go +++ b/cli/cliui/prompt_test.go @@ -35,7 +35,7 @@ func TestPrompt(t *testing.T) { }() ptty.ExpectMatch("Example") ptty.WriteLine("hello") - resp := testutil.RequireRecvCtx(ctx, t, msgChan) + resp := testutil.TryReceive(ctx, t, msgChan) require.Equal(t, "hello", resp) }) @@ -54,7 +54,7 @@ func TestPrompt(t *testing.T) { }() ptty.ExpectMatch("Example") ptty.WriteLine("yes") - resp := testutil.RequireRecvCtx(ctx, t, doneChan) + resp := testutil.TryReceive(ctx, t, doneChan) require.Equal(t, "yes", resp) }) @@ -91,7 +91,7 @@ func TestPrompt(t *testing.T) { doneChan <- resp }() - resp := testutil.RequireRecvCtx(ctx, t, doneChan) + resp := testutil.TryReceive(ctx, t, doneChan) require.Equal(t, "yes", resp) // Close the reader to end the io.Copy require.NoError(t, ptty.Close(), "close eof reader") @@ -115,7 +115,7 @@ func TestPrompt(t *testing.T) { }() ptty.ExpectMatch("Example") ptty.WriteLine("{}") - resp := testutil.RequireRecvCtx(ctx, t, doneChan) + resp := testutil.TryReceive(ctx, t, doneChan) require.Equal(t, "{}", resp) }) @@ -133,7 +133,7 @@ func TestPrompt(t *testing.T) { }() ptty.ExpectMatch("Example") ptty.WriteLine("{a") - resp := testutil.RequireRecvCtx(ctx, t, doneChan) + resp := testutil.TryReceive(ctx, t, doneChan) require.Equal(t, "{a", resp) }) @@ -153,7 +153,7 @@ func TestPrompt(t *testing.T) { ptty.WriteLine(`{ "test": "wow" }`) - resp := testutil.RequireRecvCtx(ctx, t, doneChan) + resp := testutil.TryReceive(ctx, t, doneChan) require.Equal(t, `{"test":"wow"}`, resp) }) @@ -178,7 +178,7 @@ func TestPrompt(t *testing.T) { }() ptty.ExpectMatch("Example") ptty.WriteLine("foo\nbar\nbaz\n\n\nvalid\n") - resp := testutil.RequireRecvCtx(ctx, t, doneChan) + resp := testutil.TryReceive(ctx, t, doneChan) require.Equal(t, "valid", resp) }) } diff --git a/cli/portforward_test.go b/cli/portforward_test.go index e1672a5927047..0be029748b3c8 100644 --- a/cli/portforward_test.go +++ b/cli/portforward_test.go @@ -192,8 +192,8 @@ func TestPortForward(t *testing.T) { require.ErrorIs(t, err, context.Canceled) flushCtx := testutil.Context(t, testutil.WaitShort) - testutil.RequireSendCtx(flushCtx, t, wuTick, dbtime.Now()) - _ = testutil.RequireRecvCtx(flushCtx, t, wuFlush) + testutil.RequireSend(flushCtx, t, wuTick, dbtime.Now()) + _ = testutil.TryReceive(flushCtx, t, wuFlush) updated, err := client.Workspace(context.Background(), workspace.ID) require.NoError(t, err) require.Greater(t, updated.LastUsedAt, workspace.LastUsedAt) @@ -247,8 +247,8 @@ func TestPortForward(t *testing.T) { require.ErrorIs(t, err, context.Canceled) flushCtx := testutil.Context(t, testutil.WaitShort) - testutil.RequireSendCtx(flushCtx, t, wuTick, dbtime.Now()) - _ = testutil.RequireRecvCtx(flushCtx, t, wuFlush) + testutil.RequireSend(flushCtx, t, wuTick, dbtime.Now()) + _ = testutil.TryReceive(flushCtx, t, wuFlush) updated, err := client.Workspace(context.Background(), workspace.ID) require.NoError(t, err) require.Greater(t, updated.LastUsedAt, workspace.LastUsedAt) @@ -315,8 +315,8 @@ func TestPortForward(t *testing.T) { require.ErrorIs(t, err, context.Canceled) flushCtx := testutil.Context(t, testutil.WaitShort) - testutil.RequireSendCtx(flushCtx, t, wuTick, dbtime.Now()) - _ = testutil.RequireRecvCtx(flushCtx, t, wuFlush) + testutil.RequireSend(flushCtx, t, wuTick, dbtime.Now()) + _ = testutil.TryReceive(flushCtx, t, wuFlush) updated, err := client.Workspace(context.Background(), workspace.ID) require.NoError(t, err) require.Greater(t, updated.LastUsedAt, workspace.LastUsedAt) @@ -372,8 +372,8 @@ func TestPortForward(t *testing.T) { require.ErrorIs(t, err, context.Canceled) flushCtx := testutil.Context(t, testutil.WaitShort) - testutil.RequireSendCtx(flushCtx, t, wuTick, dbtime.Now()) - _ = testutil.RequireRecvCtx(flushCtx, t, wuFlush) + testutil.RequireSend(flushCtx, t, wuTick, dbtime.Now()) + _ = testutil.TryReceive(flushCtx, t, wuFlush) updated, err := client.Workspace(context.Background(), workspace.ID) require.NoError(t, err) require.Greater(t, updated.LastUsedAt, workspace.LastUsedAt) diff --git a/cli/ssh_internal_test.go b/cli/ssh_internal_test.go index 159ee707b276e..d5e4c049347b2 100644 --- a/cli/ssh_internal_test.go +++ b/cli/ssh_internal_test.go @@ -98,7 +98,7 @@ func TestCloserStack_Empty(t *testing.T) { defer close(closed) uut.close(nil) }() - testutil.RequireRecvCtx(ctx, t, closed) + testutil.TryReceive(ctx, t, closed) } func TestCloserStack_Context(t *testing.T) { @@ -157,7 +157,7 @@ func TestCloserStack_CloseAfterContext(t *testing.T) { err := uut.push("async", ac) require.NoError(t, err) cancel() - testutil.RequireRecvCtx(testCtx, t, ac.started) + testutil.TryReceive(testCtx, t, ac.started) closed := make(chan struct{}) go func() { @@ -174,7 +174,7 @@ func TestCloserStack_CloseAfterContext(t *testing.T) { } ac.complete() - testutil.RequireRecvCtx(testCtx, t, closed) + testutil.TryReceive(testCtx, t, closed) } func TestCloserStack_Timeout(t *testing.T) { @@ -204,20 +204,20 @@ func TestCloserStack_Timeout(t *testing.T) { }() trap.MustWait(ctx).Release() // top starts right away, but it hangs - testutil.RequireRecvCtx(ctx, t, ac[2].started) + testutil.TryReceive(ctx, t, ac[2].started) // timer pops and we start the middle one mClock.Advance(gracefulShutdownTimeout).MustWait(ctx) - testutil.RequireRecvCtx(ctx, t, ac[1].started) + testutil.TryReceive(ctx, t, ac[1].started) // middle one finishes ac[1].complete() // bottom starts, but also hangs - testutil.RequireRecvCtx(ctx, t, ac[0].started) + testutil.TryReceive(ctx, t, ac[0].started) // timer has to pop twice to time out. mClock.Advance(gracefulShutdownTimeout).MustWait(ctx) mClock.Advance(gracefulShutdownTimeout).MustWait(ctx) - testutil.RequireRecvCtx(ctx, t, closed) + testutil.TryReceive(ctx, t, closed) } type fakeCloser struct { diff --git a/cli/ssh_test.go b/cli/ssh_test.go index 453073026e16f..c8ad072270169 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -271,12 +271,12 @@ func TestSSH(t *testing.T) { } // Allow one build to complete. - testutil.RequireSendCtx(ctx, t, buildPause, true) - testutil.RequireRecvCtx(ctx, t, buildDone) + testutil.RequireSend(ctx, t, buildPause, true) + testutil.TryReceive(ctx, t, buildDone) // Allow the remaining builds to continue. for i := 0; i < len(ptys)-1; i++ { - testutil.RequireSendCtx(ctx, t, buildPause, false) + testutil.RequireSend(ctx, t, buildPause, false) } var foundConflict int @@ -1017,14 +1017,14 @@ func TestSSH(t *testing.T) { } }() - msg := testutil.RequireRecvCtx(ctx, t, msgs) + msg := testutil.TryReceive(ctx, t, msgs) require.Equal(t, "test", msg) close(success) fsn.Notify() <-cmdDone fsn.AssertStopped() // wait for dial goroutine to complete - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) // wait for the remote socket to get cleaned up before retrying, // because cleaning up the socket happens asynchronously, and we diff --git a/cli/start_test.go b/cli/start_test.go index 07577998fbb9d..2e893bc20f5c4 100644 --- a/cli/start_test.go +++ b/cli/start_test.go @@ -408,7 +408,7 @@ func TestStart_AlreadyRunning(t *testing.T) { }() pty.ExpectMatch("workspace is already running") - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) } func TestStart_Starting(t *testing.T) { @@ -441,7 +441,7 @@ func TestStart_Starting(t *testing.T) { _ = dbfake.JobComplete(t, store, r.Build.JobID).Pubsub(ps).Do() pty.ExpectMatch("workspace has been started") - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) } func TestStart_NoWait(t *testing.T) { @@ -474,5 +474,5 @@ func TestStart_NoWait(t *testing.T) { }() pty.ExpectMatch("workspace has been started in no-wait mode") - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) } diff --git a/cli/update_test.go b/cli/update_test.go index 6f061f29a72b8..413c3d3c37f67 100644 --- a/cli/update_test.go +++ b/cli/update_test.go @@ -345,7 +345,7 @@ func TestUpdateValidateRichParameters(t *testing.T) { pty.ExpectMatch("does not match") pty.ExpectMatch("> Enter a value (default: \"\"): ") pty.WriteLine("abc") - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) t.Run("ValidateNumber", func(t *testing.T) { @@ -391,7 +391,7 @@ func TestUpdateValidateRichParameters(t *testing.T) { pty.ExpectMatch("is not a number") pty.ExpectMatch("> Enter a value (default: \"\"): ") pty.WriteLine("8") - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) t.Run("ValidateBool", func(t *testing.T) { @@ -437,7 +437,7 @@ func TestUpdateValidateRichParameters(t *testing.T) { pty.ExpectMatch("boolean value can be either \"true\" or \"false\"") pty.ExpectMatch("> Enter a value (default: \"\"): ") pty.WriteLine("false") - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) t.Run("RequiredParameterAdded", func(t *testing.T) { @@ -508,7 +508,7 @@ func TestUpdateValidateRichParameters(t *testing.T) { pty.WriteLine(value) } } - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) t.Run("OptionalParameterAdded", func(t *testing.T) { @@ -568,7 +568,7 @@ func TestUpdateValidateRichParameters(t *testing.T) { }() pty.ExpectMatch("Planning workspace...") - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) t.Run("ParameterOptionChanged", func(t *testing.T) { @@ -640,7 +640,7 @@ func TestUpdateValidateRichParameters(t *testing.T) { } } - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) t.Run("ParameterOptionDisappeared", func(t *testing.T) { @@ -713,7 +713,7 @@ func TestUpdateValidateRichParameters(t *testing.T) { } } - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) t.Run("ParameterOptionFailsMonotonicValidation", func(t *testing.T) { @@ -770,7 +770,7 @@ func TestUpdateValidateRichParameters(t *testing.T) { pty.ExpectMatch(match) } - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) t.Run("ImmutableRequiredParameterExists_MutableRequiredParameterAdded", func(t *testing.T) { @@ -838,7 +838,7 @@ func TestUpdateValidateRichParameters(t *testing.T) { } } - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) t.Run("MutableRequiredParameterExists_ImmutableRequiredParameterAdded", func(t *testing.T) { @@ -910,6 +910,6 @@ func TestUpdateValidateRichParameters(t *testing.T) { } } - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) }) } diff --git a/cli/usercreate_test.go b/cli/usercreate_test.go index 66f7975d0bcdf..81e1d0dceb756 100644 --- a/cli/usercreate_test.go +++ b/cli/usercreate_test.go @@ -39,7 +39,7 @@ func TestUserCreate(t *testing.T) { pty.ExpectMatch(match) pty.WriteLine(value) } - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) created, err := client.User(ctx, matches[1]) require.NoError(t, err) assert.Equal(t, matches[1], created.Username) @@ -72,7 +72,7 @@ func TestUserCreate(t *testing.T) { pty.ExpectMatch(match) pty.WriteLine(value) } - _ = testutil.RequireRecvCtx(ctx, t, doneChan) + _ = testutil.TryReceive(ctx, t, doneChan) created, err := client.User(ctx, matches[1]) require.NoError(t, err) assert.Equal(t, matches[1], created.Username) diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index c3fe158aa47b9..7a0b2af441fe4 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -400,7 +400,7 @@ func TestExecutorAutostartUserSuspended(t *testing.T) { }() // Then: nothing should happen - stats := testutil.RequireRecvCtx(ctx, t, statsCh) + stats := testutil.TryReceive(ctx, t, statsCh) assert.Len(t, stats.Errors, 0) assert.Len(t, stats.Transitions, 0) } @@ -1167,7 +1167,7 @@ func TestNotifications(t *testing.T) { // Wait for workspace to become dormant notifyEnq.Clear() ticker <- workspace.LastUsedAt.Add(timeTilDormant * 3) - _ = testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, statCh) + _ = testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, statCh) // Check that the workspace is dormant workspace = coderdtest.MustWorkspace(t, client, workspace.ID) diff --git a/coderd/database/pubsub/pubsub_internal_test.go b/coderd/database/pubsub/pubsub_internal_test.go index 9effdb2b1ed95..0f699b4e4d82c 100644 --- a/coderd/database/pubsub/pubsub_internal_test.go +++ b/coderd/database/pubsub/pubsub_internal_test.go @@ -160,19 +160,19 @@ func TestPubSub_DoesntBlockNotify(t *testing.T) { assert.NoError(t, err) cancels <- subCancel }() - subCancel := testutil.RequireRecvCtx(ctx, t, cancels) + subCancel := testutil.TryReceive(ctx, t, cancels) cancelDone := make(chan struct{}) go func() { defer close(cancelDone) subCancel() }() - testutil.RequireRecvCtx(ctx, t, cancelDone) + testutil.TryReceive(ctx, t, cancelDone) closeErrs := make(chan error) go func() { closeErrs <- uut.Close() }() - err := testutil.RequireRecvCtx(ctx, t, closeErrs) + err := testutil.TryReceive(ctx, t, closeErrs) require.NoError(t, err) } @@ -221,7 +221,7 @@ func TestPubSub_DoesntRaceListenUnlisten(t *testing.T) { } close(start) for range numEvents * 2 { - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } for i := range events { fListener.requireIsListening(t, events[i]) diff --git a/coderd/database/pubsub/pubsub_test.go b/coderd/database/pubsub/pubsub_test.go index 16227089682bb..4f4a387276355 100644 --- a/coderd/database/pubsub/pubsub_test.go +++ b/coderd/database/pubsub/pubsub_test.go @@ -60,7 +60,7 @@ func TestPGPubsub_Metrics(t *testing.T) { err := uut.Publish(event, []byte(data)) assert.NoError(t, err) }() - _ = testutil.RequireRecvCtx(ctx, t, messageChannel) + _ = testutil.TryReceive(ctx, t, messageChannel) require.Eventually(t, func() bool { latencyBytes := gatherCount * pubsub.LatencyMessageLength @@ -96,8 +96,8 @@ func TestPGPubsub_Metrics(t *testing.T) { assert.NoError(t, err) }() // should get 2 messages because we have 2 subs - _ = testutil.RequireRecvCtx(ctx, t, messageChannel) - _ = testutil.RequireRecvCtx(ctx, t, messageChannel) + _ = testutil.TryReceive(ctx, t, messageChannel) + _ = testutil.TryReceive(ctx, t, messageChannel) require.Eventually(t, func() bool { latencyBytes := gatherCount * pubsub.LatencyMessageLength @@ -167,10 +167,10 @@ func TestPGPubsubDriver(t *testing.T) { require.NoError(t, err) // wait for the message - _ = testutil.RequireRecvCtx(ctx, t, gotChan) + _ = testutil.TryReceive(ctx, t, gotChan) // read out first connection - firstConn := testutil.RequireRecvCtx(ctx, t, subDriver.Connections) + firstConn := testutil.TryReceive(ctx, t, subDriver.Connections) // drop the underlying connection being used by the pubsub // the pq.Listener should reconnect and repopulate it's listeners @@ -179,7 +179,7 @@ func TestPGPubsubDriver(t *testing.T) { require.NoError(t, err) // wait for the reconnect - _ = testutil.RequireRecvCtx(ctx, t, subDriver.Connections) + _ = testutil.TryReceive(ctx, t, subDriver.Connections) // we need to sleep because the raw connection notification // is sent before the pq.Listener can reestablish it's listeners time.Sleep(1 * time.Second) @@ -189,5 +189,5 @@ func TestPGPubsubDriver(t *testing.T) { require.NoError(t, err) // wait for the message on the old subscription - _ = testutil.RequireRecvCtx(ctx, t, gotChan) + _ = testutil.TryReceive(ctx, t, gotChan) } diff --git a/coderd/database/pubsub/watchdog_test.go b/coderd/database/pubsub/watchdog_test.go index 8a0550a35a15c..512d33c016e99 100644 --- a/coderd/database/pubsub/watchdog_test.go +++ b/coderd/database/pubsub/watchdog_test.go @@ -37,7 +37,7 @@ func TestWatchdog_NoTimeout(t *testing.T) { // we subscribe after starting the timer, so we know the timer also starts // from the baseline. - sub := testutil.RequireRecvCtx(ctx, t, fPS.subs) + sub := testutil.TryReceive(ctx, t, fPS.subs) require.Equal(t, pubsub.EventPubsubWatchdog, sub.event) // 5 min / 15 sec = 20, so do 21 ticks @@ -45,7 +45,7 @@ func TestWatchdog_NoTimeout(t *testing.T) { d, w := mClock.AdvanceNext() w.MustWait(ctx) require.LessOrEqual(t, d, 15*time.Second) - p := testutil.RequireRecvCtx(ctx, t, fPS.pubs) + p := testutil.TryReceive(ctx, t, fPS.pubs) require.Equal(t, pubsub.EventPubsubWatchdog, p) mClock.Advance(30 * time.Millisecond). // reasonable round-trip MustWait(ctx) @@ -67,7 +67,7 @@ func TestWatchdog_NoTimeout(t *testing.T) { sc, err := subTrap.Wait(ctx) // timer.Stop() called require.NoError(t, err) sc.Release() - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) } @@ -93,7 +93,7 @@ func TestWatchdog_Timeout(t *testing.T) { // we subscribe after starting the timer, so we know the timer also starts // from the baseline. - sub := testutil.RequireRecvCtx(ctx, t, fPS.subs) + sub := testutil.TryReceive(ctx, t, fPS.subs) require.Equal(t, pubsub.EventPubsubWatchdog, sub.event) // 5 min / 15 sec = 20, so do 19 ticks without timing out @@ -101,7 +101,7 @@ func TestWatchdog_Timeout(t *testing.T) { d, w := mClock.AdvanceNext() w.MustWait(ctx) require.LessOrEqual(t, d, 15*time.Second) - p := testutil.RequireRecvCtx(ctx, t, fPS.pubs) + p := testutil.TryReceive(ctx, t, fPS.pubs) require.Equal(t, pubsub.EventPubsubWatchdog, p) mClock.Advance(30 * time.Millisecond). // reasonable round-trip MustWait(ctx) @@ -117,9 +117,9 @@ func TestWatchdog_Timeout(t *testing.T) { d, w := mClock.AdvanceNext() w.MustWait(ctx) require.LessOrEqual(t, d, 15*time.Second) - p := testutil.RequireRecvCtx(ctx, t, fPS.pubs) + p := testutil.TryReceive(ctx, t, fPS.pubs) require.Equal(t, pubsub.EventPubsubWatchdog, p) - testutil.RequireRecvCtx(ctx, t, uut.Timeout()) + testutil.TryReceive(ctx, t, uut.Timeout()) err = uut.Close() require.NoError(t, err) diff --git a/coderd/entitlements/entitlements_test.go b/coderd/entitlements/entitlements_test.go index 59ba7dfa79e69..f74d662216ec4 100644 --- a/coderd/entitlements/entitlements_test.go +++ b/coderd/entitlements/entitlements_test.go @@ -78,7 +78,7 @@ func TestUpdate(t *testing.T) { }) errCh <- err }() - testutil.RequireRecvCtx(ctx, t, fetchStarted) + testutil.TryReceive(ctx, t, fetchStarted) require.False(t, set.Enabled(codersdk.FeatureMultipleOrganizations)) // start a second update while the first one is in progress go func() { @@ -97,9 +97,9 @@ func TestUpdate(t *testing.T) { errCh <- err }() close(firstDone) - err := testutil.RequireRecvCtx(ctx, t, errCh) + err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) require.True(t, set.Enabled(codersdk.FeatureMultipleOrganizations)) require.True(t, set.Enabled(codersdk.FeatureAppearance)) diff --git a/coderd/httpmw/loggermw/logger_internal_test.go b/coderd/httpmw/loggermw/logger_internal_test.go index e88f8a69c178e..53cc9f4eb9462 100644 --- a/coderd/httpmw/loggermw/logger_internal_test.go +++ b/coderd/httpmw/loggermw/logger_internal_test.go @@ -146,7 +146,7 @@ func TestLoggerMiddleware_WebSocket(t *testing.T) { defer conn.Close(websocket.StatusNormalClosure, "") // Wait for the log from within the handler - newEntry := testutil.RequireRecvCtx(ctx, t, sink.newEntries) + newEntry := testutil.TryReceive(ctx, t, sink.newEntries) require.Equal(t, newEntry.Message, "GET") // Signal the websocket handler to return (and read to handle the close frame) @@ -155,7 +155,7 @@ func TestLoggerMiddleware_WebSocket(t *testing.T) { require.ErrorAs(t, err, &websocket.CloseError{}, "websocket read should fail with close error") // Wait for the request to finish completely and verify we only logged once - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) require.Len(t, sink.entries, 1, "log was written twice") } diff --git a/coderd/notifications/manager_test.go b/coderd/notifications/manager_test.go index 590cc4f73cb03..3eaebef7c9d0f 100644 --- a/coderd/notifications/manager_test.go +++ b/coderd/notifications/manager_test.go @@ -155,7 +155,7 @@ func TestBuildPayload(t *testing.T) { require.NoError(t, err) // THEN: expect that a payload will be constructed and have the expected values - payload := testutil.RequireRecvCtx(ctx, t, interceptor.payload) + payload := testutil.TryReceive(ctx, t, interceptor.payload) require.Len(t, payload.Actions, 1) require.Equal(t, label, payload.Actions[0].Label) require.Equal(t, url, payload.Actions[0].URL) diff --git a/coderd/notifications/metrics_test.go b/coderd/notifications/metrics_test.go index 6e7be0d49efbe..e88282bbc1861 100644 --- a/coderd/notifications/metrics_test.go +++ b/coderd/notifications/metrics_test.go @@ -300,9 +300,9 @@ func TestPendingUpdatesMetric(t *testing.T) { mClock.Advance(cfg.StoreSyncInterval.Value() - cfg.FetchInterval.Value()).MustWait(ctx) // Wait until we intercept the calls to sync the pending updates to the store. - success := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, interceptor.updateSuccess) + success := testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, interceptor.updateSuccess) require.EqualValues(t, 2, success) - failure := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, interceptor.updateFailure) + failure := testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, interceptor.updateFailure) require.EqualValues(t, 2, failure) // Validate that the store synced the expected number of updates. diff --git a/coderd/notifications/notifications_test.go b/coderd/notifications/notifications_test.go index 5f6c221e7beb5..12372b74a14c3 100644 --- a/coderd/notifications/notifications_test.go +++ b/coderd/notifications/notifications_test.go @@ -260,7 +260,7 @@ func TestWebhookDispatch(t *testing.T) { mgr.Run(ctx) // THEN: the webhook is received by the mock server and has the expected contents - payload := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, sent) + payload := testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, sent) require.EqualValues(t, "1.1", payload.Version) require.Equal(t, msgID[0], payload.MsgID) require.Equal(t, payload.Payload.Labels, input) @@ -350,8 +350,8 @@ func TestBackpressure(t *testing.T) { // one batch of dispatches is sent for range batchSize { - call := testutil.RequireRecvCtx(ctx, t, handler.calls) - testutil.RequireSendCtx(ctx, t, call.result, dispatchResult{ + call := testutil.TryReceive(ctx, t, handler.calls) + testutil.RequireSend(ctx, t, call.result, dispatchResult{ retryable: false, err: nil, }) @@ -402,7 +402,7 @@ func TestBackpressure(t *testing.T) { // The batch completes w.MustWait(ctx) - require.NoError(t, testutil.RequireRecvCtx(ctx, t, stopErr)) + require.NoError(t, testutil.TryReceive(ctx, t, stopErr)) require.EqualValues(t, batchSize, storeInterceptor.sent.Load()+storeInterceptor.failed.Load()) } @@ -1808,7 +1808,7 @@ func TestCustomNotificationMethod(t *testing.T) { // THEN: the notification should be received by the custom dispatch method mgr.Run(ctx) - receivedMsgID := testutil.RequireRecvCtx(ctx, t, received) + receivedMsgID := testutil.TryReceive(ctx, t, received) require.Equal(t, msgID[0].String(), receivedMsgID.String()) // Ensure no messages received by default method (SMTP): diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 87f6be1507866..9a9eb91ac8b73 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -118,7 +118,7 @@ func TestHeartbeat(t *testing.T) { }) for i := 0; i < numBeats; i++ { - testutil.RequireRecvCtx(ctx, t, heartbeatChan) + testutil.TryReceive(ctx, t, heartbeatChan) } // goleak.VerifyTestMain ensures that the heartbeat goroutine does not leak } diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go index ad7d37e2cc849..163af320afbe9 100644 --- a/coderd/rbac/authz_test.go +++ b/coderd/rbac/authz_test.go @@ -362,7 +362,7 @@ func TestCache(t *testing.T) { authOut = make(chan error, 1) // buffered to not block authorizeFunc = func(ctx context.Context, subject rbac.Subject, action policy.Action, object rbac.Object) error { // Just return what you're told. - return testutil.RequireRecvCtx(ctx, t, authOut) + return testutil.TryReceive(ctx, t, authOut) } ma = &rbac.MockAuthorizer{AuthorizeFunc: authorizeFunc} rec = &coderdtest.RecordingAuthorizer{Wrapped: ma} @@ -371,12 +371,12 @@ func TestCache(t *testing.T) { ) // First call will result in a transient error. This should not be cached. - testutil.RequireSendCtx(ctx, t, authOut, context.Canceled) + testutil.RequireSend(ctx, t, authOut, context.Canceled) err := authz.Authorize(ctx, subj, action, obj) assert.ErrorIs(t, err, context.Canceled) // A subsequent call should still hit the authorizer. - testutil.RequireSendCtx(ctx, t, authOut, nil) + testutil.RequireSend(ctx, t, authOut, nil) err = authz.Authorize(ctx, subj, action, obj) assert.NoError(t, err) // This should be cached and not hit the wrapped authorizer again. @@ -387,7 +387,7 @@ func TestCache(t *testing.T) { subj, obj, action = coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() // A third will be a legit error - testutil.RequireSendCtx(ctx, t, authOut, assert.AnError) + testutil.RequireSend(ctx, t, authOut, assert.AnError) err = authz.Authorize(ctx, subj, action, obj) assert.EqualError(t, err, assert.AnError.Error()) // This should be cached and not hit the wrapped authorizer again. diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 4fe4550dd6806..83a5fd67a9761 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -2172,7 +2172,7 @@ func TestTemplateVersionDynamicParameters(t *testing.T) { previews := stream.Chan() // Should automatically send a form state with all defaulted/empty values - preview := testutil.RequireRecvCtx(ctx, t, previews) + preview := testutil.TryReceive(ctx, t, previews) require.Empty(t, preview.Diagnostics) require.Equal(t, "group", preview.Parameters[0].Name) require.True(t, preview.Parameters[0].Value.Valid()) @@ -2184,7 +2184,7 @@ func TestTemplateVersionDynamicParameters(t *testing.T) { Inputs: map[string]string{"group": "Bloob"}, }) require.NoError(t, err) - preview = testutil.RequireRecvCtx(ctx, t, previews) + preview = testutil.TryReceive(ctx, t, previews) require.Equal(t, 1, preview.ID) require.Empty(t, preview.Diagnostics) require.Equal(t, "group", preview.Parameters[0].Name) @@ -2197,7 +2197,7 @@ func TestTemplateVersionDynamicParameters(t *testing.T) { Inputs: map[string]string{}, }) require.NoError(t, err) - preview = testutil.RequireRecvCtx(ctx, t, previews) + preview = testutil.TryReceive(ctx, t, previews) require.Equal(t, 3, preview.ID) require.Empty(t, preview.Diagnostics) require.Equal(t, "group", preview.Parameters[0].Name) diff --git a/coderd/users_test.go b/coderd/users_test.go index e32b6d0c5b927..2e8eb5f3e842e 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -117,8 +117,8 @@ func TestFirstUser(t *testing.T) { _, err := client.CreateFirstUser(ctx, req) require.NoError(t, err) - _ = testutil.RequireRecvCtx(ctx, t, trialGenerated) - _ = testutil.RequireRecvCtx(ctx, t, entitlementsRefreshed) + _ = testutil.TryReceive(ctx, t, trialGenerated) + _ = testutil.TryReceive(ctx, t, entitlementsRefreshed) }) } diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go index de935176f22ac..a6e10ea5fdabf 100644 --- a/coderd/workspaceagents_test.go +++ b/coderd/workspaceagents_test.go @@ -653,7 +653,7 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { // random value. originalResumeToken, err := connectToCoordinatorAndFetchResumeToken(ctx, logger, client, agentAndBuild.WorkspaceAgent.ID, "") require.NoError(t, err) - originalPeerID := testutil.RequireRecvCtx(ctx, t, resumeTokenProvider.generateCalls) + originalPeerID := testutil.TryReceive(ctx, t, resumeTokenProvider.generateCalls) require.NotEqual(t, originalPeerID, uuid.Nil) // Connect with a valid resume token, and ensure that the peer ID is set to @@ -661,9 +661,9 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { clock.Advance(time.Second) newResumeToken, err := connectToCoordinatorAndFetchResumeToken(ctx, logger, client, agentAndBuild.WorkspaceAgent.ID, originalResumeToken) require.NoError(t, err) - verifiedToken := testutil.RequireRecvCtx(ctx, t, resumeTokenProvider.verifyCalls) + verifiedToken := testutil.TryReceive(ctx, t, resumeTokenProvider.verifyCalls) require.Equal(t, originalResumeToken, verifiedToken) - newPeerID := testutil.RequireRecvCtx(ctx, t, resumeTokenProvider.generateCalls) + newPeerID := testutil.TryReceive(ctx, t, resumeTokenProvider.generateCalls) require.Equal(t, originalPeerID, newPeerID) require.NotEqual(t, originalResumeToken, newResumeToken) @@ -677,7 +677,7 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode()) require.Len(t, sdkErr.Validations, 1) require.Equal(t, "resume_token", sdkErr.Validations[0].Field) - verifiedToken = testutil.RequireRecvCtx(ctx, t, resumeTokenProvider.verifyCalls) + verifiedToken = testutil.TryReceive(ctx, t, resumeTokenProvider.verifyCalls) require.Equal(t, "invalid", verifiedToken) select { @@ -725,7 +725,7 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { // random value. originalResumeToken, err := connectToCoordinatorAndFetchResumeToken(ctx, logger, client, agentAndBuild.WorkspaceAgent.ID, "") require.NoError(t, err) - originalPeerID := testutil.RequireRecvCtx(ctx, t, resumeTokenProvider.generateCalls) + originalPeerID := testutil.TryReceive(ctx, t, resumeTokenProvider.generateCalls) require.NotEqual(t, originalPeerID, uuid.Nil) // Connect with an outdated token, and ensure that the peer ID is set to a @@ -739,9 +739,9 @@ func TestWorkspaceAgentClientCoordinate_ResumeToken(t *testing.T) { clock.Advance(time.Second) newResumeToken, err := connectToCoordinatorAndFetchResumeToken(ctx, logger, client, agentAndBuild.WorkspaceAgent.ID, outdatedToken) require.NoError(t, err) - verifiedToken := testutil.RequireRecvCtx(ctx, t, resumeTokenProvider.verifyCalls) + verifiedToken := testutil.TryReceive(ctx, t, resumeTokenProvider.verifyCalls) require.Equal(t, outdatedToken, verifiedToken) - newPeerID := testutil.RequireRecvCtx(ctx, t, resumeTokenProvider.generateCalls) + newPeerID := testutil.TryReceive(ctx, t, resumeTokenProvider.generateCalls) require.NotEqual(t, originalPeerID, newPeerID) require.NotEqual(t, originalResumeToken, newResumeToken) }) @@ -1912,8 +1912,8 @@ func TestWorkspaceAgent_Metadata_CatchMemoryLeak(t *testing.T) { // testing it is not straightforward. db.err.Store(&wantErr) - testutil.RequireRecvCtx(ctx, t, metadataDone) - testutil.RequireRecvCtx(ctx, t, postDone) + testutil.TryReceive(ctx, t, metadataDone) + testutil.TryReceive(ctx, t, postDone) } func TestWorkspaceAgent_Startup(t *testing.T) { @@ -2358,7 +2358,7 @@ func TestUserTailnetTelemetry(t *testing.T) { defer wsConn.Close(websocket.StatusNormalClosure, "done") // Check telemetry - snapshot := testutil.RequireRecvCtx(ctx, t, fTelemetry.snapshots) + snapshot := testutil.TryReceive(ctx, t, fTelemetry.snapshots) require.Len(t, snapshot.UserTailnetConnections, 1) telemetryConnection := snapshot.UserTailnetConnections[0] require.Equal(t, memberUser.ID.String(), telemetryConnection.UserID) @@ -2373,7 +2373,7 @@ func TestUserTailnetTelemetry(t *testing.T) { err = wsConn.Close(websocket.StatusNormalClosure, "done") require.NoError(t, err) - snapshot = testutil.RequireRecvCtx(ctx, t, fTelemetry.snapshots) + snapshot = testutil.TryReceive(ctx, t, fTelemetry.snapshots) require.Len(t, snapshot.UserTailnetConnections, 1) telemetryDisconnection := snapshot.UserTailnetConnections[0] require.Equal(t, memberUser.ID.String(), telemetryDisconnection.UserID) diff --git a/coderd/workspaceagentsrpc_internal_test.go b/coderd/workspaceagentsrpc_internal_test.go index 36bc3bf73305e..f2a2c7c87fa37 100644 --- a/coderd/workspaceagentsrpc_internal_test.go +++ b/coderd/workspaceagentsrpc_internal_test.go @@ -90,7 +90,7 @@ func TestAgentConnectionMonitor_ContextCancel(t *testing.T) { fConn.requireEventuallyClosed(t, websocket.StatusGoingAway, "canceled") // make sure we got at least one additional update on close - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) m := fUpdater.getUpdates() require.Greater(t, m, n) } @@ -293,7 +293,7 @@ func TestAgentConnectionMonitor_StartClose(t *testing.T) { uut.close() close(closed) }() - _ = testutil.RequireRecvCtx(ctx, t, closed) + _ = testutil.TryReceive(ctx, t, closed) } type fakePingerCloser struct { diff --git a/coderd/workspaceupdates_test.go b/coderd/workspaceupdates_test.go index a41c71c1ee28d..e2b5db0fcc606 100644 --- a/coderd/workspaceupdates_test.go +++ b/coderd/workspaceupdates_test.go @@ -108,7 +108,7 @@ func TestWorkspaceUpdates(t *testing.T) { _ = sub.Close() }) - update := testutil.RequireRecvCtx(ctx, t, sub.Updates()) + update := testutil.TryReceive(ctx, t, sub.Updates()) slices.SortFunc(update.UpsertedWorkspaces, func(a, b *proto.Workspace) int { return strings.Compare(a.Name, b.Name) }) @@ -185,7 +185,7 @@ func TestWorkspaceUpdates(t *testing.T) { WorkspaceID: ws1ID, }) - update = testutil.RequireRecvCtx(ctx, t, sub.Updates()) + update = testutil.TryReceive(ctx, t, sub.Updates()) slices.SortFunc(update.UpsertedWorkspaces, func(a, b *proto.Workspace) int { return strings.Compare(a.Name, b.Name) }) @@ -284,7 +284,7 @@ func TestWorkspaceUpdates(t *testing.T) { DeletedAgents: []*proto.Agent{}, } - update := testutil.RequireRecvCtx(ctx, t, sub.Updates()) + update := testutil.TryReceive(ctx, t, sub.Updates()) slices.SortFunc(update.UpsertedWorkspaces, func(a, b *proto.Workspace) int { return strings.Compare(a.Name, b.Name) }) @@ -296,7 +296,7 @@ func TestWorkspaceUpdates(t *testing.T) { _ = resub.Close() }) - update = testutil.RequireRecvCtx(ctx, t, resub.Updates()) + update = testutil.TryReceive(ctx, t, resub.Updates()) slices.SortFunc(update.UpsertedWorkspaces, func(a, b *proto.Workspace) int { return strings.Compare(a.Name, b.Name) }) diff --git a/codersdk/agentsdk/logs_internal_test.go b/codersdk/agentsdk/logs_internal_test.go index 2c8bc4748e2e0..a8e42102391ba 100644 --- a/codersdk/agentsdk/logs_internal_test.go +++ b/codersdk/agentsdk/logs_internal_test.go @@ -63,10 +63,10 @@ func TestLogSender_Mainline(t *testing.T) { // since neither source has even been flushed, it should immediately Flush // both, although the order is not controlled var logReqs []*proto.BatchCreateLogsRequest - logReqs = append(logReqs, testutil.RequireRecvCtx(ctx, t, fDest.reqs)) - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) - logReqs = append(logReqs, testutil.RequireRecvCtx(ctx, t, fDest.reqs)) - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) + logReqs = append(logReqs, testutil.TryReceive(ctx, t, fDest.reqs)) + testutil.RequireSend(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) + logReqs = append(logReqs, testutil.TryReceive(ctx, t, fDest.reqs)) + testutil.RequireSend(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) for _, req := range logReqs { require.NotNil(t, req) srcID, err := uuid.FromBytes(req.LogSourceId) @@ -98,8 +98,8 @@ func TestLogSender_Mainline(t *testing.T) { }) uut.Flush(ls1) - req := testutil.RequireRecvCtx(ctx, t, fDest.reqs) - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) + req := testutil.TryReceive(ctx, t, fDest.reqs) + testutil.RequireSend(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) // give ourselves a 25% buffer if we're right on the cusp of a tick require.LessOrEqual(t, time.Since(t1), flushInterval*5/4) require.NotNil(t, req) @@ -108,11 +108,11 @@ func TestLogSender_Mainline(t *testing.T) { require.Equal(t, proto.Log_DEBUG, req.Logs[0].GetLevel()) require.Equal(t, t1, req.Logs[0].GetCreatedAt().AsTime()) - err := testutil.RequireRecvCtx(ctx, t, empty) + err := testutil.TryReceive(ctx, t, empty) require.NoError(t, err) cancel() - err = testutil.RequireRecvCtx(testCtx, t, loopErr) + err = testutil.TryReceive(testCtx, t, loopErr) require.ErrorIs(t, err, context.Canceled) // we can still enqueue more logs after SendLoop returns @@ -151,16 +151,16 @@ func TestLogSender_LogLimitExceeded(t *testing.T) { loopErr <- err }() - req := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + req := testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, req) - testutil.RequireSendCtx(ctx, t, fDest.resps, + testutil.RequireSend(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{LogLimitExceeded: true}) - err := testutil.RequireRecvCtx(ctx, t, loopErr) + err := testutil.TryReceive(ctx, t, loopErr) require.ErrorIs(t, err, ErrLogLimitExceeded) // Should also unblock WaitUntilEmpty - err = testutil.RequireRecvCtx(ctx, t, empty) + err = testutil.TryReceive(ctx, t, empty) require.NoError(t, err) // we can still enqueue more logs after SendLoop returns, but they don't @@ -179,7 +179,7 @@ func TestLogSender_LogLimitExceeded(t *testing.T) { err := uut.SendLoop(ctx, fDest) loopErr <- err }() - err = testutil.RequireRecvCtx(ctx, t, loopErr) + err = testutil.TryReceive(ctx, t, loopErr) require.ErrorIs(t, err, ErrLogLimitExceeded) } @@ -217,15 +217,15 @@ func TestLogSender_SkipHugeLog(t *testing.T) { loopErr <- err }() - req := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + req := testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, req) require.Len(t, req.Logs, 1, "it should skip the huge log") require.Equal(t, "test log 1, src 1", req.Logs[0].GetOutput()) require.Equal(t, proto.Log_INFO, req.Logs[0].GetLevel()) - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) + testutil.RequireSend(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) cancel() - err := testutil.RequireRecvCtx(testCtx, t, loopErr) + err := testutil.TryReceive(testCtx, t, loopErr) require.ErrorIs(t, err, context.Canceled) } @@ -258,7 +258,7 @@ func TestLogSender_InvalidUTF8(t *testing.T) { loopErr <- err }() - req := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + req := testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, req) require.Len(t, req.Logs, 2, "it should sanitize invalid UTF-8, but still send") // the 0xc3, 0x28 is an invalid 2-byte sequence in UTF-8. The sanitizer replaces 0xc3 with ❌, and then @@ -267,10 +267,10 @@ func TestLogSender_InvalidUTF8(t *testing.T) { require.Equal(t, proto.Log_INFO, req.Logs[0].GetLevel()) require.Equal(t, "test log 1, src 1", req.Logs[1].GetOutput()) require.Equal(t, proto.Log_INFO, req.Logs[1].GetLevel()) - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) + testutil.RequireSend(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) cancel() - err := testutil.RequireRecvCtx(testCtx, t, loopErr) + err := testutil.TryReceive(testCtx, t, loopErr) require.ErrorIs(t, err, context.Canceled) } @@ -303,24 +303,24 @@ func TestLogSender_Batch(t *testing.T) { // with 60k logs, we should split into two updates to avoid going over 1MiB, since each log // is about 21 bytes. gotLogs := 0 - req := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + req := testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, req) gotLogs += len(req.Logs) wire, err := protobuf.Marshal(req) require.NoError(t, err) require.Less(t, len(wire), maxBytesPerBatch, "wire should not exceed 1MiB") - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) - req = testutil.RequireRecvCtx(ctx, t, fDest.reqs) + testutil.RequireSend(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) + req = testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, req) gotLogs += len(req.Logs) wire, err = protobuf.Marshal(req) require.NoError(t, err) require.Less(t, len(wire), maxBytesPerBatch, "wire should not exceed 1MiB") require.Equal(t, 60000, gotLogs) - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) + testutil.RequireSend(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) cancel() - err = testutil.RequireRecvCtx(testCtx, t, loopErr) + err = testutil.TryReceive(testCtx, t, loopErr) require.ErrorIs(t, err, context.Canceled) } @@ -367,12 +367,12 @@ func TestLogSender_MaxQueuedLogs(t *testing.T) { // #1 come in 2 updates, plus 1 update for source #2. logsBySource := make(map[uuid.UUID]int) for i := 0; i < 3; i++ { - req := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + req := testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, req) srcID, err := uuid.FromBytes(req.LogSourceId) require.NoError(t, err) logsBySource[srcID] += len(req.Logs) - testutil.RequireSendCtx(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) + testutil.RequireSend(ctx, t, fDest.resps, &proto.BatchCreateLogsResponse{}) } require.Equal(t, map[uuid.UUID]int{ ls1: n, @@ -380,7 +380,7 @@ func TestLogSender_MaxQueuedLogs(t *testing.T) { }, logsBySource) cancel() - err := testutil.RequireRecvCtx(testCtx, t, loopErr) + err := testutil.TryReceive(testCtx, t, loopErr) require.ErrorIs(t, err, context.Canceled) } @@ -408,10 +408,10 @@ func TestLogSender_SendError(t *testing.T) { loopErr <- err }() - req := testutil.RequireRecvCtx(ctx, t, fDest.reqs) + req := testutil.TryReceive(ctx, t, fDest.reqs) require.NotNil(t, req) - err := testutil.RequireRecvCtx(ctx, t, loopErr) + err := testutil.TryReceive(ctx, t, loopErr) require.ErrorIs(t, err, expectedErr) // we can still enqueue more logs after SendLoop returns @@ -448,7 +448,7 @@ func TestLogSender_WaitUntilEmpty_ContextExpired(t *testing.T) { }() cancel() - err := testutil.RequireRecvCtx(testCtx, t, empty) + err := testutil.TryReceive(testCtx, t, empty) require.ErrorIs(t, err, context.Canceled) } diff --git a/codersdk/workspacesdk/dialer_test.go b/codersdk/workspacesdk/dialer_test.go index 58b428a15fa04..dbe351e4e492c 100644 --- a/codersdk/workspacesdk/dialer_test.go +++ b/codersdk/workspacesdk/dialer_test.go @@ -80,15 +80,15 @@ func TestWebsocketDialer_TokenController(t *testing.T) { clientCh <- clients }() - call := testutil.RequireRecvCtx(ctx, t, fTokenProv.tokenCalls) + call := testutil.TryReceive(ctx, t, fTokenProv.tokenCalls) call <- tokenResponse{"test token", true} gotToken := <-dialTokens require.Equal(t, "test token", gotToken) - clients := testutil.RequireRecvCtx(ctx, t, clientCh) + clients := testutil.TryReceive(ctx, t, clientCh) clients.Closer.Close() - err = testutil.RequireRecvCtx(ctx, t, wsErr) + err = testutil.TryReceive(ctx, t, wsErr) require.NoError(t, err) clientCh = make(chan tailnet.ControlProtocolClients, 1) @@ -98,16 +98,16 @@ func TestWebsocketDialer_TokenController(t *testing.T) { clientCh <- clients }() - call = testutil.RequireRecvCtx(ctx, t, fTokenProv.tokenCalls) + call = testutil.TryReceive(ctx, t, fTokenProv.tokenCalls) call <- tokenResponse{"test token", false} gotToken = <-dialTokens require.Equal(t, "", gotToken) - clients = testutil.RequireRecvCtx(ctx, t, clientCh) + clients = testutil.TryReceive(ctx, t, clientCh) require.Nil(t, clients.WorkspaceUpdates) clients.Closer.Close() - err = testutil.RequireRecvCtx(ctx, t, wsErr) + err = testutil.TryReceive(ctx, t, wsErr) require.NoError(t, err) } @@ -165,10 +165,10 @@ func TestWebsocketDialer_NoTokenController(t *testing.T) { gotToken := <-dialTokens require.Equal(t, "", gotToken) - clients := testutil.RequireRecvCtx(ctx, t, clientCh) + clients := testutil.TryReceive(ctx, t, clientCh) clients.Closer.Close() - err = testutil.RequireRecvCtx(ctx, t, wsErr) + err = testutil.TryReceive(ctx, t, wsErr) require.NoError(t, err) } @@ -233,12 +233,12 @@ func TestWebsocketDialer_ResumeTokenFailure(t *testing.T) { errCh <- err }() - call := testutil.RequireRecvCtx(ctx, t, fTokenProv.tokenCalls) + call := testutil.TryReceive(ctx, t, fTokenProv.tokenCalls) call <- tokenResponse{"test token", true} gotToken := <-dialTokens require.Equal(t, "test token", gotToken) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.Error(t, err) // redial should not use the token @@ -251,10 +251,10 @@ func TestWebsocketDialer_ResumeTokenFailure(t *testing.T) { gotToken = <-dialTokens require.Equal(t, "", gotToken) - clients := testutil.RequireRecvCtx(ctx, t, clientCh) + clients := testutil.TryReceive(ctx, t, clientCh) require.Error(t, err) clients.Closer.Close() - err = testutil.RequireRecvCtx(ctx, t, wsErr) + err = testutil.TryReceive(ctx, t, wsErr) require.NoError(t, err) // Successful dial should reset to using token again @@ -262,11 +262,11 @@ func TestWebsocketDialer_ResumeTokenFailure(t *testing.T) { _, err := uut.Dial(ctx, fTokenProv) errCh <- err }() - call = testutil.RequireRecvCtx(ctx, t, fTokenProv.tokenCalls) + call = testutil.TryReceive(ctx, t, fTokenProv.tokenCalls) call <- tokenResponse{"test token", true} gotToken = <-dialTokens require.Equal(t, "test token", gotToken) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.Error(t, err) } @@ -305,7 +305,7 @@ func TestWebsocketDialer_UplevelVersion(t *testing.T) { errCh <- err }() - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) var sdkErr *codersdk.Error require.ErrorAs(t, err, &sdkErr) require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode()) @@ -387,7 +387,7 @@ func TestWebsocketDialer_WorkspaceUpdates(t *testing.T) { clients.Closer.Close() - err = testutil.RequireRecvCtx(ctx, t, wsErr) + err = testutil.TryReceive(ctx, t, wsErr) require.NoError(t, err) } diff --git a/enterprise/tailnet/pgcoord_internal_test.go b/enterprise/tailnet/pgcoord_internal_test.go index 2fed758d74ae9..709fb0c225bcc 100644 --- a/enterprise/tailnet/pgcoord_internal_test.go +++ b/enterprise/tailnet/pgcoord_internal_test.go @@ -427,7 +427,7 @@ func TestPGCoordinatorUnhealthy(t *testing.T) { pID := uuid.UUID{5} _, resps := coordinator.Coordinate(ctx, pID, "test", agpl.AgentCoordinateeAuth{ID: pID}) - resp := testutil.RequireRecvCtx(ctx, t, resps) + resp := testutil.TryReceive(ctx, t, resps) require.Nil(t, resp, "channel should be closed") // give the coordinator some time to process any pending work. We are diff --git a/enterprise/tailnet/pgcoord_test.go b/enterprise/tailnet/pgcoord_test.go index b8f2c4718357c..97f68daec9f4e 100644 --- a/enterprise/tailnet/pgcoord_test.go +++ b/enterprise/tailnet/pgcoord_test.go @@ -943,9 +943,9 @@ func TestPGCoordinatorPropogatedPeerContext(t *testing.T) { reqs, _ := c1.Coordinate(peerCtx, peerID, "peer1", auth) - testutil.RequireSendCtx(ctx, t, reqs, &proto.CoordinateRequest{AddTunnel: &proto.CoordinateRequest_Tunnel{Id: agpl.UUIDToByteSlice(agentID)}}) + testutil.RequireSend(ctx, t, reqs, &proto.CoordinateRequest{AddTunnel: &proto.CoordinateRequest_Tunnel{Id: agpl.UUIDToByteSlice(agentID)}}) - _ = testutil.RequireRecvCtx(ctx, t, ch) + _ = testutil.TryReceive(ctx, t, ch) } func assertEventuallyStatus(ctx context.Context, t *testing.T, store database.Store, agentID uuid.UUID, status database.TailnetStatus) { diff --git a/enterprise/wsproxy/wsproxy_test.go b/enterprise/wsproxy/wsproxy_test.go index 4add46af9bc0a..65de627a1fb06 100644 --- a/enterprise/wsproxy/wsproxy_test.go +++ b/enterprise/wsproxy/wsproxy_test.go @@ -780,7 +780,7 @@ func TestWorkspaceProxyDERPMeshProbe(t *testing.T) { require.NoError(t, err, "failed to force proxy to re-register") // Wait for the ping to fail. - replicaErr := testutil.RequireRecvCtx(ctx, t, replicaPingErr) + replicaErr := testutil.TryReceive(ctx, t, replicaPingErr) require.NotEmpty(t, replicaErr, "replica ping error") // GET /healthz-report @@ -858,7 +858,7 @@ func TestWorkspaceProxyDERPMeshProbe(t *testing.T) { // Wait for the ping to fail. for { - replicaErr := testutil.RequireRecvCtx(ctx, t, replicaPingErr) + replicaErr := testutil.TryReceive(ctx, t, replicaPingErr) t.Log("replica ping error:", replicaErr) if replicaErr != "" { break @@ -892,7 +892,7 @@ func TestWorkspaceProxyDERPMeshProbe(t *testing.T) { // Wait for the ping to be skipped. for { - replicaErr := testutil.RequireRecvCtx(ctx, t, replicaPingErr) + replicaErr := testutil.TryReceive(ctx, t, replicaPingErr) t.Log("replica ping error:", replicaErr) // Should be empty because there are no more peers. This was where // the regression was. diff --git a/scaletest/createworkspaces/run_test.go b/scaletest/createworkspaces/run_test.go index b47ee73548b4f..c63854ff8a1fd 100644 --- a/scaletest/createworkspaces/run_test.go +++ b/scaletest/createworkspaces/run_test.go @@ -293,7 +293,7 @@ func Test_Runner(t *testing.T) { <-done t.Log("canceled scaletest workspace creation") // Ensure we have a job to interrogate - runningJob := testutil.RequireRecvCtx(testutil.Context(t, testutil.WaitShort), t, jobCh) + runningJob := testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, jobCh) require.NotZero(t, runningJob.ID) // When we run the cleanup, it should be canceled diff --git a/tailnet/configmaps_internal_test.go b/tailnet/configmaps_internal_test.go index 1727d4b5e27cd..fa027ffc7fdd4 100644 --- a/tailnet/configmaps_internal_test.go +++ b/tailnet/configmaps_internal_test.go @@ -40,7 +40,7 @@ func TestConfigMaps_setAddresses_different(t *testing.T) { addrs := []netip.Prefix{netip.MustParsePrefix("192.168.0.200/32")} uut.setAddresses(addrs) - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) require.Equal(t, addrs, nm.Addresses) // here were in the middle of a reconfig, blocked on a channel write to fEng.reconfig @@ -55,22 +55,22 @@ func TestConfigMaps_setAddresses_different(t *testing.T) { } uut.setAddresses(addrs2) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Equal(t, addrs, r.wg.Addresses) require.Equal(t, addrs, r.router.LocalAddrs) - f := testutil.RequireRecvCtx(ctx, t, fEng.filter) + f := testutil.TryReceive(ctx, t, fEng.filter) fr := f.CheckTCP(netip.MustParseAddr("33.44.55.66"), netip.MustParseAddr("192.168.0.200"), 5555) require.Equal(t, filter.Accept, fr) fr = f.CheckTCP(netip.MustParseAddr("33.44.55.66"), netip.MustParseAddr("10.20.30.40"), 5555) require.Equal(t, filter.Drop, fr, "first addr config should not include 10.20.30.40") // we should get another round of configurations from the second set of addrs - nm = testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) + nm = testutil.TryReceive(ctx, t, fEng.setNetworkMap) require.Equal(t, addrs2, nm.Addresses) - r = testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + r = testutil.TryReceive(ctx, t, fEng.reconfig) require.Equal(t, addrs2, r.wg.Addresses) require.Equal(t, addrs2, r.router.LocalAddrs) - f = testutil.RequireRecvCtx(ctx, t, fEng.filter) + f = testutil.TryReceive(ctx, t, fEng.filter) fr = f.CheckTCP(netip.MustParseAddr("33.44.55.66"), netip.MustParseAddr("192.168.0.200"), 5555) require.Equal(t, filter.Accept, fr) fr = f.CheckTCP(netip.MustParseAddr("33.44.55.66"), netip.MustParseAddr("10.20.30.40"), 5555) @@ -81,7 +81,7 @@ func TestConfigMaps_setAddresses_different(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_setAddresses_same(t *testing.T) { @@ -112,7 +112,7 @@ func TestConfigMaps_setAddresses_same(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_updatePeers_new(t *testing.T) { @@ -160,8 +160,8 @@ func TestConfigMaps_updatePeers_new(t *testing.T) { } uut.updatePeers(updates) - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 2) n1 := getNodeWithID(t, nm.Peers, 1) @@ -182,7 +182,7 @@ func TestConfigMaps_updatePeers_new(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_updatePeers_new_waitForHandshake_neverConfigures(t *testing.T) { @@ -226,7 +226,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_neverConfigures(t *testing. defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_updatePeers_new_waitForHandshake_outOfOrder(t *testing.T) { @@ -279,8 +279,8 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_outOfOrder(t *testing.T) { // it should now send the peer to the netmap - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 1) n1 := getNodeWithID(t, nm.Peers, 1) @@ -297,7 +297,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_outOfOrder(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_updatePeers_new_waitForHandshake(t *testing.T) { @@ -350,8 +350,8 @@ func TestConfigMaps_updatePeers_new_waitForHandshake(t *testing.T) { // it should now send the peer to the netmap - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 1) n1 := getNodeWithID(t, nm.Peers, 1) @@ -368,7 +368,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_updatePeers_new_waitForHandshake_timeout(t *testing.T) { @@ -408,8 +408,8 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_timeout(t *testing.T) { // it should now send the peer to the netmap - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 1) n1 := getNodeWithID(t, nm.Peers, 1) @@ -426,7 +426,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_timeout(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_updatePeers_same(t *testing.T) { @@ -485,7 +485,7 @@ func TestConfigMaps_updatePeers_same(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_updatePeers_disconnect(t *testing.T) { @@ -543,8 +543,8 @@ func TestConfigMaps_updatePeers_disconnect(t *testing.T) { assert.False(t, timer.Stop(), "timer was not stopped") // Then, configure engine without the peer. - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 0) require.Len(t, r.wg.Peers, 0) @@ -553,7 +553,7 @@ func TestConfigMaps_updatePeers_disconnect(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_updatePeers_lost(t *testing.T) { @@ -585,11 +585,11 @@ func TestConfigMaps_updatePeers_lost(t *testing.T) { }, } uut.updatePeers(updates) - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 1) require.Len(t, r.wg.Peers, 1) - _ = testutil.RequireRecvCtx(ctx, t, s1) + _ = testutil.TryReceive(ctx, t, s1) mClock.Advance(5 * time.Second).MustWait(ctx) @@ -598,7 +598,7 @@ func TestConfigMaps_updatePeers_lost(t *testing.T) { updates[0].Kind = proto.CoordinateResponse_PeerUpdate_LOST updates[0].Node = nil uut.updatePeers(updates) - _ = testutil.RequireRecvCtx(ctx, t, s2) + _ = testutil.TryReceive(ctx, t, s2) // No reprogramming yet, since we keep the peer around. select { @@ -614,7 +614,7 @@ func TestConfigMaps_updatePeers_lost(t *testing.T) { s3 := expectStatusWithHandshake(ctx, t, fEng, p1Node.Key, lh) // 5 seconds have already elapsed from above mClock.Advance(lostTimeout - 5*time.Second).MustWait(ctx) - _ = testutil.RequireRecvCtx(ctx, t, s3) + _ = testutil.TryReceive(ctx, t, s3) select { case <-fEng.setNetworkMap: t.Fatal("should not reprogram") @@ -627,18 +627,18 @@ func TestConfigMaps_updatePeers_lost(t *testing.T) { s4 := expectStatusWithHandshake(ctx, t, fEng, p1Node.Key, lh) mClock.Advance(time.Minute).MustWait(ctx) - nm = testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r = testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm = testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r = testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 0) require.Len(t, r.wg.Peers, 0) - _ = testutil.RequireRecvCtx(ctx, t, s4) + _ = testutil.TryReceive(ctx, t, s4) done := make(chan struct{}) go func() { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_updatePeers_lost_and_found(t *testing.T) { @@ -670,11 +670,11 @@ func TestConfigMaps_updatePeers_lost_and_found(t *testing.T) { }, } uut.updatePeers(updates) - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 1) require.Len(t, r.wg.Peers, 1) - _ = testutil.RequireRecvCtx(ctx, t, s1) + _ = testutil.TryReceive(ctx, t, s1) mClock.Advance(5 * time.Second).MustWait(ctx) @@ -683,7 +683,7 @@ func TestConfigMaps_updatePeers_lost_and_found(t *testing.T) { updates[0].Kind = proto.CoordinateResponse_PeerUpdate_LOST updates[0].Node = nil uut.updatePeers(updates) - _ = testutil.RequireRecvCtx(ctx, t, s2) + _ = testutil.TryReceive(ctx, t, s2) // No reprogramming yet, since we keep the peer around. select { @@ -699,7 +699,7 @@ func TestConfigMaps_updatePeers_lost_and_found(t *testing.T) { updates[0].Kind = proto.CoordinateResponse_PeerUpdate_NODE updates[0].Node = p1n uut.updatePeers(updates) - _ = testutil.RequireRecvCtx(ctx, t, s3) + _ = testutil.TryReceive(ctx, t, s3) // This does not trigger reprogramming, because we never removed the node select { case <-fEng.setNetworkMap: @@ -723,7 +723,7 @@ func TestConfigMaps_updatePeers_lost_and_found(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_setAllPeersLost(t *testing.T) { @@ -764,11 +764,11 @@ func TestConfigMaps_setAllPeersLost(t *testing.T) { }, } uut.updatePeers(updates) - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 2) require.Len(t, r.wg.Peers, 2) - _ = testutil.RequireRecvCtx(ctx, t, s1) + _ = testutil.TryReceive(ctx, t, s1) mClock.Advance(5 * time.Second).MustWait(ctx) uut.setAllPeersLost() @@ -787,20 +787,20 @@ func TestConfigMaps_setAllPeersLost(t *testing.T) { d, w := mClock.AdvanceNext() w.MustWait(ctx) require.LessOrEqual(t, d, time.Millisecond) - _ = testutil.RequireRecvCtx(ctx, t, s2) + _ = testutil.TryReceive(ctx, t, s2) - nm = testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r = testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm = testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r = testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 1) require.Len(t, r.wg.Peers, 1) // Finally, advance the clock until after the timeout s3 := expectStatusWithHandshake(ctx, t, fEng, p1Node.Key, start) mClock.Advance(lostTimeout - d - 5*time.Second).MustWait(ctx) - _ = testutil.RequireRecvCtx(ctx, t, s3) + _ = testutil.TryReceive(ctx, t, s3) - nm = testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r = testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm = testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r = testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 0) require.Len(t, r.wg.Peers, 0) @@ -809,7 +809,7 @@ func TestConfigMaps_setAllPeersLost(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_setBlockEndpoints_different(t *testing.T) { @@ -842,8 +842,8 @@ func TestConfigMaps_setBlockEndpoints_different(t *testing.T) { uut.setBlockEndpoints(true) - nm := testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - r := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + nm := testutil.TryReceive(ctx, t, fEng.setNetworkMap) + r := testutil.TryReceive(ctx, t, fEng.reconfig) require.Len(t, nm.Peers, 1) require.Len(t, nm.Peers[0].Endpoints, 0) require.Len(t, r.wg.Peers, 1) @@ -853,7 +853,7 @@ func TestConfigMaps_setBlockEndpoints_different(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_setBlockEndpoints_same(t *testing.T) { @@ -896,7 +896,7 @@ func TestConfigMaps_setBlockEndpoints_same(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_setDERPMap_different(t *testing.T) { @@ -923,7 +923,7 @@ func TestConfigMaps_setDERPMap_different(t *testing.T) { } uut.setDERPMap(derpMap) - dm := testutil.RequireRecvCtx(ctx, t, fEng.setDERPMap) + dm := testutil.TryReceive(ctx, t, fEng.setDERPMap) require.Len(t, dm.HomeParams.RegionScore, 1) require.Equal(t, dm.HomeParams.RegionScore[1], 0.025) require.Len(t, dm.Regions, 1) @@ -937,7 +937,7 @@ func TestConfigMaps_setDERPMap_different(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_setDERPMap_same(t *testing.T) { @@ -1006,7 +1006,7 @@ func TestConfigMaps_setDERPMap_same(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestConfigMaps_fillPeerDiagnostics(t *testing.T) { @@ -1066,7 +1066,7 @@ func TestConfigMaps_fillPeerDiagnostics(t *testing.T) { // When: call fillPeerDiagnostics d := PeerDiagnostics{DERPRegionNames: make(map[int]string)} uut.fillPeerDiagnostics(&d, p1ID) - testutil.RequireRecvCtx(ctx, t, s0) + testutil.TryReceive(ctx, t, s0) // Then: require.Equal(t, map[int]string{1: "AUH", 1001: "DXB"}, d.DERPRegionNames) @@ -1078,7 +1078,7 @@ func TestConfigMaps_fillPeerDiagnostics(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func expectStatusWithHandshake( @@ -1152,7 +1152,7 @@ func TestConfigMaps_updatePeers_nonexist(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) }) } } @@ -1187,8 +1187,8 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) { }) // THEN: the engine is reconfigured with those same hosts - _ = testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - req := testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + _ = testutil.TryReceive(ctx, t, fEng.setNetworkMap) + req := testutil.TryReceive(ctx, t, fEng.reconfig) require.Equal(t, req.dnsCfg, &dns.Config{ Routes: map[dnsname.FQDN][]*dnstype.Resolver{ suffix: nil, @@ -1218,8 +1218,8 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) { }) // THEN: The engine is reconfigured with only the new hosts - _ = testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - req = testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + _ = testutil.TryReceive(ctx, t, fEng.setNetworkMap) + req = testutil.TryReceive(ctx, t, fEng.reconfig) require.Equal(t, req.dnsCfg, &dns.Config{ Routes: map[dnsname.FQDN][]*dnstype.Resolver{ suffix: nil, @@ -1237,8 +1237,8 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) { // WHEN: we remove all the hosts uut.setHosts(map[dnsname.FQDN][]netip.Addr{}) - _ = testutil.RequireRecvCtx(ctx, t, fEng.setNetworkMap) - req = testutil.RequireRecvCtx(ctx, t, fEng.reconfig) + _ = testutil.TryReceive(ctx, t, fEng.setNetworkMap) + req = testutil.TryReceive(ctx, t, fEng.reconfig) // THEN: the engine is reconfigured with an empty config require.Equal(t, req.dnsCfg, &dns.Config{}) @@ -1248,7 +1248,7 @@ func TestConfigMaps_addRemoveHosts(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func newTestNode(id int) *Node { @@ -1287,7 +1287,7 @@ func requireNeverConfigures(ctx context.Context, t *testing.T, uut *phased) { } assert.Equal(t, closed, uut.phase) }() - _ = testutil.RequireRecvCtx(ctx, t, waiting) + _ = testutil.TryReceive(ctx, t, waiting) } type reconfigCall struct { diff --git a/tailnet/conn_test.go b/tailnet/conn_test.go index c22d803fe74bc..17f2abe32bd59 100644 --- a/tailnet/conn_test.go +++ b/tailnet/conn_test.go @@ -79,7 +79,7 @@ func TestTailnet(t *testing.T) { conn <- struct{}{} }() - _ = testutil.RequireRecvCtx(ctx, t, listenDone) + _ = testutil.TryReceive(ctx, t, listenDone) nc, err := w2.DialContextTCP(context.Background(), netip.AddrPortFrom(w1IP, 35565)) require.NoError(t, err) _ = nc.Close() @@ -92,7 +92,7 @@ func TestTailnet(t *testing.T) { default: } }) - node := testutil.RequireRecvCtx(ctx, t, nodes) + node := testutil.TryReceive(ctx, t, nodes) // Ensure this connected over raw (not websocket) DERP! require.Len(t, node.DERPForcedWebsocket, 0) @@ -146,11 +146,11 @@ func TestTailnet(t *testing.T) { _ = nc.Close() }() - testutil.RequireRecvCtx(ctx, t, listening) + testutil.TryReceive(ctx, t, listening) nc, err := w2.DialContextTCP(ctx, netip.AddrPortFrom(w1IP, 35565)) require.NoError(t, err) _ = nc.Close() - testutil.RequireRecvCtx(ctx, t, done) + testutil.TryReceive(ctx, t, done) nodes := make(chan *tailnet.Node, 1) w2.SetNodeCallback(func(node *tailnet.Node) { diff --git a/tailnet/controllers_test.go b/tailnet/controllers_test.go index 41b2479c6643c..67834de462655 100644 --- a/tailnet/controllers_test.go +++ b/tailnet/controllers_test.go @@ -61,7 +61,7 @@ func TestInMemoryCoordination(t *testing.T) { coordinationTest(ctx, t, uut, fConn, reqs, resps, agentID) // Recv loop should be terminated by the server hanging up after Disconnect - err := testutil.RequireRecvCtx(ctx, t, uut.Wait()) + err := testutil.TryReceive(ctx, t, uut.Wait()) require.ErrorIs(t, err, io.EOF) } @@ -118,7 +118,7 @@ func TestTunnelSrcCoordController_Mainline(t *testing.T) { coordinationTest(ctx, t, uut, fConn, reqs, resps, agentID) // Recv loop should be terminated by the server hanging up after Disconnect - err = testutil.RequireRecvCtx(ctx, t, uut.Wait()) + err = testutil.TryReceive(ctx, t, uut.Wait()) require.ErrorIs(t, err, io.EOF) } @@ -147,22 +147,22 @@ func TestTunnelSrcCoordController_AddDestination(t *testing.T) { // THEN: Controller sends AddTunnel for the destinations for i := range 2 { b0 := byte(i + 1) - call := testutil.RequireRecvCtx(ctx, t, client1.reqs) + call := testutil.TryReceive(ctx, t, client1.reqs) require.Equal(t, b0, call.req.GetAddTunnel().GetId()[0]) - testutil.RequireSendCtx(ctx, t, call.err, nil) + testutil.RequireSend(ctx, t, call.err, nil) } - _ = testutil.RequireRecvCtx(ctx, t, addDone) + _ = testutil.TryReceive(ctx, t, addDone) // THEN: Controller sets destinations on Coordinatee require.Contains(t, fConn.tunnelDestinations, dest1) require.Contains(t, fConn.tunnelDestinations, dest2) // WHEN: Closed from server side and reconnects - respCall := testutil.RequireRecvCtx(ctx, t, client1.resps) - testutil.RequireSendCtx(ctx, t, respCall.err, io.EOF) - closeCall := testutil.RequireRecvCtx(ctx, t, client1.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) - err := testutil.RequireRecvCtx(ctx, t, cw1.Wait()) + respCall := testutil.TryReceive(ctx, t, client1.resps) + testutil.RequireSend(ctx, t, respCall.err, io.EOF) + closeCall := testutil.TryReceive(ctx, t, client1.close) + testutil.RequireSend(ctx, t, closeCall, nil) + err := testutil.TryReceive(ctx, t, cw1.Wait()) require.ErrorIs(t, err, io.EOF) client2 := newFakeCoordinatorClient(ctx, t) cws := make(chan tailnet.CloserWaiter) @@ -173,21 +173,21 @@ func TestTunnelSrcCoordController_AddDestination(t *testing.T) { // THEN: should immediately send both destinations var dests []byte for range 2 { - call := testutil.RequireRecvCtx(ctx, t, client2.reqs) + call := testutil.TryReceive(ctx, t, client2.reqs) dests = append(dests, call.req.GetAddTunnel().GetId()[0]) - testutil.RequireSendCtx(ctx, t, call.err, nil) + testutil.RequireSend(ctx, t, call.err, nil) } slices.Sort(dests) require.Equal(t, dests, []byte{1, 2}) - cw2 := testutil.RequireRecvCtx(ctx, t, cws) + cw2 := testutil.TryReceive(ctx, t, cws) // close client2 - respCall = testutil.RequireRecvCtx(ctx, t, client2.resps) - testutil.RequireSendCtx(ctx, t, respCall.err, io.EOF) - closeCall = testutil.RequireRecvCtx(ctx, t, client2.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) - err = testutil.RequireRecvCtx(ctx, t, cw2.Wait()) + respCall = testutil.TryReceive(ctx, t, client2.resps) + testutil.RequireSend(ctx, t, respCall.err, io.EOF) + closeCall = testutil.TryReceive(ctx, t, client2.close) + testutil.RequireSend(ctx, t, closeCall, nil) + err = testutil.TryReceive(ctx, t, cw2.Wait()) require.ErrorIs(t, err, io.EOF) } @@ -209,9 +209,9 @@ func TestTunnelSrcCoordController_RemoveDestination(t *testing.T) { go func() { cws <- uut.New(client1) }() - call := testutil.RequireRecvCtx(ctx, t, client1.reqs) - testutil.RequireSendCtx(ctx, t, call.err, nil) - cw1 := testutil.RequireRecvCtx(ctx, t, cws) + call := testutil.TryReceive(ctx, t, client1.reqs) + testutil.RequireSend(ctx, t, call.err, nil) + cw1 := testutil.TryReceive(ctx, t, cws) // WHEN: we remove one destination removeDone := make(chan struct{}) @@ -221,17 +221,17 @@ func TestTunnelSrcCoordController_RemoveDestination(t *testing.T) { }() // THEN: Controller sends RemoveTunnel for the destination - call = testutil.RequireRecvCtx(ctx, t, client1.reqs) + call = testutil.TryReceive(ctx, t, client1.reqs) require.Equal(t, dest1[:], call.req.GetRemoveTunnel().GetId()) - testutil.RequireSendCtx(ctx, t, call.err, nil) - _ = testutil.RequireRecvCtx(ctx, t, removeDone) + testutil.RequireSend(ctx, t, call.err, nil) + _ = testutil.TryReceive(ctx, t, removeDone) // WHEN: Closed from server side and reconnect - respCall := testutil.RequireRecvCtx(ctx, t, client1.resps) - testutil.RequireSendCtx(ctx, t, respCall.err, io.EOF) - closeCall := testutil.RequireRecvCtx(ctx, t, client1.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) - err := testutil.RequireRecvCtx(ctx, t, cw1.Wait()) + respCall := testutil.TryReceive(ctx, t, client1.resps) + testutil.RequireSend(ctx, t, respCall.err, io.EOF) + closeCall := testutil.TryReceive(ctx, t, client1.close) + testutil.RequireSend(ctx, t, closeCall, nil) + err := testutil.TryReceive(ctx, t, cw1.Wait()) require.ErrorIs(t, err, io.EOF) client2 := newFakeCoordinatorClient(ctx, t) @@ -240,14 +240,14 @@ func TestTunnelSrcCoordController_RemoveDestination(t *testing.T) { }() // THEN: should immediately resolve without sending anything - cw2 := testutil.RequireRecvCtx(ctx, t, cws) + cw2 := testutil.TryReceive(ctx, t, cws) // close client2 - respCall = testutil.RequireRecvCtx(ctx, t, client2.resps) - testutil.RequireSendCtx(ctx, t, respCall.err, io.EOF) - closeCall = testutil.RequireRecvCtx(ctx, t, client2.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) - err = testutil.RequireRecvCtx(ctx, t, cw2.Wait()) + respCall = testutil.TryReceive(ctx, t, client2.resps) + testutil.RequireSend(ctx, t, respCall.err, io.EOF) + closeCall = testutil.TryReceive(ctx, t, client2.close) + testutil.RequireSend(ctx, t, closeCall, nil) + err = testutil.TryReceive(ctx, t, cw2.Wait()) require.ErrorIs(t, err, io.EOF) } @@ -274,10 +274,10 @@ func TestTunnelSrcCoordController_RemoveDestination_Error(t *testing.T) { cws <- uut.New(client1) }() for range 3 { - call := testutil.RequireRecvCtx(ctx, t, client1.reqs) - testutil.RequireSendCtx(ctx, t, call.err, nil) + call := testutil.TryReceive(ctx, t, client1.reqs) + testutil.RequireSend(ctx, t, call.err, nil) } - cw1 := testutil.RequireRecvCtx(ctx, t, cws) + cw1 := testutil.TryReceive(ctx, t, cws) // WHEN: we remove all destinations removeDone := make(chan struct{}) @@ -290,22 +290,22 @@ func TestTunnelSrcCoordController_RemoveDestination_Error(t *testing.T) { // WHEN: first RemoveTunnel call fails theErr := xerrors.New("a bad thing happened") - call := testutil.RequireRecvCtx(ctx, t, client1.reqs) + call := testutil.TryReceive(ctx, t, client1.reqs) require.Equal(t, dest1[:], call.req.GetRemoveTunnel().GetId()) - testutil.RequireSendCtx(ctx, t, call.err, theErr) + testutil.RequireSend(ctx, t, call.err, theErr) // THEN: we disconnect and do not send remaining RemoveTunnel messages - closeCall := testutil.RequireRecvCtx(ctx, t, client1.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) - _ = testutil.RequireRecvCtx(ctx, t, removeDone) + closeCall := testutil.TryReceive(ctx, t, client1.close) + testutil.RequireSend(ctx, t, closeCall, nil) + _ = testutil.TryReceive(ctx, t, removeDone) // shut down - respCall := testutil.RequireRecvCtx(ctx, t, client1.resps) - testutil.RequireSendCtx(ctx, t, respCall.err, io.EOF) + respCall := testutil.TryReceive(ctx, t, client1.resps) + testutil.RequireSend(ctx, t, respCall.err, io.EOF) // triggers second close call - closeCall = testutil.RequireRecvCtx(ctx, t, client1.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) - err := testutil.RequireRecvCtx(ctx, t, cw1.Wait()) + closeCall = testutil.TryReceive(ctx, t, client1.close) + testutil.RequireSend(ctx, t, closeCall, nil) + err := testutil.TryReceive(ctx, t, cw1.Wait()) require.ErrorIs(t, err, theErr) } @@ -331,10 +331,10 @@ func TestTunnelSrcCoordController_Sync(t *testing.T) { cws <- uut.New(client1) }() for range 2 { - call := testutil.RequireRecvCtx(ctx, t, client1.reqs) - testutil.RequireSendCtx(ctx, t, call.err, nil) + call := testutil.TryReceive(ctx, t, client1.reqs) + testutil.RequireSend(ctx, t, call.err, nil) } - cw1 := testutil.RequireRecvCtx(ctx, t, cws) + cw1 := testutil.TryReceive(ctx, t, cws) // WHEN: we sync dest2 & dest3 syncDone := make(chan struct{}) @@ -344,23 +344,23 @@ func TestTunnelSrcCoordController_Sync(t *testing.T) { }() // THEN: we get an add for dest3 and remove for dest1 - call := testutil.RequireRecvCtx(ctx, t, client1.reqs) + call := testutil.TryReceive(ctx, t, client1.reqs) require.Equal(t, dest3[:], call.req.GetAddTunnel().GetId()) - testutil.RequireSendCtx(ctx, t, call.err, nil) - call = testutil.RequireRecvCtx(ctx, t, client1.reqs) + testutil.RequireSend(ctx, t, call.err, nil) + call = testutil.TryReceive(ctx, t, client1.reqs) require.Equal(t, dest1[:], call.req.GetRemoveTunnel().GetId()) - testutil.RequireSendCtx(ctx, t, call.err, nil) + testutil.RequireSend(ctx, t, call.err, nil) - testutil.RequireRecvCtx(ctx, t, syncDone) + testutil.TryReceive(ctx, t, syncDone) // dest3 should be added to coordinatee require.Contains(t, fConn.tunnelDestinations, dest3) // shut down - respCall := testutil.RequireRecvCtx(ctx, t, client1.resps) - testutil.RequireSendCtx(ctx, t, respCall.err, io.EOF) - closeCall := testutil.RequireRecvCtx(ctx, t, client1.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) - err := testutil.RequireRecvCtx(ctx, t, cw1.Wait()) + respCall := testutil.TryReceive(ctx, t, client1.resps) + testutil.RequireSend(ctx, t, respCall.err, io.EOF) + closeCall := testutil.TryReceive(ctx, t, client1.close) + testutil.RequireSend(ctx, t, closeCall, nil) + err := testutil.TryReceive(ctx, t, cw1.Wait()) require.ErrorIs(t, err, io.EOF) } @@ -384,24 +384,24 @@ func TestTunnelSrcCoordController_AddDestination_Error(t *testing.T) { uut.AddDestination(dest1) }() theErr := xerrors.New("a bad thing happened") - call := testutil.RequireRecvCtx(ctx, t, client1.reqs) - testutil.RequireSendCtx(ctx, t, call.err, theErr) + call := testutil.TryReceive(ctx, t, client1.reqs) + testutil.RequireSend(ctx, t, call.err, theErr) // THEN: Client is closed and exits - closeCall := testutil.RequireRecvCtx(ctx, t, client1.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) + closeCall := testutil.TryReceive(ctx, t, client1.close) + testutil.RequireSend(ctx, t, closeCall, nil) // close the resps, since the client has closed - resp := testutil.RequireRecvCtx(ctx, t, client1.resps) - testutil.RequireSendCtx(ctx, t, resp.err, net.ErrClosed) + resp := testutil.TryReceive(ctx, t, client1.resps) + testutil.RequireSend(ctx, t, resp.err, net.ErrClosed) // this triggers a second Close() call on the client - closeCall = testutil.RequireRecvCtx(ctx, t, client1.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) + closeCall = testutil.TryReceive(ctx, t, client1.close) + testutil.RequireSend(ctx, t, closeCall, nil) - err := testutil.RequireRecvCtx(ctx, t, cw1.Wait()) + err := testutil.TryReceive(ctx, t, cw1.Wait()) require.ErrorIs(t, err, theErr) - _ = testutil.RequireRecvCtx(ctx, t, addDone) + _ = testutil.TryReceive(ctx, t, addDone) } func TestAgentCoordinationController_SendsReadyForHandshake(t *testing.T) { @@ -457,7 +457,7 @@ func TestAgentCoordinationController_SendsReadyForHandshake(t *testing.T) { require.NoError(t, err) dk, err := key.NewDisco().Public().MarshalText() require.NoError(t, err) - testutil.RequireSendCtx(ctx, t, resps, &proto.CoordinateResponse{ + testutil.RequireSend(ctx, t, resps, &proto.CoordinateResponse{ PeerUpdates: []*proto.CoordinateResponse_PeerUpdate{{ Id: clientID[:], Kind: proto.CoordinateResponse_PeerUpdate_NODE, @@ -469,19 +469,19 @@ func TestAgentCoordinationController_SendsReadyForHandshake(t *testing.T) { }}, }) - rfh := testutil.RequireRecvCtx(ctx, t, reqs) + rfh := testutil.TryReceive(ctx, t, reqs) require.NotNil(t, rfh.ReadyForHandshake) require.Len(t, rfh.ReadyForHandshake, 1) require.Equal(t, clientID[:], rfh.ReadyForHandshake[0].Id) go uut.Close(ctx) - dis := testutil.RequireRecvCtx(ctx, t, reqs) + dis := testutil.TryReceive(ctx, t, reqs) require.NotNil(t, dis) require.NotNil(t, dis.Disconnect) close(resps) // Recv loop should be terminated by the server hanging up after Disconnect - err = testutil.RequireRecvCtx(ctx, t, uut.Wait()) + err = testutil.TryReceive(ctx, t, uut.Wait()) require.ErrorIs(t, err, io.EOF) } @@ -493,14 +493,14 @@ func coordinationTest( agentID uuid.UUID, ) { // It should add the tunnel, since we configured as a client - req := testutil.RequireRecvCtx(ctx, t, reqs) + req := testutil.TryReceive(ctx, t, reqs) require.Equal(t, agentID[:], req.GetAddTunnel().GetId()) // when we call the callback, it should send a node update require.NotNil(t, fConn.callback) fConn.callback(&tailnet.Node{PreferredDERP: 1}) - req = testutil.RequireRecvCtx(ctx, t, reqs) + req = testutil.TryReceive(ctx, t, reqs) require.Equal(t, int32(1), req.GetUpdateSelf().GetNode().GetPreferredDerp()) // When we send a peer update, it should update the coordinatee @@ -519,7 +519,7 @@ func coordinationTest( }, }, } - testutil.RequireSendCtx(ctx, t, resps, &proto.CoordinateResponse{PeerUpdates: updates}) + testutil.RequireSend(ctx, t, resps, &proto.CoordinateResponse{PeerUpdates: updates}) require.Eventually(t, func() bool { fConn.Lock() defer fConn.Unlock() @@ -534,11 +534,11 @@ func coordinationTest( }() // When we close, it should gracefully disconnect - req = testutil.RequireRecvCtx(ctx, t, reqs) + req = testutil.TryReceive(ctx, t, reqs) require.NotNil(t, req.Disconnect) close(resps) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) // It should set all peers lost on the coordinatee @@ -593,12 +593,12 @@ func TestNewBasicDERPController_Mainline(t *testing.T) { c := uut.New(fc) ctx := testutil.Context(t, testutil.WaitShort) expectDM := &tailcfg.DERPMap{} - testutil.RequireSendCtx(ctx, t, fc.ch, expectDM) - gotDM := testutil.RequireRecvCtx(ctx, t, fs) + testutil.RequireSend(ctx, t, fc.ch, expectDM) + gotDM := testutil.TryReceive(ctx, t, fs) require.Equal(t, expectDM, gotDM) err := c.Close(ctx) require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, c.Wait()) + err = testutil.TryReceive(ctx, t, c.Wait()) require.ErrorIs(t, err, io.EOF) // ensure Close is idempotent err = c.Close(ctx) @@ -617,7 +617,7 @@ func TestNewBasicDERPController_RecvErr(t *testing.T) { } c := uut.New(fc) ctx := testutil.Context(t, testutil.WaitShort) - err := testutil.RequireRecvCtx(ctx, t, c.Wait()) + err := testutil.TryReceive(ctx, t, c.Wait()) require.ErrorIs(t, err, expectedErr) // ensure Close is idempotent err = c.Close(ctx) @@ -668,12 +668,12 @@ func TestBasicTelemetryController_Success(t *testing.T) { }) }() - call := testutil.RequireRecvCtx(ctx, t, ft.calls) + call := testutil.TryReceive(ctx, t, ft.calls) require.Len(t, call.req.GetEvents(), 1) require.Equal(t, call.req.GetEvents()[0].GetId(), []byte("test event")) - testutil.RequireSendCtx(ctx, t, call.errCh, nil) - testutil.RequireRecvCtx(ctx, t, sendDone) + testutil.RequireSend(ctx, t, call.errCh, nil) + testutil.TryReceive(ctx, t, sendDone) } func TestBasicTelemetryController_Unimplemented(t *testing.T) { @@ -695,9 +695,9 @@ func TestBasicTelemetryController_Unimplemented(t *testing.T) { uut.SendTelemetryEvent(&proto.TelemetryEvent{}) }() - call := testutil.RequireRecvCtx(ctx, t, ft.calls) - testutil.RequireSendCtx(ctx, t, call.errCh, telemetryError) - testutil.RequireRecvCtx(ctx, t, sendDone) + call := testutil.TryReceive(ctx, t, ft.calls) + testutil.RequireSend(ctx, t, call.errCh, telemetryError) + testutil.TryReceive(ctx, t, sendDone) sendDone = make(chan struct{}) go func() { @@ -706,12 +706,12 @@ func TestBasicTelemetryController_Unimplemented(t *testing.T) { }() // we get another call since it wasn't really the Unimplemented error - call = testutil.RequireRecvCtx(ctx, t, ft.calls) + call = testutil.TryReceive(ctx, t, ft.calls) // for real this time telemetryError = errUnimplemented - testutil.RequireSendCtx(ctx, t, call.errCh, telemetryError) - testutil.RequireRecvCtx(ctx, t, sendDone) + testutil.RequireSend(ctx, t, call.errCh, telemetryError) + testutil.TryReceive(ctx, t, sendDone) // now this returns immediately without a call, because unimplemented error disables calling sendDone = make(chan struct{}) @@ -719,7 +719,7 @@ func TestBasicTelemetryController_Unimplemented(t *testing.T) { defer close(sendDone) uut.SendTelemetryEvent(&proto.TelemetryEvent{}) }() - testutil.RequireRecvCtx(ctx, t, sendDone) + testutil.TryReceive(ctx, t, sendDone) // getting a "new" client resets uut.New(ft) @@ -728,9 +728,9 @@ func TestBasicTelemetryController_Unimplemented(t *testing.T) { defer close(sendDone) uut.SendTelemetryEvent(&proto.TelemetryEvent{}) }() - call = testutil.RequireRecvCtx(ctx, t, ft.calls) - testutil.RequireSendCtx(ctx, t, call.errCh, nil) - testutil.RequireRecvCtx(ctx, t, sendDone) + call = testutil.TryReceive(ctx, t, ft.calls) + testutil.RequireSend(ctx, t, call.errCh, nil) + testutil.TryReceive(ctx, t, sendDone) } func TestBasicTelemetryController_NotRecognised(t *testing.T) { @@ -747,20 +747,20 @@ func TestBasicTelemetryController_NotRecognised(t *testing.T) { uut.SendTelemetryEvent(&proto.TelemetryEvent{}) }() // returning generic protocol error doesn't trigger unknown rpc logic - call := testutil.RequireRecvCtx(ctx, t, ft.calls) - testutil.RequireSendCtx(ctx, t, call.errCh, drpc.ProtocolError.New("Protocol Error")) - testutil.RequireRecvCtx(ctx, t, sendDone) + call := testutil.TryReceive(ctx, t, ft.calls) + testutil.RequireSend(ctx, t, call.errCh, drpc.ProtocolError.New("Protocol Error")) + testutil.TryReceive(ctx, t, sendDone) sendDone = make(chan struct{}) go func() { defer close(sendDone) uut.SendTelemetryEvent(&proto.TelemetryEvent{}) }() - call = testutil.RequireRecvCtx(ctx, t, ft.calls) + call = testutil.TryReceive(ctx, t, ft.calls) // return the expected protocol error this time - testutil.RequireSendCtx(ctx, t, call.errCh, + testutil.RequireSend(ctx, t, call.errCh, drpc.ProtocolError.New("unknown rpc: /coder.tailnet.v2.Tailnet/PostTelemetry")) - testutil.RequireRecvCtx(ctx, t, sendDone) + testutil.TryReceive(ctx, t, sendDone) // now this returns immediately without a call, because unimplemented error disables calling sendDone = make(chan struct{}) @@ -768,7 +768,7 @@ func TestBasicTelemetryController_NotRecognised(t *testing.T) { defer close(sendDone) uut.SendTelemetryEvent(&proto.TelemetryEvent{}) }() - testutil.RequireRecvCtx(ctx, t, sendDone) + testutil.TryReceive(ctx, t, sendDone) } type fakeTelemetryClient struct { @@ -822,8 +822,8 @@ func TestBasicResumeTokenController_Mainline(t *testing.T) { go func() { cwCh <- uut.New(fr) }() - call := testutil.RequireRecvCtx(ctx, t, fr.calls) - testutil.RequireSendCtx(ctx, t, call.resp, &proto.RefreshResumeTokenResponse{ + call := testutil.TryReceive(ctx, t, fr.calls) + testutil.RequireSend(ctx, t, call.resp, &proto.RefreshResumeTokenResponse{ Token: "test token 1", RefreshIn: durationpb.New(100 * time.Second), ExpiresAt: timestamppb.New(mClock.Now().Add(200 * time.Second)), @@ -832,11 +832,11 @@ func TestBasicResumeTokenController_Mainline(t *testing.T) { token, ok := uut.Token() require.True(t, ok) require.Equal(t, "test token 1", token) - cw := testutil.RequireRecvCtx(ctx, t, cwCh) + cw := testutil.TryReceive(ctx, t, cwCh) w := mClock.Advance(100 * time.Second) - call = testutil.RequireRecvCtx(ctx, t, fr.calls) - testutil.RequireSendCtx(ctx, t, call.resp, &proto.RefreshResumeTokenResponse{ + call = testutil.TryReceive(ctx, t, fr.calls) + testutil.RequireSend(ctx, t, call.resp, &proto.RefreshResumeTokenResponse{ Token: "test token 2", RefreshIn: durationpb.New(50 * time.Second), ExpiresAt: timestamppb.New(mClock.Now().Add(200 * time.Second)), @@ -851,7 +851,7 @@ func TestBasicResumeTokenController_Mainline(t *testing.T) { err := cw.Close(ctx) require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, cw.Wait()) + err = testutil.TryReceive(ctx, t, cw.Wait()) require.NoError(t, err) token, ok = uut.Token() @@ -880,24 +880,24 @@ func TestBasicResumeTokenController_NewWhileRefreshing(t *testing.T) { go func() { cwCh1 <- uut.New(fr1) }() - call1 := testutil.RequireRecvCtx(ctx, t, fr1.calls) + call1 := testutil.TryReceive(ctx, t, fr1.calls) fr2 := newFakeResumeTokenClient(ctx) cwCh2 := make(chan tailnet.CloserWaiter, 1) go func() { cwCh2 <- uut.New(fr2) }() - call2 := testutil.RequireRecvCtx(ctx, t, fr2.calls) + call2 := testutil.TryReceive(ctx, t, fr2.calls) - testutil.RequireSendCtx(ctx, t, call2.resp, &proto.RefreshResumeTokenResponse{ + testutil.RequireSend(ctx, t, call2.resp, &proto.RefreshResumeTokenResponse{ Token: "test token 2.0", RefreshIn: durationpb.New(102 * time.Second), ExpiresAt: timestamppb.New(mClock.Now().Add(200 * time.Second)), }) - cw2 := testutil.RequireRecvCtx(ctx, t, cwCh2) // this ensures Close was called on 1 + cw2 := testutil.TryReceive(ctx, t, cwCh2) // this ensures Close was called on 1 - testutil.RequireSendCtx(ctx, t, call1.resp, &proto.RefreshResumeTokenResponse{ + testutil.RequireSend(ctx, t, call1.resp, &proto.RefreshResumeTokenResponse{ Token: "test token 1", RefreshIn: durationpb.New(101 * time.Second), ExpiresAt: timestamppb.New(mClock.Now().Add(200 * time.Second)), @@ -910,13 +910,13 @@ func TestBasicResumeTokenController_NewWhileRefreshing(t *testing.T) { require.Equal(t, "test token 2.0", token) // refresher 1 should already be closed. - cw1 := testutil.RequireRecvCtx(ctx, t, cwCh1) - err := testutil.RequireRecvCtx(ctx, t, cw1.Wait()) + cw1 := testutil.TryReceive(ctx, t, cwCh1) + err := testutil.TryReceive(ctx, t, cw1.Wait()) require.NoError(t, err) w := mClock.Advance(102 * time.Second) - call := testutil.RequireRecvCtx(ctx, t, fr2.calls) - testutil.RequireSendCtx(ctx, t, call.resp, &proto.RefreshResumeTokenResponse{ + call := testutil.TryReceive(ctx, t, fr2.calls) + testutil.RequireSend(ctx, t, call.resp, &proto.RefreshResumeTokenResponse{ Token: "test token 2.1", RefreshIn: durationpb.New(50 * time.Second), ExpiresAt: timestamppb.New(mClock.Now().Add(200 * time.Second)), @@ -931,7 +931,7 @@ func TestBasicResumeTokenController_NewWhileRefreshing(t *testing.T) { err = cw2.Close(ctx) require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, cw2.Wait()) + err = testutil.TryReceive(ctx, t, cw2.Wait()) require.NoError(t, err) } @@ -948,9 +948,9 @@ func TestBasicResumeTokenController_Unimplemented(t *testing.T) { fr := newFakeResumeTokenClient(ctx) cw := uut.New(fr) - call := testutil.RequireRecvCtx(ctx, t, fr.calls) - testutil.RequireSendCtx(ctx, t, call.errCh, errUnimplemented) - err := testutil.RequireRecvCtx(ctx, t, cw.Wait()) + call := testutil.TryReceive(ctx, t, fr.calls) + testutil.RequireSend(ctx, t, call.errCh, errUnimplemented) + err := testutil.TryReceive(ctx, t, cw.Wait()) require.NoError(t, err) _, ok = uut.Token() require.False(t, ok) @@ -1044,35 +1044,35 @@ func TestController_Disconnects(t *testing.T) { uut.DERPCtrl = tailnet.NewBasicDERPController(logger.Named("derp_ctrl"), fConn) uut.Run(ctx) - call := testutil.RequireRecvCtx(testCtx, t, fCoord.CoordinateCalls) + call := testutil.TryReceive(testCtx, t, fCoord.CoordinateCalls) // simulate a problem with DERPMaps by sending nil - testutil.RequireSendCtx(testCtx, t, derpMapCh, nil) + testutil.RequireSend(testCtx, t, derpMapCh, nil) // this should cause the coordinate call to hang up WITHOUT disconnecting - reqNil := testutil.RequireRecvCtx(testCtx, t, call.Reqs) + reqNil := testutil.TryReceive(testCtx, t, call.Reqs) require.Nil(t, reqNil) // and mark all peers lost - _ = testutil.RequireRecvCtx(testCtx, t, peersLost) + _ = testutil.TryReceive(testCtx, t, peersLost) // ...and then reconnect - call = testutil.RequireRecvCtx(testCtx, t, fCoord.CoordinateCalls) + call = testutil.TryReceive(testCtx, t, fCoord.CoordinateCalls) // close the coordination call, which should cause a 2nd reconnection close(call.Resps) - _ = testutil.RequireRecvCtx(testCtx, t, peersLost) - call = testutil.RequireRecvCtx(testCtx, t, fCoord.CoordinateCalls) + _ = testutil.TryReceive(testCtx, t, peersLost) + call = testutil.TryReceive(testCtx, t, fCoord.CoordinateCalls) // canceling the context should trigger the disconnect message cancel() - reqDisc := testutil.RequireRecvCtx(testCtx, t, call.Reqs) + reqDisc := testutil.TryReceive(testCtx, t, call.Reqs) require.NotNil(t, reqDisc) require.NotNil(t, reqDisc.Disconnect) close(call.Resps) - _ = testutil.RequireRecvCtx(testCtx, t, peersLost) - _ = testutil.RequireRecvCtx(testCtx, t, uut.Closed()) + _ = testutil.TryReceive(testCtx, t, peersLost) + _ = testutil.TryReceive(testCtx, t, uut.Closed()) } func TestController_TelemetrySuccess(t *testing.T) { @@ -1124,14 +1124,14 @@ func TestController_TelemetrySuccess(t *testing.T) { uut.Run(ctx) // Coordinate calls happen _after_ telemetry is connected up, so we use this // to ensure telemetry is connected before sending our event - cc := testutil.RequireRecvCtx(ctx, t, fCoord.CoordinateCalls) + cc := testutil.TryReceive(ctx, t, fCoord.CoordinateCalls) defer close(cc.Resps) tel.SendTelemetryEvent(&proto.TelemetryEvent{ Id: []byte("test event"), }) - testEvents := testutil.RequireRecvCtx(ctx, t, eventCh) + testEvents := testutil.TryReceive(ctx, t, eventCh) require.Len(t, testEvents, 1) require.Equal(t, []byte("test event"), testEvents[0].Id) @@ -1157,27 +1157,27 @@ func TestController_WorkspaceUpdates(t *testing.T) { uut.Run(ctx) // it should dial and pass the client to the controller - call := testutil.RequireRecvCtx(testCtx, t, fCtrl.calls) + call := testutil.TryReceive(testCtx, t, fCtrl.calls) require.Equal(t, fClient, call.client) fCW := newFakeCloserWaiter() - testutil.RequireSendCtx[tailnet.CloserWaiter](testCtx, t, call.resp, fCW) + testutil.RequireSend[tailnet.CloserWaiter](testCtx, t, call.resp, fCW) // if the CloserWaiter exits... - testutil.RequireSendCtx(testCtx, t, fCW.errCh, theError) + testutil.RequireSend(testCtx, t, fCW.errCh, theError) // it should close, redial and reconnect - cCall := testutil.RequireRecvCtx(testCtx, t, fClient.close) - testutil.RequireSendCtx(testCtx, t, cCall, nil) + cCall := testutil.TryReceive(testCtx, t, fClient.close) + testutil.RequireSend(testCtx, t, cCall, nil) - call = testutil.RequireRecvCtx(testCtx, t, fCtrl.calls) + call = testutil.TryReceive(testCtx, t, fCtrl.calls) require.Equal(t, fClient, call.client) fCW = newFakeCloserWaiter() - testutil.RequireSendCtx[tailnet.CloserWaiter](testCtx, t, call.resp, fCW) + testutil.RequireSend[tailnet.CloserWaiter](testCtx, t, call.resp, fCW) // canceling the context should close the client cancel() - cCall = testutil.RequireRecvCtx(testCtx, t, fClient.close) - testutil.RequireSendCtx(testCtx, t, cCall, nil) + cCall = testutil.TryReceive(testCtx, t, fClient.close) + testutil.RequireSend(testCtx, t, cCall, nil) } type fakeTailnetConn struct { @@ -1492,12 +1492,12 @@ func setupConnectedAllWorkspaceUpdatesController( coordCW := tsc.New(coordC) t.Cleanup(func() { // hang up coord client - coordRecv := testutil.RequireRecvCtx(ctx, t, coordC.resps) - testutil.RequireSendCtx(ctx, t, coordRecv.err, io.EOF) + coordRecv := testutil.TryReceive(ctx, t, coordC.resps) + testutil.RequireSend(ctx, t, coordRecv.err, io.EOF) // sends close on client - cCall := testutil.RequireRecvCtx(ctx, t, coordC.close) - testutil.RequireSendCtx(ctx, t, cCall, nil) - err := testutil.RequireRecvCtx(ctx, t, coordCW.Wait()) + cCall := testutil.TryReceive(ctx, t, coordC.close) + testutil.RequireSend(ctx, t, cCall, nil) + err := testutil.TryReceive(ctx, t, coordCW.Wait()) require.ErrorIs(t, err, io.EOF) }) @@ -1506,9 +1506,9 @@ func setupConnectedAllWorkspaceUpdatesController( updateCW := uut.New(updateC) t.Cleanup(func() { // hang up WorkspaceUpdates client - upRecvCall := testutil.RequireRecvCtx(ctx, t, updateC.recv) - testutil.RequireSendCtx(ctx, t, upRecvCall.err, io.EOF) - err := testutil.RequireRecvCtx(ctx, t, updateCW.Wait()) + upRecvCall := testutil.TryReceive(ctx, t, updateC.recv) + testutil.RequireSend(ctx, t, upRecvCall.err, io.EOF) + err := testutil.TryReceive(ctx, t, updateCW.Wait()) require.ErrorIs(t, err, io.EOF) }) return coordC, updateC, uut @@ -1544,15 +1544,15 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) { }, } - upRecvCall := testutil.RequireRecvCtx(ctx, t, updateC.recv) - testutil.RequireSendCtx(ctx, t, upRecvCall.resp, initUp) + upRecvCall := testutil.TryReceive(ctx, t, updateC.recv) + testutil.RequireSend(ctx, t, upRecvCall.resp, initUp) // This should trigger AddTunnel for each agent var adds []uuid.UUID for range 3 { - coordCall := testutil.RequireRecvCtx(ctx, t, coordC.reqs) + coordCall := testutil.TryReceive(ctx, t, coordC.reqs) adds = append(adds, uuid.Must(uuid.FromBytes(coordCall.req.GetAddTunnel().GetId()))) - testutil.RequireSendCtx(ctx, t, coordCall.err, nil) + testutil.RequireSend(ctx, t, coordCall.err, nil) } require.Contains(t, adds, w1a1ID) require.Contains(t, adds, w2a1ID) @@ -1576,9 +1576,9 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) { "w1.mctest.": {ws1a1IP}, expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()}, } - dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls) + dnsCall := testutil.TryReceive(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) - testutil.RequireSendCtx(ctx, t, dnsCall.err, nil) + testutil.RequireSend(ctx, t, dnsCall.err, nil) currentState := tailnet.WorkspaceUpdate{ UpsertedWorkspaces: []*tailnet.Workspace{ @@ -1614,7 +1614,7 @@ func TestTunnelAllWorkspaceUpdatesController_Initial(t *testing.T) { } // And the callback - cbUpdate := testutil.RequireRecvCtx(ctx, t, fUH.ch) + cbUpdate := testutil.TryReceive(ctx, t, fUH.ch) require.Equal(t, currentState, cbUpdate) // Current recvState should match @@ -1656,13 +1656,13 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { }, } - upRecvCall := testutil.RequireRecvCtx(ctx, t, updateC.recv) - testutil.RequireSendCtx(ctx, t, upRecvCall.resp, initUp) + upRecvCall := testutil.TryReceive(ctx, t, updateC.recv) + testutil.RequireSend(ctx, t, upRecvCall.resp, initUp) // Add for w1a1 - coordCall := testutil.RequireRecvCtx(ctx, t, coordC.reqs) + coordCall := testutil.TryReceive(ctx, t, coordC.reqs) require.Equal(t, w1a1ID[:], coordCall.req.GetAddTunnel().GetId()) - testutil.RequireSendCtx(ctx, t, coordCall.err, nil) + testutil.RequireSend(ctx, t, coordCall.err, nil) expectedCoderConnectFQDN, err := dnsname.ToFQDN( fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, tailnet.CoderDNSSuffix)) @@ -1675,9 +1675,9 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { "w1.coder.": {ws1a1IP}, expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()}, } - dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls) + dnsCall := testutil.TryReceive(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) - testutil.RequireSendCtx(ctx, t, dnsCall.err, nil) + testutil.RequireSend(ctx, t, dnsCall.err, nil) initRecvUp := tailnet.WorkspaceUpdate{ UpsertedWorkspaces: []*tailnet.Workspace{ @@ -1694,7 +1694,7 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { DeletedAgents: []*tailnet.Agent{}, } - cbUpdate := testutil.RequireRecvCtx(ctx, t, fUH.ch) + cbUpdate := testutil.TryReceive(ctx, t, fUH.ch) require.Equal(t, initRecvUp, cbUpdate) // Current state should match initial @@ -1711,18 +1711,18 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { {Id: w1a1ID[:], WorkspaceId: w1ID[:]}, }, } - upRecvCall = testutil.RequireRecvCtx(ctx, t, updateC.recv) - testutil.RequireSendCtx(ctx, t, upRecvCall.resp, agentUpdate) + upRecvCall = testutil.TryReceive(ctx, t, updateC.recv) + testutil.RequireSend(ctx, t, upRecvCall.resp, agentUpdate) // Add for w1a2 - coordCall = testutil.RequireRecvCtx(ctx, t, coordC.reqs) + coordCall = testutil.TryReceive(ctx, t, coordC.reqs) require.Equal(t, w1a2ID[:], coordCall.req.GetAddTunnel().GetId()) - testutil.RequireSendCtx(ctx, t, coordCall.err, nil) + testutil.RequireSend(ctx, t, coordCall.err, nil) // Remove for w1a1 - coordCall = testutil.RequireRecvCtx(ctx, t, coordC.reqs) + coordCall = testutil.TryReceive(ctx, t, coordC.reqs) require.Equal(t, w1a1ID[:], coordCall.req.GetRemoveTunnel().GetId()) - testutil.RequireSendCtx(ctx, t, coordCall.err, nil) + testutil.RequireSend(ctx, t, coordCall.err, nil) // DNS contains only w1a2 expectedDNS = map[dnsname.FQDN][]netip.Addr{ @@ -1731,11 +1731,11 @@ func TestTunnelAllWorkspaceUpdatesController_DeleteAgent(t *testing.T) { "w1.coder.": {ws1a2IP}, expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()}, } - dnsCall = testutil.RequireRecvCtx(ctx, t, fDNS.calls) + dnsCall = testutil.TryReceive(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) - testutil.RequireSendCtx(ctx, t, dnsCall.err, nil) + testutil.RequireSend(ctx, t, dnsCall.err, nil) - cbUpdate = testutil.RequireRecvCtx(ctx, t, fUH.ch) + cbUpdate = testutil.TryReceive(ctx, t, fUH.ch) sndRecvUpdate := tailnet.WorkspaceUpdate{ UpsertedWorkspaces: []*tailnet.Workspace{}, UpsertedAgents: []*tailnet.Agent{ @@ -1804,8 +1804,8 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) { {Id: w1a1ID[:], Name: "w1a1", WorkspaceId: w1ID[:]}, }, } - upRecvCall := testutil.RequireRecvCtx(ctx, t, updateC.recv) - testutil.RequireSendCtx(ctx, t, upRecvCall.resp, initUp) + upRecvCall := testutil.TryReceive(ctx, t, updateC.recv) + testutil.RequireSend(ctx, t, upRecvCall.resp, initUp) expectedCoderConnectFQDN, err := dnsname.ToFQDN( fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, tailnet.CoderDNSSuffix)) @@ -1818,16 +1818,16 @@ func TestTunnelAllWorkspaceUpdatesController_DNSError(t *testing.T) { "w1.coder.": {ws1a1IP}, expectedCoderConnectFQDN: {tsaddr.CoderServiceIPv6()}, } - dnsCall := testutil.RequireRecvCtx(ctx, t, fDNS.calls) + dnsCall := testutil.TryReceive(ctx, t, fDNS.calls) require.Equal(t, expectedDNS, dnsCall.hosts) - testutil.RequireSendCtx(ctx, t, dnsCall.err, dnsError) + testutil.RequireSend(ctx, t, dnsCall.err, dnsError) // should trigger a close on the client - closeCall := testutil.RequireRecvCtx(ctx, t, updateC.close) - testutil.RequireSendCtx(ctx, t, closeCall, io.EOF) + closeCall := testutil.TryReceive(ctx, t, updateC.close) + testutil.RequireSend(ctx, t, closeCall, io.EOF) // error should be our initial DNS error - err = testutil.RequireRecvCtx(ctx, t, updateCW.Wait()) + err = testutil.TryReceive(ctx, t, updateCW.Wait()) require.ErrorIs(t, err, dnsError) } @@ -1927,12 +1927,12 @@ func TestTunnelAllWorkspaceUpdatesController_HandleErrors(t *testing.T) { updateC := newFakeWorkspaceUpdateClient(ctx, t) updateCW := uut.New(updateC) - recvCall := testutil.RequireRecvCtx(ctx, t, updateC.recv) - testutil.RequireSendCtx(ctx, t, recvCall.resp, tc.update) - closeCall := testutil.RequireRecvCtx(ctx, t, updateC.close) - testutil.RequireSendCtx(ctx, t, closeCall, nil) + recvCall := testutil.TryReceive(ctx, t, updateC.recv) + testutil.RequireSend(ctx, t, recvCall.resp, tc.update) + closeCall := testutil.TryReceive(ctx, t, updateC.close) + testutil.RequireSend(ctx, t, closeCall, nil) - err := testutil.RequireRecvCtx(ctx, t, updateCW.Wait()) + err := testutil.TryReceive(ctx, t, updateCW.Wait()) require.ErrorContains(t, err, tc.errorContains) }) } diff --git a/tailnet/coordinator_test.go b/tailnet/coordinator_test.go index 8bb43c3d0cc89..81a4ddc2182fc 100644 --- a/tailnet/coordinator_test.go +++ b/tailnet/coordinator_test.go @@ -270,8 +270,8 @@ func TestCoordinatorPropogatedPeerContext(t *testing.T) { reqs, _ := c1.Coordinate(peerCtx, peerID, "peer1", auth) - testutil.RequireSendCtx(ctx, t, reqs, &proto.CoordinateRequest{AddTunnel: &proto.CoordinateRequest_Tunnel{Id: tailnet.UUIDToByteSlice(agentID)}}) - _ = testutil.RequireRecvCtx(ctx, t, ch) + testutil.RequireSend(ctx, t, reqs, &proto.CoordinateRequest{AddTunnel: &proto.CoordinateRequest_Tunnel{Id: tailnet.UUIDToByteSlice(agentID)}}) + _ = testutil.TryReceive(ctx, t, ch) // If we don't cancel the context, the coordinator close will wait until the // peer request loop finishes, which will be after the timeout peerCtxCancel() diff --git a/tailnet/node_internal_test.go b/tailnet/node_internal_test.go index 0c04a668090d3..b9257e2ddeab1 100644 --- a/tailnet/node_internal_test.go +++ b/tailnet/node_internal_test.go @@ -42,7 +42,7 @@ func TestNodeUpdater_setNetInfo_different(t *testing.T) { DERPLatency: dl, }) - node := testutil.RequireRecvCtx(ctx, t, nodeCh) + node := testutil.TryReceive(ctx, t, nodeCh) require.Equal(t, nodeKey, node.Key) require.Equal(t, discoKey, node.DiscoKey) require.Equal(t, 1, node.PreferredDERP) @@ -56,7 +56,7 @@ func TestNodeUpdater_setNetInfo_different(t *testing.T) { }) close(goCh) // allows callback to complete - node = testutil.RequireRecvCtx(ctx, t, nodeCh) + node = testutil.TryReceive(ctx, t, nodeCh) require.Equal(t, nodeKey, node.Key) require.Equal(t, discoKey, node.DiscoKey) require.Equal(t, 2, node.PreferredDERP) @@ -67,7 +67,7 @@ func TestNodeUpdater_setNetInfo_different(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setNetInfo_same(t *testing.T) { @@ -108,7 +108,7 @@ func TestNodeUpdater_setNetInfo_same(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setDERPForcedWebsocket_different(t *testing.T) { @@ -137,7 +137,7 @@ func TestNodeUpdater_setDERPForcedWebsocket_different(t *testing.T) { uut.setDERPForcedWebsocket(1, "test") // Then: we receive an update with the reason set - node := testutil.RequireRecvCtx(ctx, t, nodeCh) + node := testutil.TryReceive(ctx, t, nodeCh) require.Equal(t, nodeKey, node.Key) require.Equal(t, discoKey, node.DiscoKey) require.True(t, maps.Equal(map[int]string{1: "test"}, node.DERPForcedWebsocket)) @@ -147,7 +147,7 @@ func TestNodeUpdater_setDERPForcedWebsocket_different(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setDERPForcedWebsocket_same(t *testing.T) { @@ -185,7 +185,7 @@ func TestNodeUpdater_setDERPForcedWebsocket_same(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setStatus_different(t *testing.T) { @@ -220,7 +220,7 @@ func TestNodeUpdater_setStatus_different(t *testing.T) { }, nil) // Then: we receive an update with the endpoint - node := testutil.RequireRecvCtx(ctx, t, nodeCh) + node := testutil.TryReceive(ctx, t, nodeCh) require.Equal(t, nodeKey, node.Key) require.Equal(t, discoKey, node.DiscoKey) require.Equal(t, []string{"[fe80::1]:5678"}, node.Endpoints) @@ -235,7 +235,7 @@ func TestNodeUpdater_setStatus_different(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setStatus_same(t *testing.T) { @@ -275,7 +275,7 @@ func TestNodeUpdater_setStatus_same(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setStatus_error(t *testing.T) { @@ -313,7 +313,7 @@ func TestNodeUpdater_setStatus_error(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setStatus_outdated(t *testing.T) { @@ -355,7 +355,7 @@ func TestNodeUpdater_setStatus_outdated(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setAddresses_different(t *testing.T) { @@ -385,7 +385,7 @@ func TestNodeUpdater_setAddresses_different(t *testing.T) { uut.setAddresses(addrs) // Then: we receive an update with the addresses - node := testutil.RequireRecvCtx(ctx, t, nodeCh) + node := testutil.TryReceive(ctx, t, nodeCh) require.Equal(t, nodeKey, node.Key) require.Equal(t, discoKey, node.DiscoKey) require.Equal(t, addrs, node.Addresses) @@ -396,7 +396,7 @@ func TestNodeUpdater_setAddresses_different(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setAddresses_same(t *testing.T) { @@ -435,7 +435,7 @@ func TestNodeUpdater_setAddresses_same(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setCallback(t *testing.T) { @@ -466,7 +466,7 @@ func TestNodeUpdater_setCallback(t *testing.T) { }) // Then: we get a node update - node := testutil.RequireRecvCtx(ctx, t, nodeCh) + node := testutil.TryReceive(ctx, t, nodeCh) require.Equal(t, nodeKey, node.Key) require.Equal(t, discoKey, node.DiscoKey) require.Equal(t, 1, node.PreferredDERP) @@ -476,7 +476,7 @@ func TestNodeUpdater_setCallback(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setBlockEndpoints_different(t *testing.T) { @@ -506,7 +506,7 @@ func TestNodeUpdater_setBlockEndpoints_different(t *testing.T) { uut.setBlockEndpoints(true) // Then: we receive an update without endpoints - node := testutil.RequireRecvCtx(ctx, t, nodeCh) + node := testutil.TryReceive(ctx, t, nodeCh) require.Equal(t, nodeKey, node.Key) require.Equal(t, discoKey, node.DiscoKey) require.Len(t, node.Endpoints, 0) @@ -515,7 +515,7 @@ func TestNodeUpdater_setBlockEndpoints_different(t *testing.T) { uut.setBlockEndpoints(false) // Then: we receive an update with endpoints - node = testutil.RequireRecvCtx(ctx, t, nodeCh) + node = testutil.TryReceive(ctx, t, nodeCh) require.Equal(t, nodeKey, node.Key) require.Equal(t, discoKey, node.DiscoKey) require.Len(t, node.Endpoints, 1) @@ -525,7 +525,7 @@ func TestNodeUpdater_setBlockEndpoints_different(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_setBlockEndpoints_same(t *testing.T) { @@ -563,7 +563,7 @@ func TestNodeUpdater_setBlockEndpoints_same(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_fillPeerDiagnostics(t *testing.T) { @@ -611,7 +611,7 @@ func TestNodeUpdater_fillPeerDiagnostics(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } func TestNodeUpdater_fillPeerDiagnostics_noCallback(t *testing.T) { @@ -651,5 +651,5 @@ func TestNodeUpdater_fillPeerDiagnostics_noCallback(t *testing.T) { defer close(done) uut.close() }() - _ = testutil.RequireRecvCtx(ctx, t, done) + _ = testutil.TryReceive(ctx, t, done) } diff --git a/tailnet/service_test.go b/tailnet/service_test.go index 096f7dce2a4bf..0c268b05edb50 100644 --- a/tailnet/service_test.go +++ b/tailnet/service_test.go @@ -76,14 +76,14 @@ func TestClientService_ServeClient_V2(t *testing.T) { }) require.NoError(t, err) - call := testutil.RequireRecvCtx(ctx, t, fCoord.CoordinateCalls) + call := testutil.TryReceive(ctx, t, fCoord.CoordinateCalls) require.NotNil(t, call) require.Equal(t, call.ID, clientID) require.Equal(t, call.Name, "client") require.NoError(t, call.Auth.Authorize(ctx, &proto.CoordinateRequest{ AddTunnel: &proto.CoordinateRequest_Tunnel{Id: agentID[:]}, })) - req := testutil.RequireRecvCtx(ctx, t, call.Reqs) + req := testutil.TryReceive(ctx, t, call.Reqs) require.Equal(t, int32(11), req.GetUpdateSelf().GetNode().GetPreferredDerp()) call.Resps <- &proto.CoordinateResponse{PeerUpdates: []*proto.CoordinateResponse_PeerUpdate{ @@ -126,7 +126,7 @@ func TestClientService_ServeClient_V2(t *testing.T) { res, err := client.PostTelemetry(ctx, telemetryReq) require.NoError(t, err) require.NotNil(t, res) - gotEvents := testutil.RequireRecvCtx(ctx, t, telemetryEvents) + gotEvents := testutil.TryReceive(ctx, t, telemetryEvents) require.Len(t, gotEvents, 2) require.Equal(t, "hi", string(gotEvents[0].Id)) require.Equal(t, "bye", string(gotEvents[1].Id)) @@ -134,7 +134,7 @@ func TestClientService_ServeClient_V2(t *testing.T) { // RPCs closed; we need to close the Conn to end the session. err = c.Close() require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.True(t, xerrors.Is(err, io.EOF) || xerrors.Is(err, io.ErrClosedPipe)) } @@ -174,7 +174,7 @@ func TestClientService_ServeClient_V1(t *testing.T) { errCh <- err }() - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.ErrorIs(t, err, tailnet.ErrUnsupportedVersion) } @@ -201,7 +201,7 @@ func TestNetworkTelemetryBatcher(t *testing.T) { // Should overflow and send a batch. ctx := testutil.Context(t, testutil.WaitShort) - batch := testutil.RequireRecvCtx(ctx, t, events) + batch := testutil.TryReceive(ctx, t, events) require.Len(t, batch, 3) require.Equal(t, "1", string(batch[0].Id)) require.Equal(t, "2", string(batch[1].Id)) @@ -209,7 +209,7 @@ func TestNetworkTelemetryBatcher(t *testing.T) { // Should send any pending events when the ticker fires. mClock.Advance(time.Millisecond) - batch = testutil.RequireRecvCtx(ctx, t, events) + batch = testutil.TryReceive(ctx, t, events) require.Len(t, batch, 1) require.Equal(t, "4", string(batch[0].Id)) @@ -220,7 +220,7 @@ func TestNetworkTelemetryBatcher(t *testing.T) { }) err := b.Close() require.NoError(t, err) - batch = testutil.RequireRecvCtx(ctx, t, events) + batch = testutil.TryReceive(ctx, t, events) require.Len(t, batch, 2) require.Equal(t, "5", string(batch[0].Id)) require.Equal(t, "6", string(batch[1].Id)) @@ -250,11 +250,11 @@ func TestClientUserCoordinateeAuth(t *testing.T) { }) require.NoError(t, err) - call := testutil.RequireRecvCtx(ctx, t, fCoord.CoordinateCalls) + call := testutil.TryReceive(ctx, t, fCoord.CoordinateCalls) require.NotNil(t, call) require.Equal(t, call.ID, clientID) require.Equal(t, call.Name, "client") - req := testutil.RequireRecvCtx(ctx, t, call.Reqs) + req := testutil.TryReceive(ctx, t, call.Reqs) require.Equal(t, int32(11), req.GetUpdateSelf().GetNode().GetPreferredDerp()) // Authorize uses `ClientUserCoordinateeAuth` @@ -354,7 +354,7 @@ func createUpdateService(t *testing.T, ctx context.Context, clientID uuid.UUID, t.Cleanup(func() { err = c.Close() require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.True(t, xerrors.Is(err, io.EOF) || xerrors.Is(err, io.ErrClosedPipe)) }) return fCoord, client diff --git a/testutil/chan.go b/testutil/chan.go new file mode 100644 index 0000000000000..a6766a1a49053 --- /dev/null +++ b/testutil/chan.go @@ -0,0 +1,57 @@ +package testutil + +import ( + "context" + "testing" +) + +// TryReceive will attempt to receive a value from the chan and return it. If +// the context expires before a value can be received, it will fail the test. If +// the channel is closed, the zero value of the channel type will be returned. +// +// Safety: Must only be called from the Go routine that created `t`. +func TryReceive[A any](ctx context.Context, t testing.TB, c <-chan A) A { + t.Helper() + select { + case <-ctx.Done(): + t.Fatal("timeout") + var a A + return a + case a := <-c: + return a + } +} + +// RequireReceive will receive a value from the chan and return it. If the +// context expires or the channel is closed before a value can be received, +// it will fail the test. +// +// Safety: Must only be called from the Go routine that created `t`. +func RequireReceive[A any](ctx context.Context, t testing.TB, c <-chan A) A { + t.Helper() + select { + case <-ctx.Done(): + t.Fatal("timeout") + var a A + return a + case a, ok := <-c: + if !ok { + t.Fatal("channel closed") + } + return a + } +} + +// RequireSend will send the given value over the chan and then return. If +// the context expires before the send succeeds, it will fail the test. +// +// Safety: Must only be called from the Go routine that created `t`. +func RequireSend[A any](ctx context.Context, t testing.TB, c chan<- A, a A) { + t.Helper() + select { + case <-ctx.Done(): + t.Fatal("timeout") + case c <- a: + // OK! + } +} diff --git a/testutil/ctx.go b/testutil/ctx.go index b1179dfdf554a..e23c48da85722 100644 --- a/testutil/ctx.go +++ b/testutil/ctx.go @@ -11,37 +11,3 @@ func Context(t *testing.T, dur time.Duration) context.Context { t.Cleanup(cancel) return ctx } - -func RequireRecvCtx[A any](ctx context.Context, t testing.TB, c <-chan A) (a A) { - t.Helper() - select { - case <-ctx.Done(): - t.Fatal("timeout") - return a - case a = <-c: - return a - } -} - -// NOTE: no AssertRecvCtx because it'd be bad if we returned a default value on -// the cases it times out. - -func RequireSendCtx[A any](ctx context.Context, t testing.TB, c chan<- A, a A) { - t.Helper() - select { - case <-ctx.Done(): - t.Fatal("timeout") - case c <- a: - // OK! - } -} - -func AssertSendCtx[A any](ctx context.Context, t testing.TB, c chan<- A, a A) { - t.Helper() - select { - case <-ctx.Done(): - t.Error("timeout") - case c <- a: - // OK! - } -} diff --git a/vpn/client_test.go b/vpn/client_test.go index 41602d1ffa79f..4b05bf108e8e4 100644 --- a/vpn/client_test.go +++ b/vpn/client_test.go @@ -143,11 +143,11 @@ func TestClient_WorkspaceUpdates(t *testing.T) { connErrCh <- err connCh <- conn }() - testutil.RequireRecvCtx(ctx, t, user) - testutil.RequireRecvCtx(ctx, t, connInfo) - err = testutil.RequireRecvCtx(ctx, t, connErrCh) + testutil.TryReceive(ctx, t, user) + testutil.TryReceive(ctx, t, connInfo) + err = testutil.TryReceive(ctx, t, connErrCh) require.NoError(t, err) - conn := testutil.RequireRecvCtx(ctx, t, connCh) + conn := testutil.TryReceive(ctx, t, connCh) // Send a workspace update update := &proto.WorkspaceUpdate{ @@ -165,10 +165,10 @@ func TestClient_WorkspaceUpdates(t *testing.T) { }, }, } - testutil.RequireSendCtx(ctx, t, outUpdateCh, update) + testutil.RequireSend(ctx, t, outUpdateCh, update) // It'll be received by the update handler - recvUpdate := testutil.RequireRecvCtx(ctx, t, inUpdateCh) + recvUpdate := testutil.TryReceive(ctx, t, inUpdateCh) require.Len(t, recvUpdate.UpsertedWorkspaces, 1) require.Equal(t, wsID, recvUpdate.UpsertedWorkspaces[0].ID) require.Len(t, recvUpdate.UpsertedAgents, 1) @@ -202,7 +202,7 @@ func TestClient_WorkspaceUpdates(t *testing.T) { // Close the conn conn.Close() - err = testutil.RequireRecvCtx(ctx, t, serveErrCh) + err = testutil.TryReceive(ctx, t, serveErrCh) require.NoError(t, err) }) } diff --git a/vpn/speaker_internal_test.go b/vpn/speaker_internal_test.go index 789a92217d029..2f3d131093382 100644 --- a/vpn/speaker_internal_test.go +++ b/vpn/speaker_internal_test.go @@ -58,12 +58,12 @@ func TestSpeaker_RawPeer(t *testing.T) { _, err = mp.Write([]byte("codervpn manager 1.3,2.1\n")) require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) tun.start() // send a message and verify it follows protocol for encoding - testutil.RequireSendCtx(ctx, t, tun.sendCh, &TunnelMessage{ + testutil.RequireSend(ctx, t, tun.sendCh, &TunnelMessage{ Msg: &TunnelMessage_Start{ Start: &StartResponse{}, }, @@ -107,7 +107,7 @@ func TestSpeaker_HandshakeRWFailure(t *testing.T) { tun = s errCh <- err }() - err := testutil.RequireRecvCtx(ctx, t, errCh) + err := testutil.TryReceive(ctx, t, errCh) require.ErrorContains(t, err, "handshake failed") require.Nil(t, tun) } @@ -131,7 +131,7 @@ func TestSpeaker_HandshakeCtxDone(t *testing.T) { errCh <- err }() cancel() - err := testutil.RequireRecvCtx(testCtx, t, errCh) + err := testutil.TryReceive(testCtx, t, errCh) require.ErrorContains(t, err, "handshake failed") require.Nil(t, tun) } @@ -168,7 +168,7 @@ func TestSpeaker_OversizeHandshake(t *testing.T) { _, err = mp.Write([]byte(badHandshake)) require.Error(t, err) // other side closes when we write too much - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.ErrorContains(t, err, "handshake failed") require.Nil(t, tun) } @@ -216,7 +216,7 @@ func TestSpeaker_HandshakeInvalid(t *testing.T) { require.NoError(t, err) require.Equal(t, expectedHandshake, string(b[:n])) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.ErrorContains(t, err, "validate header") require.Nil(t, tun) }) @@ -258,7 +258,7 @@ func TestSpeaker_CorruptMessage(t *testing.T) { _, err = mp.Write([]byte("codervpn manager 1.0\n")) require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) tun.start() @@ -290,7 +290,7 @@ func TestSpeaker_unaryRPC_mainline(t *testing.T) { resp = r errCh <- err }() - req := testutil.RequireRecvCtx(ctx, t, tun.requests) + req := testutil.TryReceive(ctx, t, tun.requests) require.NotEqualValues(t, 0, req.msg.GetRpc().GetMsgId()) require.Equal(t, "https://coder.example.com", req.msg.GetStart().GetCoderUrl()) err := req.sendReply(&TunnelMessage{ @@ -299,7 +299,7 @@ func TestSpeaker_unaryRPC_mainline(t *testing.T) { }, }) require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) require.True(t, ok) @@ -334,12 +334,12 @@ func TestSpeaker_unaryRPC_canceled(t *testing.T) { resp = r errCh <- err }() - req := testutil.RequireRecvCtx(testCtx, t, tun.requests) + req := testutil.TryReceive(testCtx, t, tun.requests) require.NotEqualValues(t, 0, req.msg.GetRpc().GetMsgId()) require.Equal(t, "https://coder.example.com", req.msg.GetStart().GetCoderUrl()) cancel() - err := testutil.RequireRecvCtx(testCtx, t, errCh) + err := testutil.TryReceive(testCtx, t, errCh) require.ErrorIs(t, err, context.Canceled) require.Nil(t, resp) @@ -370,7 +370,7 @@ func TestSpeaker_unaryRPC_hung_up(t *testing.T) { resp = r errCh <- err }() - req := testutil.RequireRecvCtx(testCtx, t, tun.requests) + req := testutil.TryReceive(testCtx, t, tun.requests) require.NotEqualValues(t, 0, req.msg.GetRpc().GetMsgId()) require.Equal(t, "https://coder.example.com", req.msg.GetStart().GetCoderUrl()) @@ -378,7 +378,7 @@ func TestSpeaker_unaryRPC_hung_up(t *testing.T) { err := tun.Close() require.NoError(t, err) // Then: we should get an error on the RPC. - err = testutil.RequireRecvCtx(testCtx, t, errCh) + err = testutil.TryReceive(testCtx, t, errCh) require.ErrorIs(t, err, io.ErrUnexpectedEOF) require.Nil(t, resp) } @@ -397,7 +397,7 @@ func TestSpeaker_unaryRPC_sendLoop(t *testing.T) { // When: serdes sendloop is closed // Send a message from the manager. This closes the manager serdes sendloop, since it will error // when writing the message to the (closed) pipe. - testutil.RequireSendCtx(ctx, t, mgr.sendCh, &ManagerMessage{ + testutil.RequireSend(ctx, t, mgr.sendCh, &ManagerMessage{ Msg: &ManagerMessage_GetPeerUpdate{}, }) @@ -417,7 +417,7 @@ func TestSpeaker_unaryRPC_sendLoop(t *testing.T) { }() // Then: we should get an error on the RPC. - err = testutil.RequireRecvCtx(testCtx, t, errCh) + err = testutil.TryReceive(testCtx, t, errCh) require.ErrorIs(t, err, io.ErrUnexpectedEOF) require.Nil(t, resp) } @@ -448,9 +448,9 @@ func setupSpeakers(t *testing.T) ( mgr = s errCh <- err }() - err := testutil.RequireRecvCtx(ctx, t, errCh) + err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) tun.start() mgr.start() diff --git a/vpn/tunnel_internal_test.go b/vpn/tunnel_internal_test.go index 3689bd37ac6f6..d1d7377361f79 100644 --- a/vpn/tunnel_internal_test.go +++ b/vpn/tunnel_internal_test.go @@ -114,9 +114,9 @@ func TestTunnel_StartStop(t *testing.T) { errCh <- err }() // Then: `NewConn` is called, - testutil.RequireSendCtx(ctx, t, client.ch, conn) + testutil.RequireSend(ctx, t, client.ch, conn) // And: a response is received - err := testutil.RequireRecvCtx(ctx, t, errCh) + err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) require.True(t, ok) @@ -130,9 +130,9 @@ func TestTunnel_StartStop(t *testing.T) { errCh <- err }() // Then: `Close` is called on the connection - testutil.RequireRecvCtx(ctx, t, conn.closed) + testutil.TryReceive(ctx, t, conn.closed) // And: a Stop response is received - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok = resp.Msg.(*TunnelMessage_Stop) require.True(t, ok) @@ -178,8 +178,8 @@ func TestTunnel_PeerUpdate(t *testing.T) { resp = r errCh <- err }() - testutil.RequireSendCtx(ctx, t, client.ch, conn) - err := testutil.RequireRecvCtx(ctx, t, errCh) + testutil.RequireSend(ctx, t, client.ch, conn) + err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) require.True(t, ok) @@ -194,7 +194,7 @@ func TestTunnel_PeerUpdate(t *testing.T) { }) require.NoError(t, err) // Then: the tunnel sends a PeerUpdate message - req := testutil.RequireRecvCtx(ctx, t, mgr.requests) + req := testutil.TryReceive(ctx, t, mgr.requests) require.Nil(t, req.msg.Rpc) require.NotNil(t, req.msg.GetPeerUpdate()) require.Len(t, req.msg.GetPeerUpdate().UpsertedWorkspaces, 1) @@ -209,7 +209,7 @@ func TestTunnel_PeerUpdate(t *testing.T) { errCh <- err }() // Then: a PeerUpdate message is sent using the Conn's state - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok = resp.Msg.(*TunnelMessage_PeerUpdate) require.True(t, ok) @@ -243,8 +243,8 @@ func TestTunnel_NetworkSettings(t *testing.T) { resp = r errCh <- err }() - testutil.RequireSendCtx(ctx, t, client.ch, conn) - err := testutil.RequireRecvCtx(ctx, t, errCh) + testutil.RequireSend(ctx, t, client.ch, conn) + err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) require.True(t, ok) @@ -257,11 +257,11 @@ func TestTunnel_NetworkSettings(t *testing.T) { errCh <- err }() // Then: the tunnel sends a NetworkSettings message - req := testutil.RequireRecvCtx(ctx, t, mgr.requests) + req := testutil.TryReceive(ctx, t, mgr.requests) require.NotNil(t, req.msg.Rpc) require.Equal(t, uint32(1200), req.msg.GetNetworkSettings().Mtu) go func() { - testutil.RequireSendCtx(ctx, t, mgr.sendCh, &ManagerMessage{ + testutil.RequireSend(ctx, t, mgr.sendCh, &ManagerMessage{ Rpc: &RPC{ResponseTo: req.msg.Rpc.MsgId}, Msg: &ManagerMessage_NetworkSettings{ NetworkSettings: &NetworkSettingsResponse{ @@ -271,7 +271,7 @@ func TestTunnel_NetworkSettings(t *testing.T) { }) }() // And: `ApplyNetworkSettings` returns without error once the manager responds - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) } @@ -383,8 +383,8 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { resp = r errCh <- err }() - testutil.RequireSendCtx(ctx, t, client.ch, conn) - err := testutil.RequireRecvCtx(ctx, t, errCh) + testutil.RequireSend(ctx, t, client.ch, conn) + err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) _, ok := resp.Msg.(*TunnelMessage_Start) require.True(t, ok) @@ -408,7 +408,7 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { }, }) require.NoError(t, err) - req := testutil.RequireRecvCtx(ctx, t, mgr.requests) + req := testutil.TryReceive(ctx, t, mgr.requests) require.Nil(t, req.msg.Rpc) require.NotNil(t, req.msg.GetPeerUpdate()) require.Len(t, req.msg.GetPeerUpdate().UpsertedAgents, 1) @@ -420,7 +420,7 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { mClock.AdvanceNext() // Then: the tunnel sends a PeerUpdate message of agent upserts, // with the last handshake and latency set - req = testutil.RequireRecvCtx(ctx, t, mgr.requests) + req = testutil.TryReceive(ctx, t, mgr.requests) require.Nil(t, req.msg.Rpc) require.NotNil(t, req.msg.GetPeerUpdate()) require.Len(t, req.msg.GetPeerUpdate().UpsertedAgents, 1) @@ -443,11 +443,11 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { }, }) require.NoError(t, err) - testutil.RequireRecvCtx(ctx, t, mgr.requests) + testutil.TryReceive(ctx, t, mgr.requests) // The new update includes the new agent mClock.AdvanceNext() - req = testutil.RequireRecvCtx(ctx, t, mgr.requests) + req = testutil.TryReceive(ctx, t, mgr.requests) require.Nil(t, req.msg.Rpc) require.NotNil(t, req.msg.GetPeerUpdate()) require.Len(t, req.msg.GetPeerUpdate().UpsertedAgents, 2) @@ -474,11 +474,11 @@ func TestTunnel_sendAgentUpdate(t *testing.T) { }, }) require.NoError(t, err) - testutil.RequireRecvCtx(ctx, t, mgr.requests) + testutil.TryReceive(ctx, t, mgr.requests) // The new update doesn't include the deleted agent mClock.AdvanceNext() - req = testutil.RequireRecvCtx(ctx, t, mgr.requests) + req = testutil.TryReceive(ctx, t, mgr.requests) require.Nil(t, req.msg.Rpc) require.NotNil(t, req.msg.GetPeerUpdate()) require.Len(t, req.msg.GetPeerUpdate().UpsertedAgents, 1) @@ -506,9 +506,9 @@ func setupTunnel(t *testing.T, ctx context.Context, client *fakeClient, mClock q mgr = manager errCh <- err }() - err := testutil.RequireRecvCtx(ctx, t, errCh) + err := testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) - err = testutil.RequireRecvCtx(ctx, t, errCh) + err = testutil.TryReceive(ctx, t, errCh) require.NoError(t, err) mgr.start() return tun, mgr From 3d787da83bd2db9f78e9233606330301265f3059 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 16 Apr 2025 17:49:18 +0100 Subject: [PATCH 113/384] feat: setup connection to dynamic parameters websocket (#17393) resolves coder/preview#57 --- site/src/api/api.ts | 26 +++++++++ .../DynamicParameter/DynamicParameter.tsx | 23 ++++---- .../CreateWorkspacePageExperimental.tsx | 58 +++++++++++++++++-- .../CreateWorkspacePageViewExperimental.tsx | 18 ++---- 4 files changed, 94 insertions(+), 31 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 70d54e5ea0fee..f7e0cd0889f70 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1009,6 +1009,32 @@ class ApiMethods { return response.data; }; + templateVersionDynamicParameters = ( + versionId: string, + { + onMessage, + onError, + }: { + onMessage: (response: TypesGen.DynamicParametersResponse) => void; + onError: (error: Error) => void; + }, + ): WebSocket => { + const socket = createWebSocket( + `/api/v2/templateversions/${versionId}/dynamic-parameters`, + ); + + socket.addEventListener("message", (event) => + onMessage(JSON.parse(event.data) as TypesGen.DynamicParametersResponse), + ); + + socket.addEventListener("error", () => { + onError(new Error("Connection for dynamic parameters failed.")); + socket.close(); + }); + + return socket; + }; + /** * @param organization Can be the organization's ID or name */ diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index d3f2cbbd69fa6..939316625f3db 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -1,4 +1,5 @@ import type { + NullHCLString, PreviewParameter, PreviewParameterOption, WorkspaceBuildParameter, @@ -156,10 +157,8 @@ const ParameterField: FC = ({ disabled, id, }) => { - const value = parameter.value.valid ? parameter.value.value : ""; - const defaultValue = parameter.default_value.valid - ? parameter.default_value.value - : ""; + const value = validValue(parameter.value); + const defaultValue = validValue(parameter.default_value); switch (parameter.form_type) { case "dropdown": @@ -376,9 +375,7 @@ export const getInitialParameterValues = ( if (parameter.ephemeral) { return { name: parameter.name, - value: parameter.default_value.valid - ? parameter.default_value.value - : "", + value: validValue(parameter.default_value), }; } @@ -390,15 +387,19 @@ export const getInitialParameterValues = ( name: parameter.name, value: autofillParam && - isValidValue(parameter, autofillParam) && + isValidParameterOption(parameter, autofillParam) && autofillParam.value ? autofillParam.value - : "", + : validValue(parameter.default_value), }; }); }; -const isValidValue = ( +const validValue = (value: NullHCLString) => { + return value.valid ? value.value : ""; +}; + +const isValidParameterOption = ( previewParam: PreviewParameter, buildParam: WorkspaceBuildParameter, ) => { @@ -409,7 +410,7 @@ const isValidValue = ( return validValues.includes(buildParam.value); } - return true; + return false; }; export const useValidationSchemaForDynamicParameters = ( diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx index 14f34a2e29f0b..27d76a23a83cd 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx @@ -9,7 +9,6 @@ import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces"; import type { DynamicParametersRequest, DynamicParametersResponse, - Template, Workspace, } from "api/typesGenerated"; import { Loader } from "components/Loader/Loader"; @@ -32,6 +31,7 @@ import type { AutofillBuildParameter } from "utils/richParameters"; import { CreateWorkspacePageViewExperimental } from "./CreateWorkspacePageViewExperimental"; export const createWorkspaceModes = ["form", "auto", "duplicate"] as const; export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number]; +import { API } from "api/api"; import { type CreateWorkspacePermissions, createWorkspaceChecks, @@ -47,8 +47,9 @@ const CreateWorkspacePageExperimental: FC = () => { const [currentResponse, setCurrentResponse] = useState(null); - const [wsResponseId, setWSResponseId] = useState(0); - const sendMessage = (message: DynamicParametersRequest) => {}; + const [wsResponseId, setWSResponseId] = useState(-1); + const ws = useRef(null); + const [wsError, setWsError] = useState(null); const customVersionId = searchParams.get("version") ?? undefined; const defaultName = searchParams.get("name"); @@ -80,6 +81,49 @@ const CreateWorkspacePageExperimental: FC = () => { const realizedVersionId = customVersionId ?? templateQuery.data?.active_version_id; + const onMessage = useCallback((response: DynamicParametersResponse) => { + setCurrentResponse((prev) => { + if (prev?.id === response.id) { + return prev; + } + return response; + }); + }, []); + + // Initialize the WebSocket connection when there is a valid template version ID + useEffect(() => { + if (!realizedVersionId) { + return; + } + + const socket = API.templateVersionDynamicParameters(realizedVersionId, { + onMessage, + onError: (error) => { + setWsError(error); + }, + }); + + ws.current = socket; + + return () => { + socket.close(); + }; + }, [realizedVersionId, onMessage]); + + const sendMessage = useCallback((formValues: Record) => { + setWSResponseId((prevId) => { + const request: DynamicParametersRequest = { + id: prevId + 1, + inputs: formValues, + }; + if (ws.current && ws.current.readyState === WebSocket.OPEN) { + ws.current.send(JSON.stringify(request)); + return prevId + 1; + } + return prevId; + }); + }, []); + const organizationId = templateQuery.data?.organization_id; const { @@ -90,7 +134,9 @@ const CreateWorkspacePageExperimental: FC = () => { } = useExternalAuth(realizedVersionId); const isLoadingFormData = - templateQuery.isLoading || permissionsQuery.isLoading; + ws.current?.readyState !== WebSocket.OPEN || + templateQuery.isLoading || + permissionsQuery.isLoading; const loadFormDataError = templateQuery.error ?? permissionsQuery.error; const title = autoCreateWorkspaceMutation.isLoading @@ -189,11 +235,12 @@ const CreateWorkspacePageExperimental: FC = () => { { parameters={sortedParams} presets={templateVersionPresetsQuery.data ?? []} creatingWorkspace={createWorkspaceMutation.isLoading} - setWSResponseId={setWSResponseId} sendMessage={sendMessage} onCancel={() => { navigate(-1); diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index 49fd6e9188960..86f06b84bfe44 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -67,8 +67,7 @@ export interface CreateWorkspacePageViewExperimentalProps { owner: TypesGen.User, ) => void; resetMutation: () => void; - sendMessage: (message: DynamicParametersRequest) => void; - setWSResponseId: (value: React.SetStateAction) => void; + sendMessage: (message: Record) => void; startPollingExternalAuth: () => void; } @@ -95,7 +94,6 @@ export const CreateWorkspacePageViewExperimental: FC< onCancel, resetMutation, sendMessage, - setWSResponseId, startPollingExternalAuth, }) => { const [owner, setOwner] = useState(defaultOwner); @@ -222,15 +220,7 @@ export const CreateWorkspacePageViewExperimental: FC< // Update the input for the changed parameter formInputs[parameter.name] = value; - setWSResponseId((prevId) => { - const newId = prevId + 1; - const request: DynamicParametersRequest = { - id: newId, - inputs: formInputs, - }; - sendMessage(request); - return newId; - }); + sendMessage(formInputs); }; const { debounced: handleChangeDebounced } = useDebouncedFunction( @@ -240,7 +230,7 @@ export const CreateWorkspacePageViewExperimental: FC< value: string, ) => { await form.setFieldValue(parameterField, { - name: parameter.form_type, + name: parameter.name, value, }); sendDynamicParamsRequest(parameter, value); @@ -257,7 +247,7 @@ export const CreateWorkspacePageViewExperimental: FC< handleChangeDebounced(parameter, parameterField, value); } else { await form.setFieldValue(parameterField, { - name: parameter.form_type, + name: parameter.name, value, }); sendDynamicParamsRequest(parameter, value); From a8c2586404f8e13dac8203a894280615a80732bf Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 16 Apr 2025 18:00:56 +0100 Subject: [PATCH 114/384] feat: implement UI for top level dynamic parameters diagnostics (#17394) Screenshot 2025-04-14 at 21 31 11 --- site/src/index.css | 2 + .../DynamicParameter/DynamicParameter.tsx | 13 +++-- .../CreateWorkspacePageViewExperimental.tsx | 50 ++++++++++++++++--- site/tailwind.config.js | 1 + 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/site/src/index.css b/site/src/index.css index 6037a0d2fbfc4..fe8699bc62b07 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -30,6 +30,7 @@ --surface-sky: 201 94% 86%; --border-default: 240 6% 90%; --border-success: 142 76% 36%; + --border-warning: 30.66, 97.16%, 72.35%; --border-destructive: 0 84% 60%; --border-hover: 240, 5%, 34%; --overlay-default: 240 5% 84% / 80%; @@ -67,6 +68,7 @@ --surface-sky: 204 80% 16%; --border-default: 240 4% 16%; --border-success: 142 76% 36%; + --border-warning: 30.66, 97.16%, 72.35%; --border-destructive: 0 91% 71%; --border-hover: 240, 5%, 34%; --overlay-default: 240 10% 4% / 80%; diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index 939316625f3db..e1e79bdcd7a06 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -247,10 +247,13 @@ const ParameterField: FC = ({ className="flex items-center space-x-2" > -
    @@ -350,15 +353,15 @@ const ParameterDiagnostics: FC = ({
    {diagnostics.map((diagnostic, index) => (
    -
    {diagnostic.summary}
    - {diagnostic.detail &&
    {diagnostic.detail}
    } +

    {diagnostic.summary}

    + {diagnostic.detail &&

    {diagnostic.detail}

    }
    ))}
    diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index 86f06b84bfe44..3674884c1fb37 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -1,9 +1,5 @@ import type * as TypesGen from "api/typesGenerated"; -import type { - DynamicParametersRequest, - PreviewDiagnostics, - PreviewParameter, -} from "api/typesGenerated"; +import type { PreviewDiagnostics, PreviewParameter } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; @@ -19,7 +15,7 @@ import { Switch } from "components/Switch/Switch"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import { type FormikContextType, useFormik } from "formik"; import { useDebouncedFunction } from "hooks/debounce"; -import { ArrowLeft } from "lucide-react"; +import { ArrowLeft, CircleAlert, TriangleAlert } from "lucide-react"; import { DynamicParameter, getInitialParameterValues, @@ -413,6 +409,7 @@ export const CreateWorkspacePageViewExperimental: FC< parameters cannot be modified once the workspace is created.

    + {presets.length > 0 && (
    @@ -502,3 +499,44 @@ export const CreateWorkspacePageViewExperimental: FC< ); }; + +interface DiagnosticsProps { + diagnostics: PreviewParameter["diagnostics"]; +} + +export const Diagnostics: FC = ({ diagnostics }) => { + return ( +
    + {diagnostics.map((diagnostic, index) => ( +
    +
    + {diagnostic.severity === "error" && ( +
    + {diagnostic.detail &&

    {diagnostic.detail}

    } +
    + ))} +
    + ); +}; diff --git a/site/tailwind.config.js b/site/tailwind.config.js index 971a729332aff..3e612408596f5 100644 --- a/site/tailwind.config.js +++ b/site/tailwind.config.js @@ -52,6 +52,7 @@ module.exports = { }, border: { DEFAULT: "hsl(var(--border-default))", + warning: "hsl(var(--border-warning))", destructive: "hsl(var(--border-destructive))", success: "hsl(var(--border-success))", hover: "hsl(var(--border-hover))", From d20966d5004f0f3564ba611b76e78b6dc5824e66 Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Wed, 16 Apr 2025 20:11:02 +0200 Subject: [PATCH 115/384] chore: update go to 1.24.2 (#17356) this updates `go` to the latest stable patch version `1.24.2` in: - `go.mod` - `dogfood/coder/Dockerfile` - `.github/actions/setup-go/action.yaml` - `flake.nix` written with the assistance of ClaudeCode. --------- Co-authored-by: Thomas Kosiewski --- .github/actions/setup-go/action.yaml | 2 +- dogfood/coder/Dockerfile | 2 +- flake.nix | 4 ++-- go.mod | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/actions/setup-go/action.yaml b/.github/actions/setup-go/action.yaml index 7858b8ecc6cac..76b7c5d87d206 100644 --- a/.github/actions/setup-go/action.yaml +++ b/.github/actions/setup-go/action.yaml @@ -4,7 +4,7 @@ description: | inputs: version: description: "The Go version to use." - default: "1.24.1" + default: "1.24.2" runs: using: "composite" steps: diff --git a/dogfood/coder/Dockerfile b/dogfood/coder/Dockerfile index 1559279e41aa9..8a8f02e79e5dd 100644 --- a/dogfood/coder/Dockerfile +++ b/dogfood/coder/Dockerfile @@ -9,7 +9,7 @@ RUN cargo install typos-cli watchexec-cli && \ FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 AS go # Install Go manually, so that we can control the version -ARG GO_VERSION=1.24.1 +ARG GO_VERSION=1.24.2 # Boring Go is needed to build FIPS-compliant binaries. RUN apt-get update && \ diff --git a/flake.nix b/flake.nix index bb8f466383f04..af8c2b42bf00f 100644 --- a/flake.nix +++ b/flake.nix @@ -130,7 +130,7 @@ gnused gnugrep gnutar - go_1_22 + unstablePkgs.go_1_24 go-migrate (pinnedPkgs.golangci-lint) gopls @@ -196,7 +196,7 @@ # slim bundle into it's own derivation. buildFat = osArch: - pkgs.buildGo122Module { + unstablePkgs.buildGo124Module { name = "coder-${osArch}"; # Updated with ./scripts/update-flake.sh`. # This should be updated whenever go.mod changes! diff --git a/go.mod b/go.mod index d3e9c55f3d937..826d5cd2c0235 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/coder/coder/v2 -go 1.24.1 +go 1.24.2 // Required until a v3 of chroma is created to lazily initialize all XML files. // None of our dependencies seem to use the registries anyways, so this From c4d3dd27917da9f9782095233f53f430458118e6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 16 Apr 2025 14:39:57 -0500 Subject: [PATCH 116/384] chore: prevent null loading sync settings (#17430) Nulls passed to the frontend caused a page to fail to load. `Record` can be `nil` in golang --- coderd/idpsync/group.go | 3 +++ coderd/idpsync/organization.go | 3 +++ coderd/idpsync/role.go | 3 +++ 3 files changed, 9 insertions(+) diff --git a/coderd/idpsync/group.go b/coderd/idpsync/group.go index 4524284260359..b85ce1b749e28 100644 --- a/coderd/idpsync/group.go +++ b/coderd/idpsync/group.go @@ -268,6 +268,9 @@ func (s *GroupSyncSettings) Set(v string) error { } func (s *GroupSyncSettings) String() string { + if s.Mapping == nil { + s.Mapping = make(map[string][]uuid.UUID) + } return runtimeconfig.JSONString(s) } diff --git a/coderd/idpsync/organization.go b/coderd/idpsync/organization.go index be65daba369df..5d56bc7d239a5 100644 --- a/coderd/idpsync/organization.go +++ b/coderd/idpsync/organization.go @@ -217,6 +217,9 @@ func (s *OrganizationSyncSettings) Set(v string) error { } func (s *OrganizationSyncSettings) String() string { + if s.Mapping == nil { + s.Mapping = make(map[string][]uuid.UUID) + } return runtimeconfig.JSONString(s) } diff --git a/coderd/idpsync/role.go b/coderd/idpsync/role.go index 54ec787661826..c21e7c99c4614 100644 --- a/coderd/idpsync/role.go +++ b/coderd/idpsync/role.go @@ -286,5 +286,8 @@ func (s *RoleSyncSettings) Set(v string) error { } func (s *RoleSyncSettings) String() string { + if s.Mapping == nil { + s.Mapping = make(map[string][]string) + } return runtimeconfig.JSONString(s) } From 2e5cd299f2004a25e772c86c798a30df897a05b4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 16 Apr 2025 15:55:37 -0500 Subject: [PATCH 117/384] chore: load 'assign_default' value from legacy value (#17428) If this value was set before v2.19.0, then assign_default was in a json field that would not match. And it would default to `false`. This corrects that. --- coderd/idpsync/organization.go | 11 +++++ coderd/idpsync/organizations_test.go | 68 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/coderd/idpsync/organization.go b/coderd/idpsync/organization.go index 5d56bc7d239a5..f0736e1ea7559 100644 --- a/coderd/idpsync/organization.go +++ b/coderd/idpsync/organization.go @@ -213,6 +213,17 @@ type OrganizationSyncSettings struct { } func (s *OrganizationSyncSettings) Set(v string) error { + legacyCheck := make(map[string]any) + err := json.Unmarshal([]byte(v), &legacyCheck) + if assign, ok := legacyCheck["AssignDefault"]; err == nil && ok { + // The legacy JSON key was 'AssignDefault' instead of 'assign_default' + // Set the default value from the legacy if it exists. + isBool, ok := assign.(bool) + if ok { + s.AssignDefault = isBool + } + } + return json.Unmarshal([]byte(v), s) } diff --git a/coderd/idpsync/organizations_test.go b/coderd/idpsync/organizations_test.go index 3a00499bdbced..c3c0a052f7100 100644 --- a/coderd/idpsync/organizations_test.go +++ b/coderd/idpsync/organizations_test.go @@ -2,6 +2,7 @@ package idpsync_test import ( "database/sql" + "fmt" "testing" "github.com/golang-jwt/jwt/v4" @@ -19,6 +20,73 @@ import ( "github.com/coder/coder/v2/testutil" ) +func TestFromLegacySettings(t *testing.T) { + t.Parallel() + + legacy := func(assignDefault bool) string { + return fmt.Sprintf(`{ + "Field":"groups", + "Mapping":{ + "engineering":[ + "10b2bd19-f5ca-4905-919f-bf02e95e3b6a" + ] + }, + "AssignDefault":%t + }`, assignDefault) + } + + t.Run("AssignDefault,True", func(t *testing.T) { + t.Parallel() + + var settings idpsync.OrganizationSyncSettings + settings.AssignDefault = true + err := settings.Set(legacy(true)) + require.NoError(t, err) + + require.Equal(t, settings.Field, "groups", "field") + require.Equal(t, settings.Mapping, map[string][]uuid.UUID{ + "engineering": { + uuid.MustParse("10b2bd19-f5ca-4905-919f-bf02e95e3b6a"), + }, + }, "mapping") + require.True(t, settings.AssignDefault, "assign default") + }) + + t.Run("AssignDefault,False", func(t *testing.T) { + t.Parallel() + + var settings idpsync.OrganizationSyncSettings + settings.AssignDefault = true + err := settings.Set(legacy(false)) + require.NoError(t, err) + + require.Equal(t, settings.Field, "groups", "field") + require.Equal(t, settings.Mapping, map[string][]uuid.UUID{ + "engineering": { + uuid.MustParse("10b2bd19-f5ca-4905-919f-bf02e95e3b6a"), + }, + }, "mapping") + require.False(t, settings.AssignDefault, "assign default") + }) + + t.Run("CorrectAssign", func(t *testing.T) { + t.Parallel() + + var settings idpsync.OrganizationSyncSettings + settings.AssignDefault = true + err := settings.Set(legacy(false)) + require.NoError(t, err) + + require.Equal(t, settings.Field, "groups", "field") + require.Equal(t, settings.Mapping, map[string][]uuid.UUID{ + "engineering": { + uuid.MustParse("10b2bd19-f5ca-4905-919f-bf02e95e3b6a"), + }, + }, "mapping") + require.False(t, settings.AssignDefault, "assign default") + }) +} + func TestParseOrganizationClaims(t *testing.T) { t.Parallel() From 7f6e5139eb62d62f96e04721bf76715b78166199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Wed, 16 Apr 2025 16:21:14 -0700 Subject: [PATCH 118/384] chore: format code (#17438) --- coderd/idpsync/organizations_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/coderd/idpsync/organizations_test.go b/coderd/idpsync/organizations_test.go index c3c0a052f7100..c3f17cefebd28 100644 --- a/coderd/idpsync/organizations_test.go +++ b/coderd/idpsync/organizations_test.go @@ -25,13 +25,13 @@ func TestFromLegacySettings(t *testing.T) { legacy := func(assignDefault bool) string { return fmt.Sprintf(`{ - "Field":"groups", - "Mapping":{ - "engineering":[ - "10b2bd19-f5ca-4905-919f-bf02e95e3b6a" - ] - }, - "AssignDefault":%t + "Field": "groups", + "Mapping": { + "engineering": [ + "10b2bd19-f5ca-4905-919f-bf02e95e3b6a" + ] + }, + "AssignDefault": %t }`, assignDefault) } From 0bc49ff5ae1202bfb2853a6c080801738f54ff49 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 16 Apr 2025 19:14:11 -0500 Subject: [PATCH 119/384] test: fix flake in TestRoleSyncTable with test cases sharing resources (#17441) The test case definition shares maps that can have concurrent access if run in parallel. --- coderd/idpsync/role_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/coderd/idpsync/role_test.go b/coderd/idpsync/role_test.go index 7d686442144b1..d766ada6057f7 100644 --- a/coderd/idpsync/role_test.go +++ b/coderd/idpsync/role_test.go @@ -225,9 +225,8 @@ func TestRoleSyncTable(t *testing.T) { // deployment. This tests all organizations being synced together. // The reason we do them individually, is that it is much easier to // debug a single test case. + //nolint:paralleltest, tparallel // This should run after all the individual tests t.Run("AllTogether", func(t *testing.T) { - t.Parallel() - db, _ := dbtestutil.NewDB(t) manager := runtimeconfig.NewManager() s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{ From 6f5da1e2ee07a385d425240262999109a263dec1 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Thu, 17 Apr 2025 12:09:46 +0500 Subject: [PATCH 120/384] chore: add windsurf icon (#17443) --- site/src/theme/icons.json | 1 + site/static/icon/windsurf.svg | 43 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 site/static/icon/windsurf.svg diff --git a/site/src/theme/icons.json b/site/src/theme/icons.json index 7c7d8dac9e9db..a9307bfc78446 100644 --- a/site/src/theme/icons.json +++ b/site/src/theme/icons.json @@ -101,5 +101,6 @@ "vault.svg", "webstorm.svg", "widgets.svg", + "windsurf.svg", "zed.svg" ] diff --git a/site/static/icon/windsurf.svg b/site/static/icon/windsurf.svg new file mode 100644 index 0000000000000..a7684d4cb7862 --- /dev/null +++ b/site/static/icon/windsurf.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3b54254177599abddf91028381507893bdf250ff Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 17 Apr 2025 11:23:24 +0400 Subject: [PATCH 121/384] feat: add coder connect exists hidden subcommand (#17418) Adds a new hidden subcommand `coder connect exists ` that checks if the name exists via Coder Connect. This will be used in SSH config to match only if Coder Connect is unavailable for the hostname in question, so that the SSH client will directly dial the workspace over an existing Coder Connect tunnel. Also refactors the way we inject a test DNS resolver into the lookup functions so that we can test from outside the `workspacesdk` package. --- cli/configssh.go | 3 +- cli/connect.go | 47 ++++++++++ cli/connect_test.go | 76 ++++++++++++++++ cli/root.go | 12 ++- cli/root_test.go | 3 +- codersdk/workspacesdk/workspacesdk.go | 39 +++++++-- .../workspacesdk_internal_test.go | 86 ------------------- codersdk/workspacesdk/workspacesdk_test.go | 74 ++++++++++++++++ 8 files changed, 242 insertions(+), 98 deletions(-) create mode 100644 cli/connect.go create mode 100644 cli/connect_test.go delete mode 100644 codersdk/workspacesdk/workspacesdk_internal_test.go diff --git a/cli/configssh.go b/cli/configssh.go index 6a0f41c2a2fbc..c089141846d39 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -22,9 +22,10 @@ import ( "golang.org/x/exp/constraints" "golang.org/x/xerrors" + "github.com/coder/serpent" + "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" - "github.com/coder/serpent" ) const ( diff --git a/cli/connect.go b/cli/connect.go new file mode 100644 index 0000000000000..d1245147f3848 --- /dev/null +++ b/cli/connect.go @@ -0,0 +1,47 @@ +package cli + +import ( + "github.com/coder/serpent" + + "github.com/coder/coder/v2/codersdk/workspacesdk" +) + +func (r *RootCmd) connectCmd() *serpent.Command { + cmd := &serpent.Command{ + Use: "connect", + Short: "Commands related to Coder Connect (OS-level tunneled connection to workspaces).", + Handler: func(i *serpent.Invocation) error { + return i.Command.HelpHandler(i) + }, + Hidden: true, + Children: []*serpent.Command{ + r.existsCmd(), + }, + } + return cmd +} + +func (*RootCmd) existsCmd() *serpent.Command { + cmd := &serpent.Command{ + Use: "exists ", + Short: "Checks if the given hostname exists via Coder Connect.", + Long: "This command is designed to be used in scripts to check if the given hostname exists via Coder " + + "Connect. It prints no output. It returns exit code 0 if it does exist and code 1 if it does not.", + Middleware: serpent.Chain( + serpent.RequireNArgs(1), + ), + Handler: func(inv *serpent.Invocation) error { + hostname := inv.Args[0] + exists, err := workspacesdk.ExistsViaCoderConnect(inv.Context(), hostname) + if err != nil { + return err + } + if !exists { + // we don't want to print any output, since this command is designed to be a check in scripts / SSH config. + return ErrSilent + } + return nil + }, + } + return cmd +} diff --git a/cli/connect_test.go b/cli/connect_test.go new file mode 100644 index 0000000000000..031cd2f95b1f9 --- /dev/null +++ b/cli/connect_test.go @@ -0,0 +1,76 @@ +package cli_test + +import ( + "bytes" + "context" + "net" + "testing" + + "github.com/stretchr/testify/require" + "tailscale.com/net/tsaddr" + + "github.com/coder/serpent" + + "github.com/coder/coder/v2/cli" + "github.com/coder/coder/v2/codersdk/workspacesdk" + "github.com/coder/coder/v2/testutil" +) + +func TestConnectExists_Running(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + var root cli.RootCmd + cmd, err := root.Command(root.AGPL()) + require.NoError(t, err) + + inv := (&serpent.Invocation{ + Command: cmd, + Args: []string{"connect", "exists", "test.example"}, + }).WithContext(withCoderConnectRunning(ctx)) + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + inv.Stdout = stdout + inv.Stderr = stderr + err = inv.Run() + require.NoError(t, err) +} + +func TestConnectExists_NotRunning(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + var root cli.RootCmd + cmd, err := root.Command(root.AGPL()) + require.NoError(t, err) + + inv := (&serpent.Invocation{ + Command: cmd, + Args: []string{"connect", "exists", "test.example"}, + }).WithContext(withCoderConnectNotRunning(ctx)) + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + inv.Stdout = stdout + inv.Stderr = stderr + err = inv.Run() + require.ErrorIs(t, err, cli.ErrSilent) +} + +type fakeResolver struct { + shouldReturnSuccess bool +} + +func (f *fakeResolver) LookupIP(_ context.Context, _, _ string) ([]net.IP, error) { + if f.shouldReturnSuccess { + return []net.IP{net.ParseIP(tsaddr.CoderServiceIPv6().String())}, nil + } + return nil, &net.DNSError{IsNotFound: true} +} + +func withCoderConnectRunning(ctx context.Context) context.Context { + return workspacesdk.WithTestOnlyCoderContextResolver(ctx, &fakeResolver{shouldReturnSuccess: true}) +} + +func withCoderConnectNotRunning(ctx context.Context) context.Context { + return workspacesdk.WithTestOnlyCoderContextResolver(ctx, &fakeResolver{shouldReturnSuccess: false}) +} diff --git a/cli/root.go b/cli/root.go index 75cbb4dd2ca1a..5c70379b75a44 100644 --- a/cli/root.go +++ b/cli/root.go @@ -31,6 +31,8 @@ import ( "github.com/coder/pretty" + "github.com/coder/serpent" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/cli/config" @@ -38,7 +40,6 @@ import ( "github.com/coder/coder/v2/cli/telemetry" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" - "github.com/coder/serpent" ) var ( @@ -49,6 +50,10 @@ var ( workspaceCommand = map[string]string{ "workspaces": "", } + + // ErrSilent is a sentinel error that tells the command handler to just exit with a non-zero error, but not print + // anything. + ErrSilent = xerrors.New("silent error") ) const ( @@ -122,6 +127,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command { r.whoami(), // Hidden + r.connectCmd(), r.expCmd(), r.gitssh(), r.support(), @@ -175,6 +181,10 @@ func (r *RootCmd) RunWithSubcommands(subcommands []*serpent.Command) { //nolint:revive,gocritic os.Exit(code) } + if errors.Is(err, ErrSilent) { + //nolint:revive,gocritic + os.Exit(code) + } f := PrettyErrorFormatter{w: os.Stderr, verbose: r.verbose} if err != nil { f.Format(err) diff --git a/cli/root_test.go b/cli/root_test.go index ac1454152672e..698c9aff60186 100644 --- a/cli/root_test.go +++ b/cli/root_test.go @@ -10,12 +10,13 @@ import ( "sync/atomic" "testing" + "github.com/coder/serpent" + "github.com/coder/coder/v2/coderd" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/testutil" - "github.com/coder/serpent" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/codersdk/workspacesdk/workspacesdk.go b/codersdk/workspacesdk/workspacesdk.go index 25188917dafc9..83f236a215b56 100644 --- a/codersdk/workspacesdk/workspacesdk.go +++ b/codersdk/workspacesdk/workspacesdk.go @@ -20,11 +20,12 @@ import ( "cdr.dev/slog" + "github.com/coder/quartz" + "github.com/coder/websocket" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/tailnet/proto" - "github.com/coder/quartz" - "github.com/coder/websocket" ) var ErrSkipClose = xerrors.New("skip tailnet close") @@ -128,19 +129,16 @@ func init() { } } -type resolver interface { +type Resolver interface { LookupIP(ctx context.Context, network, host string) ([]net.IP, error) } type Client struct { client *codersdk.Client - - // overridden in tests - resolver resolver } func New(c *codersdk.Client) *Client { - return &Client{client: c, resolver: net.DefaultResolver} + return &Client{client: c} } // AgentConnectionInfo returns required information for establishing @@ -392,6 +390,12 @@ func (c *Client) AgentReconnectingPTY(ctx context.Context, opts WorkspaceAgentRe return websocket.NetConn(context.Background(), conn, websocket.MessageBinary), nil } +func WithTestOnlyCoderContextResolver(ctx context.Context, r Resolver) context.Context { + return context.WithValue(ctx, dnsResolverContextKey{}, r) +} + +type dnsResolverContextKey struct{} + type CoderConnectQueryOptions struct { HostnameSuffix string } @@ -409,15 +413,32 @@ func (c *Client) IsCoderConnectRunning(ctx context.Context, o CoderConnectQueryO suffix = info.HostnameSuffix } domainName := fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, suffix) + return ExistsViaCoderConnect(ctx, domainName) +} + +func testOrDefaultResolver(ctx context.Context) Resolver { + // check the context for a non-default resolver. This is only used in testing. + resolver, ok := ctx.Value(dnsResolverContextKey{}).(Resolver) + if !ok || resolver == nil { + resolver = net.DefaultResolver + } + return resolver +} + +// ExistsViaCoderConnect checks if the given hostname exists via Coder Connect. This doesn't guarantee the +// workspace is actually reachable, if, for example, its agent is unhealthy, but rather that Coder Connect knows about +// the workspace and advertises the hostname via DNS. +func ExistsViaCoderConnect(ctx context.Context, hostname string) (bool, error) { + resolver := testOrDefaultResolver(ctx) var dnsError *net.DNSError - ips, err := c.resolver.LookupIP(ctx, "ip6", domainName) + ips, err := resolver.LookupIP(ctx, "ip6", hostname) if xerrors.As(err, &dnsError) { if dnsError.IsNotFound { return false, nil } } if err != nil { - return false, xerrors.Errorf("lookup DNS %s: %w", domainName, err) + return false, xerrors.Errorf("lookup DNS %s: %w", hostname, err) } // The returned IP addresses are probably from the Coder Connect DNS server, but there are sometimes weird captive diff --git a/codersdk/workspacesdk/workspacesdk_internal_test.go b/codersdk/workspacesdk/workspacesdk_internal_test.go deleted file mode 100644 index 1b98ebdc2e671..0000000000000 --- a/codersdk/workspacesdk/workspacesdk_internal_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package workspacesdk - -import ( - "context" - "fmt" - "net" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/xerrors" - - "github.com/coder/coder/v2/coderd/httpapi" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/testutil" - - "tailscale.com/net/tsaddr" - - "github.com/coder/coder/v2/tailnet" -) - -func TestClient_IsCoderConnectRunning(t *testing.T) { - t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) - - srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/api/v2/workspaceagents/connection", r.URL.Path) - httpapi.Write(ctx, rw, http.StatusOK, AgentConnectionInfo{ - HostnameSuffix: "test", - }) - })) - defer srv.Close() - - apiURL, err := url.Parse(srv.URL) - require.NoError(t, err) - sdkClient := codersdk.New(apiURL) - client := New(sdkClient) - - // Right name, right IP - expectedName := fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "test") - client.resolver = &fakeResolver{t: t, hostMap: map[string][]net.IP{ - expectedName: {net.ParseIP(tsaddr.CoderServiceIPv6().String())}, - }} - - result, err := client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{}) - require.NoError(t, err) - require.True(t, result) - - // Wrong name - result, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{HostnameSuffix: "coder"}) - require.NoError(t, err) - require.False(t, result) - - // Not found - client.resolver = &fakeResolver{t: t, err: &net.DNSError{IsNotFound: true}} - result, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{}) - require.NoError(t, err) - require.False(t, result) - - // Some other error - client.resolver = &fakeResolver{t: t, err: xerrors.New("a bad thing happened")} - _, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{}) - require.Error(t, err) - - // Right name, wrong IP - client.resolver = &fakeResolver{t: t, hostMap: map[string][]net.IP{ - expectedName: {net.ParseIP("2001::34")}, - }} - result, err = client.IsCoderConnectRunning(ctx, CoderConnectQueryOptions{}) - require.NoError(t, err) - require.False(t, result) -} - -type fakeResolver struct { - t testing.TB - hostMap map[string][]net.IP - err error -} - -func (f *fakeResolver) LookupIP(_ context.Context, network, host string) ([]net.IP, error) { - assert.Equal(f.t, "ip6", network) - return f.hostMap[host], f.err -} diff --git a/codersdk/workspacesdk/workspacesdk_test.go b/codersdk/workspacesdk/workspacesdk_test.go index e7ccd96e208fa..16a523b2d4d53 100644 --- a/codersdk/workspacesdk/workspacesdk_test.go +++ b/codersdk/workspacesdk/workspacesdk_test.go @@ -1,12 +1,18 @@ package workspacesdk_test import ( + "context" + "fmt" + "net" "net/http" "net/http/httptest" "net/url" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "github.com/coder/websocket" @@ -15,6 +21,7 @@ import ( "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" + "github.com/coder/coder/v2/tailnet" "github.com/coder/coder/v2/testutil" ) @@ -72,3 +79,70 @@ func TestWorkspaceDialerFailure(t *testing.T) { // Then: an error indicating a database issue is returned, to conditionalize the behavior of the caller. require.ErrorIs(t, err, codersdk.ErrDatabaseNotReachable) } + +func TestClient_IsCoderConnectRunning(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v2/workspaceagents/connection", r.URL.Path) + httpapi.Write(ctx, rw, http.StatusOK, workspacesdk.AgentConnectionInfo{ + HostnameSuffix: "test", + }) + })) + defer srv.Close() + + apiURL, err := url.Parse(srv.URL) + require.NoError(t, err) + sdkClient := codersdk.New(apiURL) + client := workspacesdk.New(sdkClient) + + // Right name, right IP + expectedName := fmt.Sprintf(tailnet.IsCoderConnectEnabledFmtString, "test") + ctxResolveExpected := workspacesdk.WithTestOnlyCoderContextResolver(ctx, + &fakeResolver{t: t, hostMap: map[string][]net.IP{ + expectedName: {net.ParseIP(tsaddr.CoderServiceIPv6().String())}, + }}) + + result, err := client.IsCoderConnectRunning(ctxResolveExpected, workspacesdk.CoderConnectQueryOptions{}) + require.NoError(t, err) + require.True(t, result) + + // Wrong name + result, err = client.IsCoderConnectRunning(ctxResolveExpected, workspacesdk.CoderConnectQueryOptions{HostnameSuffix: "coder"}) + require.NoError(t, err) + require.False(t, result) + + // Not found + ctxResolveNotFound := workspacesdk.WithTestOnlyCoderContextResolver(ctx, + &fakeResolver{t: t, err: &net.DNSError{IsNotFound: true}}) + result, err = client.IsCoderConnectRunning(ctxResolveNotFound, workspacesdk.CoderConnectQueryOptions{}) + require.NoError(t, err) + require.False(t, result) + + // Some other error + ctxResolverErr := workspacesdk.WithTestOnlyCoderContextResolver(ctx, + &fakeResolver{t: t, err: xerrors.New("a bad thing happened")}) + _, err = client.IsCoderConnectRunning(ctxResolverErr, workspacesdk.CoderConnectQueryOptions{}) + require.Error(t, err) + + // Right name, wrong IP + ctxResolverWrongIP := workspacesdk.WithTestOnlyCoderContextResolver(ctx, + &fakeResolver{t: t, hostMap: map[string][]net.IP{ + expectedName: {net.ParseIP("2001::34")}, + }}) + result, err = client.IsCoderConnectRunning(ctxResolverWrongIP, workspacesdk.CoderConnectQueryOptions{}) + require.NoError(t, err) + require.False(t, result) +} + +type fakeResolver struct { + t testing.TB + hostMap map[string][]net.IP + err error +} + +func (f *fakeResolver) LookupIP(_ context.Context, network, host string) ([]net.IP, error) { + assert.Equal(f.t, "ip6", network) + return f.hostMap[host], f.err +} From b0854aa97139f05301dfc89aff5e0e76fce6ce90 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 17 Apr 2025 12:04:00 +0400 Subject: [PATCH 122/384] feat: modify config-ssh to check for Coder Connect (#17419) relates to #16828 Changes SSH config so that suffixes only match if Coder Connect is not running / available. This means that we will use the existing Coder Connect tunnel if it is available, rather than creating a new tunnel via `coder ssh --stdio`. --- cli/configssh.go | 283 +++++++++++++++++++++++------------------- cli/configssh_test.go | 15 ++- 2 files changed, 166 insertions(+), 132 deletions(-) diff --git a/cli/configssh.go b/cli/configssh.go index c089141846d39..e90c8080abb0d 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -48,13 +48,17 @@ const ( type sshConfigOptions struct { waitEnum string // Deprecated: moving away from prefix to hostnameSuffix - userHostPrefix string - hostnameSuffix string - sshOptions []string - disableAutostart bool - header []string - headerCommand string - removedKeys map[string]bool + userHostPrefix string + hostnameSuffix string + sshOptions []string + disableAutostart bool + header []string + headerCommand string + removedKeys map[string]bool + globalConfigPath string + coderBinaryPath string + skipProxyCommand bool + forceUnixSeparators bool } // addOptions expects options in the form of "option=value" or "option value". @@ -107,6 +111,80 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool { o.hostnameSuffix == other.hostnameSuffix } +func (o sshConfigOptions) writeToBuffer(buf *bytes.Buffer) error { + escapedCoderBinary, err := sshConfigExecEscape(o.coderBinaryPath, o.forceUnixSeparators) + if err != nil { + return xerrors.Errorf("escape coder binary for ssh failed: %w", err) + } + + escapedGlobalConfig, err := sshConfigExecEscape(o.globalConfigPath, o.forceUnixSeparators) + if err != nil { + return xerrors.Errorf("escape global config for ssh failed: %w", err) + } + + rootFlags := fmt.Sprintf("--global-config %s", escapedGlobalConfig) + for _, h := range o.header { + rootFlags += fmt.Sprintf(" --header %q", h) + } + if o.headerCommand != "" { + rootFlags += fmt.Sprintf(" --header-command %q", o.headerCommand) + } + + flags := "" + if o.waitEnum != "auto" { + flags += " --wait=" + o.waitEnum + } + if o.disableAutostart { + flags += " --disable-autostart=true" + } + + // Prefix block: + if o.userHostPrefix != "" { + _, _ = buf.WriteString("Host") + + _, _ = buf.WriteString(" ") + _, _ = buf.WriteString(o.userHostPrefix) + _, _ = buf.WriteString("*\n") + + for _, v := range o.sshOptions { + _, _ = buf.WriteString("\t") + _, _ = buf.WriteString(v) + _, _ = buf.WriteString("\n") + } + if !o.skipProxyCommand && o.userHostPrefix != "" { + _, _ = buf.WriteString("\t") + _, _ = fmt.Fprintf(buf, + "ProxyCommand %s %s ssh --stdio%s --ssh-host-prefix %s %%h", + escapedCoderBinary, rootFlags, flags, o.userHostPrefix, + ) + _, _ = buf.WriteString("\n") + } + } + + // Suffix block + if o.hostnameSuffix == "" { + return nil + } + _, _ = fmt.Fprintf(buf, "\nHost *.%s\n", o.hostnameSuffix) + for _, v := range o.sshOptions { + _, _ = buf.WriteString("\t") + _, _ = buf.WriteString(v) + _, _ = buf.WriteString("\n") + } + // the ^^ options should always apply, but we only want to use the proxy command if Coder Connect is not running. + if !o.skipProxyCommand { + _, _ = fmt.Fprintf(buf, "\nMatch host *.%s !exec \"%s connect exists %%h\"\n", + o.hostnameSuffix, escapedCoderBinary) + _, _ = buf.WriteString("\t") + _, _ = fmt.Fprintf(buf, + "ProxyCommand %s %s ssh --stdio%s --hostname-suffix %s %%h", + escapedCoderBinary, rootFlags, flags, o.hostnameSuffix, + ) + _, _ = buf.WriteString("\n") + } + return nil +} + // slicesSortedEqual compares two slices without side-effects or regard to order. func slicesSortedEqual[S ~[]E, E constraints.Ordered](a, b S) bool { if len(a) != len(b) { @@ -147,13 +225,11 @@ func (o sshConfigOptions) asList() (list []string) { func (r *RootCmd) configSSH() *serpent.Command { var ( - sshConfigFile string - sshConfigOpts sshConfigOptions - usePreviousOpts bool - dryRun bool - skipProxyCommand bool - forceUnixSeparators bool - coderCliPath string + sshConfigFile string + sshConfigOpts sshConfigOptions + usePreviousOpts bool + dryRun bool + coderCliPath string ) client := new(codersdk.Client) cmd := &serpent.Command{ @@ -177,7 +253,7 @@ func (r *RootCmd) configSSH() *serpent.Command { Handler: func(inv *serpent.Invocation) error { ctx := inv.Context() - if sshConfigOpts.waitEnum != "auto" && skipProxyCommand { + if sshConfigOpts.waitEnum != "auto" && sshConfigOpts.skipProxyCommand { // The wait option is applied to the ProxyCommand. If the user // specifies skip-proxy-command, then wait cannot be applied. return xerrors.Errorf("cannot specify both --skip-proxy-command and --wait") @@ -207,18 +283,7 @@ func (r *RootCmd) configSSH() *serpent.Command { return err } } - - escapedCoderBinary, err := sshConfigExecEscape(coderBinary, forceUnixSeparators) - if err != nil { - return xerrors.Errorf("escape coder binary for ssh failed: %w", err) - } - root := r.createConfig() - escapedGlobalConfig, err := sshConfigExecEscape(string(root), forceUnixSeparators) - if err != nil { - return xerrors.Errorf("escape global config for ssh failed: %w", err) - } - homedir, err := os.UserHomeDir() if err != nil { return xerrors.Errorf("user home dir failed: %w", err) @@ -320,94 +385,15 @@ func (r *RootCmd) configSSH() *serpent.Command { coderdConfig.HostnamePrefix = "coder." } - if sshConfigOpts.userHostPrefix != "" { - // Override with user flag. - coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix - } - if sshConfigOpts.hostnameSuffix != "" { - // Override with user flag. - coderdConfig.HostnameSuffix = sshConfigOpts.hostnameSuffix - } - - // Write agent configuration. - defaultOptions := []string{ - "ConnectTimeout=0", - "StrictHostKeyChecking=no", - // Without this, the "REMOTE HOST IDENTITY CHANGED" - // message will appear. - "UserKnownHostsFile=/dev/null", - // This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts." - // message from appearing on every SSH. This happens because we ignore the known hosts. - "LogLevel ERROR", - } - - if !skipProxyCommand { - rootFlags := fmt.Sprintf("--global-config %s", escapedGlobalConfig) - for _, h := range sshConfigOpts.header { - rootFlags += fmt.Sprintf(" --header %q", h) - } - if sshConfigOpts.headerCommand != "" { - rootFlags += fmt.Sprintf(" --header-command %q", sshConfigOpts.headerCommand) - } - - flags := "" - if sshConfigOpts.waitEnum != "auto" { - flags += " --wait=" + sshConfigOpts.waitEnum - } - if sshConfigOpts.disableAutostart { - flags += " --disable-autostart=true" - } - if coderdConfig.HostnamePrefix != "" { - flags += " --ssh-host-prefix " + coderdConfig.HostnamePrefix - } - if coderdConfig.HostnameSuffix != "" { - flags += " --hostname-suffix " + coderdConfig.HostnameSuffix - } - defaultOptions = append(defaultOptions, fmt.Sprintf( - "ProxyCommand %s %s ssh --stdio%s %%h", - escapedCoderBinary, rootFlags, flags, - )) - } - - // Create a copy of the options so we can modify them. - configOptions := sshConfigOpts - configOptions.sshOptions = nil - - // User options first (SSH only uses the first - // option unless it can be given multiple times) - for _, opt := range sshConfigOpts.sshOptions { - err := configOptions.addOptions(opt) - if err != nil { - return xerrors.Errorf("add flag config option %q: %w", opt, err) - } - } - - // Deployment options second, allow them to - // override standard options. - for k, v := range coderdConfig.SSHConfigOptions { - opt := fmt.Sprintf("%s %s", k, v) - err := configOptions.addOptions(opt) - if err != nil { - return xerrors.Errorf("add coderd config option %q: %w", opt, err) - } - } - - // Finally, add the standard options. - if err := configOptions.addOptions(defaultOptions...); err != nil { + configOptions, err := mergeSSHOptions(sshConfigOpts, coderdConfig, string(root), coderBinary) + if err != nil { return err } - - hostBlock := []string{ - sshConfigHostLinePatterns(coderdConfig), - } - // Prefix with '\t' - for _, v := range configOptions.sshOptions { - hostBlock = append(hostBlock, "\t"+v) + err = configOptions.writeToBuffer(buf) + if err != nil { + return err } - _, _ = buf.WriteString(strings.Join(hostBlock, "\n")) - _ = buf.WriteByte('\n') - sshConfigWriteSectionEnd(buf) // Write the remainder of the users config file to buf. @@ -523,7 +509,7 @@ func (r *RootCmd) configSSH() *serpent.Command { Flag: "skip-proxy-command", Env: "CODER_SSH_SKIP_PROXY_COMMAND", Description: "Specifies whether the ProxyCommand option should be skipped. Useful for testing.", - Value: serpent.BoolOf(&skipProxyCommand), + Value: serpent.BoolOf(&sshConfigOpts.skipProxyCommand), Hidden: true, }, { @@ -564,7 +550,7 @@ func (r *RootCmd) configSSH() *serpent.Command { Description: "By default, 'config-ssh' uses the os path separator when writing the ssh config. " + "This might be an issue in Windows machine that use a unix-like shell. " + "This flag forces the use of unix file paths (the forward slash '/').", - Value: serpent.BoolOf(&forceUnixSeparators), + Value: serpent.BoolOf(&sshConfigOpts.forceUnixSeparators), // On non-windows showing this command is useless because it is a noop. // Hide vs disable it though so if a command is copied from a Windows // machine to a unix machine it will still work and not throw an @@ -577,6 +563,63 @@ func (r *RootCmd) configSSH() *serpent.Command { return cmd } +func mergeSSHOptions( + user sshConfigOptions, coderd codersdk.SSHConfigResponse, globalConfigPath, coderBinaryPath string, +) ( + sshConfigOptions, error, +) { + // Write agent configuration. + defaultOptions := []string{ + "ConnectTimeout=0", + "StrictHostKeyChecking=no", + // Without this, the "REMOTE HOST IDENTITY CHANGED" + // message will appear. + "UserKnownHostsFile=/dev/null", + // This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts." + // message from appearing on every SSH. This happens because we ignore the known hosts. + "LogLevel ERROR", + } + + // Create a copy of the options so we can modify them. + configOptions := user + configOptions.sshOptions = nil + + configOptions.globalConfigPath = globalConfigPath + configOptions.coderBinaryPath = coderBinaryPath + // user config takes precedence + if user.userHostPrefix == "" { + configOptions.userHostPrefix = coderd.HostnamePrefix + } + if user.hostnameSuffix == "" { + configOptions.hostnameSuffix = coderd.HostnameSuffix + } + + // User options first (SSH only uses the first + // option unless it can be given multiple times) + for _, opt := range user.sshOptions { + err := configOptions.addOptions(opt) + if err != nil { + return sshConfigOptions{}, xerrors.Errorf("add flag config option %q: %w", opt, err) + } + } + + // Deployment options second, allow them to + // override standard options. + for k, v := range coderd.SSHConfigOptions { + opt := fmt.Sprintf("%s %s", k, v) + err := configOptions.addOptions(opt) + if err != nil { + return sshConfigOptions{}, xerrors.Errorf("add coderd config option %q: %w", opt, err) + } + } + + // Finally, add the standard options. + if err := configOptions.addOptions(defaultOptions...); err != nil { + return sshConfigOptions{}, err + } + return configOptions, nil +} + //nolint:revive func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOptions) { nl := "\n" @@ -844,19 +887,3 @@ func diffBytes(name string, b1, b2 []byte, color bool) ([]byte, error) { } return b, nil } - -func sshConfigHostLinePatterns(config codersdk.SSHConfigResponse) string { - builder := strings.Builder{} - // by inspection, WriteString always returns nil error - _, _ = builder.WriteString("Host") - if config.HostnamePrefix != "" { - _, _ = builder.WriteString(" ") - _, _ = builder.WriteString(config.HostnamePrefix) - _, _ = builder.WriteString("*") - } - if config.HostnameSuffix != "" { - _, _ = builder.WriteString(" *.") - _, _ = builder.WriteString(config.HostnameSuffix) - } - return builder.String() -} diff --git a/cli/configssh_test.go b/cli/configssh_test.go index b42241b6b3aad..72faaa00c1ca0 100644 --- a/cli/configssh_test.go +++ b/cli/configssh_test.go @@ -615,13 +615,21 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { name: "Hostname Suffix", args: []string{ "--yes", + "--ssh-option", "Foo=bar", "--hostname-suffix", "testy", }, wantErr: false, hasAgent: true, wantConfig: wantConfig{ - ssh: []string{"Host coder.* *.testy"}, - regexMatch: `ProxyCommand .* ssh .* --hostname-suffix testy %h`, + ssh: []string{ + "Host *.testy", + "Foo=bar", + "ConnectTimeout=0", + "StrictHostKeyChecking=no", + "UserKnownHostsFile=/dev/null", + "LogLevel ERROR", + }, + regexMatch: `Match host \*\.testy !exec ".* connect exists %h"\n\tProxyCommand .* ssh .* --hostname-suffix testy %h`, }, }, { @@ -634,8 +642,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) { wantErr: false, hasAgent: true, wantConfig: wantConfig{ - ssh: []string{"Host presto.* *.testy"}, - regexMatch: `ProxyCommand .* ssh .* --ssh-host-prefix presto\. --hostname-suffix testy %h`, + ssh: []string{"Host presto.*", "Match host *.testy !exec"}, }, }, } From 9fe3fd4e2875f96850ab6b473e763cc3dd3e3ccd Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 17 Apr 2025 12:16:29 +0400 Subject: [PATCH 123/384] chore: change config-ssh Call to Action to use suffix (#17445) fixes #16828 With all the recent changes, I believe it is now safe to change the Call to Action for `config-ssh` to use the hostname suffix rather than prefix if it was set. --- cli/configssh.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/configssh.go b/cli/configssh.go index e90c8080abb0d..65f36697d873f 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -457,7 +457,11 @@ func (r *RootCmd) configSSH() *serpent.Command { if len(res.Workspaces) > 0 { _, _ = fmt.Fprintln(out, "You should now be able to ssh into your workspace.") - _, _ = fmt.Fprintf(out, "For example, try running:\n\n\t$ ssh %s%s\n", coderdConfig.HostnamePrefix, res.Workspaces[0].Name) + if configOptions.hostnameSuffix != "" { + _, _ = fmt.Fprintf(out, "For example, try running:\n\n\t$ ssh %s.%s\n", res.Workspaces[0].Name, configOptions.hostnameSuffix) + } else if configOptions.userHostPrefix != "" { + _, _ = fmt.Fprintf(out, "For example, try running:\n\n\t$ ssh %s%s\n", configOptions.userHostPrefix, res.Workspaces[0].Name) + } } else { _, _ = fmt.Fprint(out, "You don't have any workspaces yet, try creating one with:\n\n\t$ coder create \n") } From 67a912796a716c757c9e458bec601ed3f4de367f Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 17 Apr 2025 11:27:18 +0100 Subject: [PATCH 124/384] feat: add slider component (#17431) The slider component is part of the components supported by Dynamic Parameters There are no Figma designs for the slider component. This is based on the shadcn slider. Screenshot 2025-04-16 at 19 26 11 --- site/src/components/Slider/Slider.stories.tsx | 57 +++++++++++++++++++ site/src/components/Slider/Slider.tsx | 39 +++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 site/src/components/Slider/Slider.stories.tsx create mode 100644 site/src/components/Slider/Slider.tsx diff --git a/site/src/components/Slider/Slider.stories.tsx b/site/src/components/Slider/Slider.stories.tsx new file mode 100644 index 0000000000000..480e12c090382 --- /dev/null +++ b/site/src/components/Slider/Slider.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; +import { Slider } from "./Slider"; + +const meta: Meta = { + title: "components/Slider", + component: Slider, + args: {}, + argTypes: { + value: { + control: "number", + description: "The controlled value of the slider", + }, + defaultValue: { + control: "number", + description: "The default value when initially rendered", + }, + disabled: { + control: "boolean", + description: + "When true, prevents the user from interacting with the slider", + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Controlled: Story = { + render: (args) => { + const [value, setValue] = React.useState(50); + return ( + setValue(v)} /> + ); + }, + args: { value: [50], min: 0, max: 100, step: 1 }, +}; + +export const Uncontrolled: Story = { + args: { defaultValue: [30], min: 0, max: 100, step: 1 }, +}; + +export const Disabled: Story = { + args: { defaultValue: [40], disabled: true }, +}; + +export const MultipleThumbs: Story = { + args: { + defaultValue: [20, 80], + min: 0, + max: 100, + step: 5, + minStepsBetweenThumbs: 1, + }, +}; diff --git a/site/src/components/Slider/Slider.tsx b/site/src/components/Slider/Slider.tsx new file mode 100644 index 0000000000000..4fdd21353e963 --- /dev/null +++ b/site/src/components/Slider/Slider.tsx @@ -0,0 +1,39 @@ +/** + * Copied from shadc/ui on 04/16/2025 + * @see {@link https://ui.shadcn.com/docs/components/slider} + */ +import * as SliderPrimitive from "@radix-ui/react-slider"; +import * as React from "react"; + +import { cn } from "utils/cn"; + +export const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + + +)); From daafa0d689518122805ad848cb596035d25ceb3a Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 17 Apr 2025 13:50:18 +0200 Subject: [PATCH 125/384] chore: add missing prometheus tests for UNKNOWN/STATIC paths (#17446) --- coderd/httpmw/prometheus_test.go | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/coderd/httpmw/prometheus_test.go b/coderd/httpmw/prometheus_test.go index d40558f5ca5e7..e05ae53d3836c 100644 --- a/coderd/httpmw/prometheus_test.go +++ b/coderd/httpmw/prometheus_test.go @@ -106,6 +106,64 @@ func TestPrometheus(t *testing.T) { require.Equal(t, "/api/v2/users/{user}", concurrentRequests["path"]) require.Equal(t, "GET", concurrentRequests["method"]) }) + + t.Run("StaticRoute", func(t *testing.T) { + t.Parallel() + reg := prometheus.NewRegistry() + promMW := httpmw.Prometheus(reg) + + r := chi.NewRouter() + r.Use(promMW) + r.NotFound(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }) + r.Get("/static/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + req := httptest.NewRequest("GET", "/static/bundle.js", nil) + sw := &tracing.StatusWriter{ResponseWriter: httptest.NewRecorder()} + + r.ServeHTTP(sw, req) + + metrics, err := reg.Gather() + require.NoError(t, err) + require.Greater(t, len(metrics), 0) + metricLabels := getMetricLabels(metrics) + + reqProcessed, ok := metricLabels["coderd_api_requests_processed_total"] + require.True(t, ok, "coderd_api_requests_processed_total metric not found") + require.Equal(t, "STATIC", reqProcessed["path"]) + require.Equal(t, "GET", reqProcessed["method"]) + }) + + t.Run("UnknownRoute", func(t *testing.T) { + t.Parallel() + reg := prometheus.NewRegistry() + promMW := httpmw.Prometheus(reg) + + r := chi.NewRouter() + r.Use(promMW) + r.NotFound(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }) + r.Get("/api/v2/users/{user}", func(w http.ResponseWriter, r *http.Request) {}) + + req := httptest.NewRequest("GET", "/api/v2/weird_path", nil) + sw := &tracing.StatusWriter{ResponseWriter: httptest.NewRecorder()} + + r.ServeHTTP(sw, req) + + metrics, err := reg.Gather() + require.NoError(t, err) + require.Greater(t, len(metrics), 0) + metricLabels := getMetricLabels(metrics) + + reqProcessed, ok := metricLabels["coderd_api_requests_processed_total"] + require.True(t, ok, "coderd_api_requests_processed_total metric not found") + require.Equal(t, "UNKNOWN", reqProcessed["path"]) + require.Equal(t, "GET", reqProcessed["method"]) + }) } func getMetricLabels(metrics []*cm.MetricFamily) map[string]map[string]string { From b3aba6dab7fcba75a2f33b1f071051ce081ea737 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 17 Apr 2025 16:17:19 +0400 Subject: [PATCH 126/384] test: ignore context.Canceled in acquireWithCancel (#17448) fixes https://github.com/coder/internal/issues/584 Ignore canceled error when sending an acquired job, since dRPC is racy and will sometimes return this error even after successfully sending the job, if the test is quickly finished. --- provisionerd/provisionerd_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/provisionerd/provisionerd_test.go b/provisionerd/provisionerd_test.go index 8d5ba1621b8b7..c711e0d4925c8 100644 --- a/provisionerd/provisionerd_test.go +++ b/provisionerd/provisionerd_test.go @@ -1270,6 +1270,11 @@ func (a *acquireOne) acquireWithCancel(stream proto.DRPCProvisionerDaemon_Acquir return nil } err := stream.Send(a.job) - assert.NoError(a.t, err) + // dRPC is racy, and sometimes will return context.Canceled after it has successfully sent the message if we cancel + // right away, e.g. in unit tests that complete. So, just swallow the error in that case. If we are canceled before + // the job was acquired, presumably something else in the test will have failed. + if !xerrors.Is(err, context.Canceled) { + assert.NoError(a.t, err) + } return nil } From 6a79965948d49f3e9b0133b7994c953f382b6354 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 17 Apr 2025 13:50:51 +0100 Subject: [PATCH 127/384] fix(agent/agentcontainers): handle race between docker ps and docker inspect (#17447) Fixes https://github.com/coder/internal/issues/586#event-17291038671 --- agent/agentcontainers/containers_dockercli.go | 31 ++++++++++--------- agent/agentcontainers/devcontainercli_test.go | 3 ++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/agent/agentcontainers/containers_dockercli.go b/agent/agentcontainers/containers_dockercli.go index 208c3ec2ea89b..d5499f6b1af2b 100644 --- a/agent/agentcontainers/containers_dockercli.go +++ b/agent/agentcontainers/containers_dockercli.go @@ -24,19 +24,6 @@ import ( "github.com/coder/coder/v2/codersdk" ) -// DockerCLILister is a ContainerLister that lists containers using the docker CLI -type DockerCLILister struct { - execer agentexec.Execer -} - -var _ Lister = &DockerCLILister{} - -func NewDocker(execer agentexec.Execer) Lister { - return &DockerCLILister{ - execer: agentexec.DefaultExecer, - } -} - // DockerEnvInfoer is an implementation of agentssh.EnvInfoer that returns // information about a container. type DockerEnvInfoer struct { @@ -241,6 +228,19 @@ func run(ctx context.Context, execer agentexec.Execer, cmd string, args ...strin return stdout, stderr, err } +// DockerCLILister is a ContainerLister that lists containers using the docker CLI +type DockerCLILister struct { + execer agentexec.Execer +} + +var _ Lister = &DockerCLILister{} + +func NewDocker(execer agentexec.Execer) Lister { + return &DockerCLILister{ + execer: agentexec.DefaultExecer, + } +} + func (dcl *DockerCLILister) List(ctx context.Context) (codersdk.WorkspaceAgentListContainersResponse, error) { var stdoutBuf, stderrBuf bytes.Buffer // List all container IDs, one per line, with no truncation @@ -319,9 +319,12 @@ func runDockerInspect(ctx context.Context, execer agentexec.Execer, ids ...strin stdout = bytes.TrimSpace(stdoutBuf.Bytes()) stderr = bytes.TrimSpace(stderrBuf.Bytes()) if err != nil { + if bytes.Contains(stderr, []byte("No such object:")) { + // This can happen if a container is deleted between the time we check for its existence and the time we inspect it. + return stdout, stderr, nil + } return stdout, stderr, err } - return stdout, stderr, nil } diff --git a/agent/agentcontainers/devcontainercli_test.go b/agent/agentcontainers/devcontainercli_test.go index 22a81fb8e38a2..d768b997cc1e1 100644 --- a/agent/agentcontainers/devcontainercli_test.go +++ b/agent/agentcontainers/devcontainercli_test.go @@ -229,6 +229,9 @@ func TestDockerDevcontainerCLI(t *testing.T) { if os.Getenv("CODER_TEST_USE_DOCKER") != "1" { t.Skip("skipping Docker test; set CODER_TEST_USE_DOCKER=1 to run") } + if _, err := exec.LookPath("devcontainer"); err != nil { + t.Fatal("this test requires the devcontainer CLI: npm install -g @devcontainers/cli") + } // Connect to Docker. pool, err := dockertest.NewPool("") From 27bc60d1b9eae069dfe63eb468f8f719751931ef Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Thu, 17 Apr 2025 09:29:29 -0400 Subject: [PATCH 128/384] feat: implement reconciliation loop (#17261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/coder/internal/issues/510
    Refactoring Summary ### 1) `CalculateActions` Function #### Issues Before Refactoring: - Large function (~150 lines), making it difficult to read and maintain. - The control flow is hard to follow due to complex conditional logic. - The `ReconciliationActions` struct was partially initialized early, then mutated in multiple places, making the flow error-prone. Original source: https://github.com/coder/coder/blob/fe60b569ad754245e28bac71e0ef3c83536631bb/coderd/prebuilds/state.go#L13-L167 #### Improvements After Refactoring: - Simplified and broken down into smaller, focused helper methods. - The flow of the function is now more linear and easier to understand. - Struct initialization is cleaner, avoiding partial and incremental mutations. Refactored function: https://github.com/coder/coder/blob/eeb0407d783cdda71ec2418c113f325542c47b1c/coderd/prebuilds/state.go#L67-L84 --- ### 2) `ReconciliationActions` Struct #### Issues Before Refactoring: - The struct mixed both actionable decisions and diagnostic state, which blurred its purpose. - It was unclear which fields were necessary for reconciliation logic, and which were purely for logging/observability. #### Improvements After Refactoring: - Split into two clear, purpose-specific structs: - **`ReconciliationActions`** — defines the intended reconciliation action. - **`ReconciliationState`** — captures runtime state and metadata, primarily for logging and diagnostics. Original struct: https://github.com/coder/coder/blob/fe60b569ad754245e28bac71e0ef3c83536631bb/coderd/prebuilds/reconcile.go#L29-L41
    --------- Signed-off-by: Danny Kopping Co-authored-by: Sas Swart Co-authored-by: Danny Kopping Co-authored-by: Dean Sheather Co-authored-by: Spike Curtis Co-authored-by: Danny Kopping --- coderd/database/lock.go | 3 +- coderd/database/querier.go | 2 +- coderd/database/queries.sql.go | 8 +- coderd/database/queries/prebuilds.sql | 6 +- coderd/prebuilds/api.go | 27 + coderd/prebuilds/global_snapshot.go | 66 ++ coderd/prebuilds/noop.go | 35 + coderd/prebuilds/preset_snapshot.go | 254 ++++ coderd/prebuilds/preset_snapshot_test.go | 758 ++++++++++++ coderd/prebuilds/util.go | 26 + coderd/util/slice/slice.go | 11 + coderd/util/slice/slice_test.go | 59 + codersdk/deployment.go | 13 + enterprise/coderd/prebuilds/reconcile.go | 541 +++++++++ enterprise/coderd/prebuilds/reconcile_test.go | 1027 +++++++++++++++++ site/src/api/typesGenerated.ts | 7 + 16 files changed, 2834 insertions(+), 9 deletions(-) create mode 100644 coderd/prebuilds/api.go create mode 100644 coderd/prebuilds/global_snapshot.go create mode 100644 coderd/prebuilds/noop.go create mode 100644 coderd/prebuilds/preset_snapshot.go create mode 100644 coderd/prebuilds/preset_snapshot_test.go create mode 100644 coderd/prebuilds/util.go create mode 100644 enterprise/coderd/prebuilds/reconcile.go create mode 100644 enterprise/coderd/prebuilds/reconcile_test.go diff --git a/coderd/database/lock.go b/coderd/database/lock.go index 7ccb3b8f56fec..e5091cdfd29cc 100644 --- a/coderd/database/lock.go +++ b/coderd/database/lock.go @@ -12,8 +12,7 @@ const ( LockIDDBPurge LockIDNotificationsReportGenerator LockIDCryptoKeyRotation - LockIDReconcileTemplatePrebuilds - LockIDDeterminePrebuildsState + LockIDReconcilePrebuilds ) // GenLockID generates a unique and consistent lock ID from a given string. diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 1cef5ada197f5..9fbfbde410d40 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -64,7 +64,7 @@ type sqlcQuerier interface { CleanTailnetCoordinators(ctx context.Context) error CleanTailnetLostPeers(ctx context.Context) error CleanTailnetTunnels(ctx context.Context) error - // CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by template version ID and transition. + // CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by preset ID and transition. // Prebuild considered in-progress if it's in the "starting", "stopping", or "deleting" state. CountInProgressPrebuilds(ctx context.Context) ([]CountInProgressPrebuildsRow, error) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 72f2c4a8fcb8e..60416b1a35730 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5938,7 +5938,7 @@ func (q *sqlQuerier) ClaimPrebuiltWorkspace(ctx context.Context, arg ClaimPrebui } const countInProgressPrebuilds = `-- name: CountInProgressPrebuilds :many -SELECT t.id AS template_id, wpb.template_version_id, wpb.transition, COUNT(wpb.transition)::int AS count +SELECT t.id AS template_id, wpb.template_version_id, wpb.transition, COUNT(wpb.transition)::int AS count, wlb.template_version_preset_id as preset_id FROM workspace_latest_builds wlb INNER JOIN workspace_prebuild_builds wpb ON wpb.id = wlb.id -- We only need these counts for active template versions. @@ -5949,7 +5949,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) -GROUP BY t.id, wpb.template_version_id, wpb.transition +GROUP BY t.id, wpb.template_version_id, wpb.transition, wlb.template_version_preset_id ` type CountInProgressPrebuildsRow struct { @@ -5957,9 +5957,10 @@ type CountInProgressPrebuildsRow struct { TemplateVersionID uuid.UUID `db:"template_version_id" json:"template_version_id"` Transition WorkspaceTransition `db:"transition" json:"transition"` Count int32 `db:"count" json:"count"` + PresetID uuid.NullUUID `db:"preset_id" json:"preset_id"` } -// CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by template version ID and transition. +// CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by preset ID and transition. // Prebuild considered in-progress if it's in the "starting", "stopping", or "deleting" state. func (q *sqlQuerier) CountInProgressPrebuilds(ctx context.Context) ([]CountInProgressPrebuildsRow, error) { rows, err := q.db.QueryContext(ctx, countInProgressPrebuilds) @@ -5975,6 +5976,7 @@ func (q *sqlQuerier) CountInProgressPrebuilds(ctx context.Context) ([]CountInPro &i.TemplateVersionID, &i.Transition, &i.Count, + &i.PresetID, ); err != nil { return nil, err } diff --git a/coderd/database/queries/prebuilds.sql b/coderd/database/queries/prebuilds.sql index 53f5020f3607e..1d3a827c98586 100644 --- a/coderd/database/queries/prebuilds.sql +++ b/coderd/database/queries/prebuilds.sql @@ -57,9 +57,9 @@ WHERE (b.transition = 'start'::workspace_transition AND b.job_status = 'succeeded'::provisioner_job_status); -- name: CountInProgressPrebuilds :many --- CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by template version ID and transition. +-- CountInProgressPrebuilds returns the number of in-progress prebuilds, grouped by preset ID and transition. -- Prebuild considered in-progress if it's in the "starting", "stopping", or "deleting" state. -SELECT t.id AS template_id, wpb.template_version_id, wpb.transition, COUNT(wpb.transition)::int AS count +SELECT t.id AS template_id, wpb.template_version_id, wpb.transition, COUNT(wpb.transition)::int AS count, wlb.template_version_preset_id as preset_id FROM workspace_latest_builds wlb INNER JOIN workspace_prebuild_builds wpb ON wpb.id = wlb.id -- We only need these counts for active template versions. @@ -70,7 +70,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) -GROUP BY t.id, wpb.template_version_id, wpb.transition; +GROUP BY t.id, wpb.template_version_id, wpb.transition, wlb.template_version_preset_id; -- GetPresetsBackoff groups workspace builds by preset ID. -- Each preset is associated with exactly one template version ID. diff --git a/coderd/prebuilds/api.go b/coderd/prebuilds/api.go new file mode 100644 index 0000000000000..6ebfb8acced44 --- /dev/null +++ b/coderd/prebuilds/api.go @@ -0,0 +1,27 @@ +package prebuilds + +import ( + "context" +) + +// ReconciliationOrchestrator manages the lifecycle of prebuild reconciliation. +// It runs a continuous loop to check and reconcile prebuild states, and can be stopped gracefully. +type ReconciliationOrchestrator interface { + Reconciler + + // RunLoop starts a continuous reconciliation loop that periodically calls ReconcileAll + // to ensure all prebuilds are in their desired states. The loop runs until the context + // is canceled or Stop is called. + RunLoop(ctx context.Context) + + // 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) +} + +type Reconciler interface { + // ReconcileAll orchestrates the reconciliation of all prebuilds across all templates. + // It takes a global snapshot of the system state and then reconciles each preset + // in parallel, creating or deleting prebuilds as needed to reach their desired states. + ReconcileAll(ctx context.Context) error +} diff --git a/coderd/prebuilds/global_snapshot.go b/coderd/prebuilds/global_snapshot.go new file mode 100644 index 0000000000000..0cf3fa3facc3a --- /dev/null +++ b/coderd/prebuilds/global_snapshot.go @@ -0,0 +1,66 @@ +package prebuilds + +import ( + "github.com/google/uuid" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/util/slice" +) + +// GlobalSnapshot represents a full point-in-time snapshot of state relating to prebuilds across all templates. +type GlobalSnapshot struct { + Presets []database.GetTemplatePresetsWithPrebuildsRow + RunningPrebuilds []database.GetRunningPrebuiltWorkspacesRow + PrebuildsInProgress []database.CountInProgressPrebuildsRow + Backoffs []database.GetPresetsBackoffRow +} + +func NewGlobalSnapshot( + presets []database.GetTemplatePresetsWithPrebuildsRow, + runningPrebuilds []database.GetRunningPrebuiltWorkspacesRow, + prebuildsInProgress []database.CountInProgressPrebuildsRow, + backoffs []database.GetPresetsBackoffRow, +) GlobalSnapshot { + return GlobalSnapshot{ + Presets: presets, + RunningPrebuilds: runningPrebuilds, + PrebuildsInProgress: prebuildsInProgress, + Backoffs: backoffs, + } +} + +func (s GlobalSnapshot) FilterByPreset(presetID uuid.UUID) (*PresetSnapshot, error) { + preset, found := slice.Find(s.Presets, func(preset database.GetTemplatePresetsWithPrebuildsRow) bool { + return preset.ID == presetID + }) + if !found { + return nil, xerrors.Errorf("no preset found with ID %q", presetID) + } + + running := slice.Filter(s.RunningPrebuilds, func(prebuild database.GetRunningPrebuiltWorkspacesRow) bool { + if !prebuild.CurrentPresetID.Valid { + return false + } + return prebuild.CurrentPresetID.UUID == preset.ID + }) + + inProgress := slice.Filter(s.PrebuildsInProgress, func(prebuild database.CountInProgressPrebuildsRow) bool { + return prebuild.PresetID.UUID == preset.ID + }) + + var backoffPtr *database.GetPresetsBackoffRow + backoff, found := slice.Find(s.Backoffs, func(row database.GetPresetsBackoffRow) bool { + return row.PresetID == preset.ID + }) + if found { + backoffPtr = &backoff + } + + return &PresetSnapshot{ + Preset: preset, + Running: running, + InProgress: inProgress, + Backoff: backoffPtr, + }, nil +} diff --git a/coderd/prebuilds/noop.go b/coderd/prebuilds/noop.go new file mode 100644 index 0000000000000..ffe4e7b442af9 --- /dev/null +++ b/coderd/prebuilds/noop.go @@ -0,0 +1,35 @@ +package prebuilds + +import ( + "context" + + "github.com/coder/coder/v2/coderd/database" +) + +type NoopReconciler struct{} + +func NewNoopReconciler() *NoopReconciler { + return &NoopReconciler{} +} + +func (NoopReconciler) RunLoop(context.Context) {} + +func (NoopReconciler) Stop(context.Context, error) {} + +func (NoopReconciler) ReconcileAll(context.Context) error { + return nil +} + +func (NoopReconciler) SnapshotState(context.Context, database.Store) (*GlobalSnapshot, error) { + return &GlobalSnapshot{}, nil +} + +func (NoopReconciler) ReconcilePreset(context.Context, PresetSnapshot) error { + return nil +} + +func (NoopReconciler) CalculateActions(context.Context, PresetSnapshot) (*ReconciliationActions, error) { + return &ReconciliationActions{}, nil +} + +var _ ReconciliationOrchestrator = NoopReconciler{} diff --git a/coderd/prebuilds/preset_snapshot.go b/coderd/prebuilds/preset_snapshot.go new file mode 100644 index 0000000000000..b6f05e588a6c0 --- /dev/null +++ b/coderd/prebuilds/preset_snapshot.go @@ -0,0 +1,254 @@ +package prebuilds + +import ( + "slices" + "time" + + "github.com/google/uuid" + + "github.com/coder/quartz" + + "github.com/coder/coder/v2/coderd/database" +) + +// ActionType represents the type of action needed to reconcile prebuilds. +type ActionType int + +const ( + // ActionTypeUndefined represents an uninitialized or invalid action type. + ActionTypeUndefined ActionType = iota + + // ActionTypeCreate indicates that new prebuilds should be created. + ActionTypeCreate + + // ActionTypeDelete indicates that existing prebuilds should be deleted. + ActionTypeDelete + + // ActionTypeBackoff indicates that prebuild creation should be delayed. + ActionTypeBackoff +) + +// PresetSnapshot is a filtered view of GlobalSnapshot focused on a single preset. +// It contains the raw data needed to calculate the current state of a preset's prebuilds, +// including running prebuilds, in-progress builds, and backoff information. +type PresetSnapshot struct { + Preset database.GetTemplatePresetsWithPrebuildsRow + Running []database.GetRunningPrebuiltWorkspacesRow + InProgress []database.CountInProgressPrebuildsRow + Backoff *database.GetPresetsBackoffRow +} + +// ReconciliationState represents the processed state of a preset's prebuilds, +// calculated from a PresetSnapshot. While PresetSnapshot contains raw data, +// ReconciliationState contains derived metrics that are directly used to +// determine what actions are needed (create, delete, or backoff). +// For example, it calculates how many prebuilds are eligible, how many are +// extraneous, and how many are in various transition states. +type ReconciliationState struct { + Actual int32 // Number of currently running prebuilds + Desired int32 // Number of prebuilds desired as defined in the preset + Eligible int32 // Number of prebuilds that are ready to be claimed + Extraneous int32 // Number of extra running prebuilds beyond the desired count + + // Counts of prebuilds in various transition states + Starting int32 + Stopping int32 + Deleting int32 +} + +// ReconciliationActions represents actions needed to reconcile the current state with the desired state. +// Based on ActionType, exactly one of Create, DeleteIDs, or BackoffUntil will be set. +type ReconciliationActions struct { + // ActionType determines which field is set and what action should be taken + ActionType ActionType + + // Create is set when ActionType is ActionTypeCreate and indicates the number of prebuilds to create + Create int32 + + // DeleteIDs is set when ActionType is ActionTypeDelete and contains the IDs of prebuilds to delete + DeleteIDs []uuid.UUID + + // BackoffUntil is set when ActionType is ActionTypeBackoff and indicates when to retry creating prebuilds + BackoffUntil time.Time +} + +// CalculateState computes the current state of prebuilds for a preset, including: +// - Actual: Number of currently running prebuilds +// - Desired: Number of prebuilds desired as defined in the preset +// - Eligible: Number of prebuilds that are ready to be claimed +// - Extraneous: Number of extra running prebuilds beyond the desired count +// - Starting/Stopping/Deleting: Counts of prebuilds in various transition states +// +// The function takes into account whether the preset is active (using the active template version) +// and calculates appropriate counts based on the current state of running prebuilds and +// in-progress transitions. This state information is used to determine what reconciliation +// actions are needed to reach the desired state. +func (p PresetSnapshot) CalculateState() *ReconciliationState { + var ( + actual int32 + desired int32 + eligible int32 + extraneous int32 + ) + + if p.isActive() { + // #nosec G115 - Safe conversion as p.Running slice length is expected to be within int32 range + actual = int32(len(p.Running)) + desired = p.Preset.DesiredInstances.Int32 + eligible = p.countEligible() + extraneous = max(actual-desired, 0) + } + + starting, stopping, deleting := p.countInProgress() + + return &ReconciliationState{ + Actual: actual, + Desired: desired, + Eligible: eligible, + Extraneous: extraneous, + + Starting: starting, + Stopping: stopping, + Deleting: deleting, + } +} + +// CalculateActions determines what actions are needed to reconcile the current state with the desired state. +// The function: +// 1. First checks if a backoff period is needed (if previous builds failed) +// 2. If the preset is inactive (template version is not active), it will delete all running prebuilds +// 3. For active presets, it calculates the number of prebuilds to create or delete based on: +// - The desired number of instances +// - Currently running prebuilds +// - Prebuilds in transition states (starting/stopping/deleting) +// - Any extraneous prebuilds that need to be removed +// +// The function returns a ReconciliationActions struct that will have exactly one action type set: +// - ActionTypeBackoff: Only BackoffUntil is set, indicating when to retry +// - ActionTypeCreate: Only Create is set, indicating how many prebuilds to create +// - ActionTypeDelete: Only DeleteIDs is set, containing IDs of prebuilds to delete +func (p PresetSnapshot) CalculateActions(clock quartz.Clock, backoffInterval time.Duration) (*ReconciliationActions, error) { + // TODO: align workspace states with how we represent them on the FE and the CLI + // right now there's some slight differences which can lead to additional prebuilds being created + + // TODO: add mechanism to prevent prebuilds being reconciled from being claimable by users; i.e. if a prebuild is + // about to be deleted, it should not be deleted if it has been claimed - beware of TOCTOU races! + + actions, needsBackoff := p.needsBackoffPeriod(clock, backoffInterval) + if needsBackoff { + return actions, nil + } + + if !p.isActive() { + return p.handleInactiveTemplateVersion() + } + + return p.handleActiveTemplateVersion() +} + +// isActive returns true if the preset's template version is the active version, and it is neither deleted nor deprecated. +// This determines whether we should maintain prebuilds for this preset or delete them. +func (p PresetSnapshot) isActive() bool { + return p.Preset.UsingActiveVersion && !p.Preset.Deleted && !p.Preset.Deprecated +} + +// handleActiveTemplateVersion deletes excess prebuilds if there are too many, +// otherwise creates new ones to reach the desired count. +func (p PresetSnapshot) handleActiveTemplateVersion() (*ReconciliationActions, error) { + state := p.CalculateState() + + // If we have more prebuilds than desired, delete the oldest ones + if state.Extraneous > 0 { + return &ReconciliationActions{ + ActionType: ActionTypeDelete, + DeleteIDs: p.getOldestPrebuildIDs(int(state.Extraneous)), + }, nil + } + + // Calculate how many new prebuilds we need to create + // We subtract starting prebuilds since they're already being created + prebuildsToCreate := max(state.Desired-state.Actual-state.Starting, 0) + + return &ReconciliationActions{ + ActionType: ActionTypeCreate, + Create: prebuildsToCreate, + }, nil +} + +// handleInactiveTemplateVersion deletes all running prebuilds except those already being deleted +// to avoid duplicate deletion attempts. +func (p PresetSnapshot) handleInactiveTemplateVersion() (*ReconciliationActions, error) { + prebuildsToDelete := len(p.Running) + deleteIDs := p.getOldestPrebuildIDs(prebuildsToDelete) + + return &ReconciliationActions{ + ActionType: ActionTypeDelete, + DeleteIDs: deleteIDs, + }, nil +} + +// needsBackoffPeriod checks if we should delay prebuild creation due to recent failures. +// If there were failures, it calculates a backoff period based on the number of failures +// and returns true if we're still within that period. +func (p PresetSnapshot) needsBackoffPeriod(clock quartz.Clock, backoffInterval time.Duration) (*ReconciliationActions, bool) { + if p.Backoff == nil || p.Backoff.NumFailed == 0 { + return nil, false + } + backoffUntil := p.Backoff.LastBuildAt.Add(time.Duration(p.Backoff.NumFailed) * backoffInterval) + if clock.Now().After(backoffUntil) { + return nil, false + } + + return &ReconciliationActions{ + ActionType: ActionTypeBackoff, + BackoffUntil: backoffUntil, + }, true +} + +// countEligible returns the number of prebuilds that are ready to be claimed. +// A prebuild is eligible if it's running and its agents are in ready state. +func (p PresetSnapshot) countEligible() int32 { + var count int32 + for _, prebuild := range p.Running { + if prebuild.Ready { + count++ + } + } + return count +} + +// countInProgress returns counts of prebuilds in transition states (starting, stopping, deleting). +// These counts are tracked at the template level, so all presets sharing the same template see the same values. +func (p PresetSnapshot) countInProgress() (starting int32, stopping int32, deleting int32) { + for _, progress := range p.InProgress { + num := progress.Count + switch progress.Transition { + case database.WorkspaceTransitionStart: + starting += num + case database.WorkspaceTransitionStop: + stopping += num + case database.WorkspaceTransitionDelete: + deleting += num + } + } + + return starting, stopping, deleting +} + +// getOldestPrebuildIDs returns the IDs of the N oldest prebuilds, sorted by creation time. +// This is used when we need to delete prebuilds, ensuring we remove the oldest ones first. +func (p PresetSnapshot) getOldestPrebuildIDs(n int) []uuid.UUID { + // Sort by creation time, oldest first + slices.SortFunc(p.Running, func(a, b database.GetRunningPrebuiltWorkspacesRow) int { + return a.CreatedAt.Compare(b.CreatedAt) + }) + + // Take the first N IDs + n = min(n, len(p.Running)) + ids := make([]uuid.UUID, n) + for i := 0; i < n; i++ { + ids[i] = p.Running[i].ID + } + + return ids +} diff --git a/coderd/prebuilds/preset_snapshot_test.go b/coderd/prebuilds/preset_snapshot_test.go new file mode 100644 index 0000000000000..cce8ea67cb05c --- /dev/null +++ b/coderd/prebuilds/preset_snapshot_test.go @@ -0,0 +1,758 @@ +package prebuilds_test + +import ( + "database/sql" + "fmt" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/coder/quartz" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/prebuilds" +) + +type options struct { + templateID uuid.UUID + templateVersionID uuid.UUID + presetID uuid.UUID + presetName string + prebuiltWorkspaceID uuid.UUID + workspaceName string +} + +// templateID is common across all option sets. +var templateID = uuid.UUID{1} + +const ( + backoffInterval = time.Second * 5 + + optionSet0 = iota + optionSet1 + optionSet2 +) + +var opts = map[uint]options{ + optionSet0: { + templateID: templateID, + templateVersionID: uuid.UUID{11}, + presetID: uuid.UUID{12}, + presetName: "my-preset", + prebuiltWorkspaceID: uuid.UUID{13}, + workspaceName: "prebuilds0", + }, + optionSet1: { + templateID: templateID, + templateVersionID: uuid.UUID{21}, + presetID: uuid.UUID{22}, + presetName: "my-preset", + prebuiltWorkspaceID: uuid.UUID{23}, + workspaceName: "prebuilds1", + }, + optionSet2: { + templateID: templateID, + templateVersionID: uuid.UUID{31}, + presetID: uuid.UUID{32}, + presetName: "my-preset", + prebuiltWorkspaceID: uuid.UUID{33}, + workspaceName: "prebuilds2", + }, +} + +// A new template version with a preset without prebuilds configured should result in no prebuilds being created. +func TestNoPrebuilds(t *testing.T) { + t.Parallel() + current := opts[optionSet0] + clock := quartz.NewMock(t) + + presets := []database.GetTemplatePresetsWithPrebuildsRow{ + preset(true, 0, current), + } + + snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil) + ps, err := snapshot.FilterByPreset(current.presetID) + require.NoError(t, err) + + state := ps.CalculateState() + actions, err := ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + + validateState(t, prebuilds.ReconciliationState{ /*all zero values*/ }, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 0, + }, *actions) +} + +// A new template version with a preset with prebuilds configured should result in a new prebuild being created. +func TestNetNew(t *testing.T) { + t.Parallel() + current := opts[optionSet0] + clock := quartz.NewMock(t) + + presets := []database.GetTemplatePresetsWithPrebuildsRow{ + preset(true, 1, current), + } + + snapshot := prebuilds.NewGlobalSnapshot(presets, nil, nil, nil) + ps, err := snapshot.FilterByPreset(current.presetID) + require.NoError(t, err) + + state := ps.CalculateState() + actions, err := ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + + validateState(t, prebuilds.ReconciliationState{ + Desired: 1, + }, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 1, + }, *actions) +} + +// A new template version is created with a preset with prebuilds configured; this outdates the older version and +// requires the old prebuilds to be destroyed and new prebuilds to be created. +func TestOutdatedPrebuilds(t *testing.T) { + t.Parallel() + outdated := opts[optionSet0] + current := opts[optionSet1] + clock := quartz.NewMock(t) + + // GIVEN: 2 presets, one outdated and one new. + presets := []database.GetTemplatePresetsWithPrebuildsRow{ + preset(false, 1, outdated), + preset(true, 1, current), + } + + // GIVEN: a running prebuild for the outdated preset. + running := []database.GetRunningPrebuiltWorkspacesRow{ + prebuiltWorkspace(outdated, clock), + } + + // GIVEN: no in-progress builds. + var inProgress []database.CountInProgressPrebuildsRow + + // WHEN: calculating the outdated preset's state. + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + ps, err := snapshot.FilterByPreset(outdated.presetID) + require.NoError(t, err) + + // THEN: we should identify that this prebuild is outdated and needs to be deleted. + state := ps.CalculateState() + actions, err := ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + validateState(t, prebuilds.ReconciliationState{}, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeDelete, + DeleteIDs: []uuid.UUID{outdated.prebuiltWorkspaceID}, + }, *actions) + + // WHEN: calculating the current preset's state. + ps, err = snapshot.FilterByPreset(current.presetID) + require.NoError(t, err) + + // THEN: we should not be blocked from creating a new prebuild while the outdate one deletes. + state = ps.CalculateState() + actions, err = ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + validateState(t, prebuilds.ReconciliationState{Desired: 1}, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 1, + }, *actions) +} + +// Make sure that outdated prebuild will be deleted, even if deletion of another outdated prebuild is already in progress. +func TestDeleteOutdatedPrebuilds(t *testing.T) { + t.Parallel() + outdated := opts[optionSet0] + clock := quartz.NewMock(t) + + // GIVEN: 1 outdated preset. + presets := []database.GetTemplatePresetsWithPrebuildsRow{ + preset(false, 1, outdated), + } + + // GIVEN: one running prebuild for the outdated preset. + running := []database.GetRunningPrebuiltWorkspacesRow{ + prebuiltWorkspace(outdated, clock), + } + + // GIVEN: one deleting prebuild for the outdated preset. + inProgress := []database.CountInProgressPrebuildsRow{ + { + TemplateID: outdated.templateID, + TemplateVersionID: outdated.templateVersionID, + Transition: database.WorkspaceTransitionDelete, + Count: 1, + PresetID: uuid.NullUUID{ + UUID: outdated.presetID, + Valid: true, + }, + }, + } + + // WHEN: calculating the outdated preset's state. + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + ps, err := snapshot.FilterByPreset(outdated.presetID) + require.NoError(t, err) + + // THEN: we should identify that this prebuild is outdated and needs to be deleted. + // Despite the fact that deletion of another outdated prebuild is already in progress. + state := ps.CalculateState() + actions, err := ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + validateState(t, prebuilds.ReconciliationState{ + Deleting: 1, + }, *state) + + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeDelete, + DeleteIDs: []uuid.UUID{outdated.prebuiltWorkspaceID}, + }, *actions) +} + +// A new template version is created with a preset with prebuilds configured; while a prebuild is provisioning up or down, +// the calculated actions should indicate the state correctly. +func TestInProgressActions(t *testing.T) { + t.Parallel() + current := opts[optionSet0] + clock := quartz.NewMock(t) + + cases := []struct { + name string + transition database.WorkspaceTransition + desired int32 + running int32 + inProgress int32 + checkFn func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) + }{ + // With no running prebuilds and one starting, no creations/deletions should take place. + { + name: fmt.Sprintf("%s-short", database.WorkspaceTransitionStart), + transition: database.WorkspaceTransitionStart, + desired: 1, + running: 0, + inProgress: 1, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Desired: 1, Starting: 1}, state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + }, actions) + }, + }, + // With one running prebuild and one starting, no creations/deletions should occur since we're approaching the correct state. + { + name: fmt.Sprintf("%s-balanced", database.WorkspaceTransitionStart), + transition: database.WorkspaceTransitionStart, + desired: 2, + running: 1, + inProgress: 1, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Actual: 1, Desired: 2, Starting: 1}, state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + }, actions) + }, + }, + // With one running prebuild and one starting, no creations/deletions should occur + // SIDE-NOTE: once the starting prebuild completes, the older of the two will be considered extraneous since we only desire 2. + { + name: fmt.Sprintf("%s-extraneous", database.WorkspaceTransitionStart), + transition: database.WorkspaceTransitionStart, + desired: 2, + running: 2, + inProgress: 1, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 2, Starting: 1}, state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + }, actions) + }, + }, + // With one prebuild desired and one stopping, a new prebuild will be created. + { + name: fmt.Sprintf("%s-short", database.WorkspaceTransitionStop), + transition: database.WorkspaceTransitionStop, + desired: 1, + running: 0, + inProgress: 1, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Desired: 1, Stopping: 1}, state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 1, + }, actions) + }, + }, + // With 3 prebuilds desired, 2 running, and 1 stopping, a new prebuild will be created. + { + name: fmt.Sprintf("%s-balanced", database.WorkspaceTransitionStop), + transition: database.WorkspaceTransitionStop, + desired: 3, + running: 2, + inProgress: 1, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 3, Stopping: 1}, state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 1, + }, actions) + }, + }, + // With 3 prebuilds desired, 3 running, and 1 stopping, no creations/deletions should occur since the desired state is already achieved. + { + name: fmt.Sprintf("%s-extraneous", database.WorkspaceTransitionStop), + transition: database.WorkspaceTransitionStop, + desired: 3, + running: 3, + inProgress: 1, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Actual: 3, Desired: 3, Stopping: 1}, state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + }, actions) + }, + }, + // With one prebuild desired and one deleting, a new prebuild will be created. + { + name: fmt.Sprintf("%s-short", database.WorkspaceTransitionDelete), + transition: database.WorkspaceTransitionDelete, + desired: 1, + running: 0, + inProgress: 1, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Desired: 1, Deleting: 1}, state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 1, + }, actions) + }, + }, + // With 2 prebuilds desired, 1 running, and 1 deleting, a new prebuild will be created. + { + name: fmt.Sprintf("%s-balanced", database.WorkspaceTransitionDelete), + transition: database.WorkspaceTransitionDelete, + desired: 2, + running: 1, + inProgress: 1, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Actual: 1, Desired: 2, Deleting: 1}, state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 1, + }, actions) + }, + }, + // With 2 prebuilds desired, 2 running, and 1 deleting, no creations/deletions should occur since the desired state is already achieved. + { + name: fmt.Sprintf("%s-extraneous", database.WorkspaceTransitionDelete), + transition: database.WorkspaceTransitionDelete, + desired: 2, + running: 2, + inProgress: 1, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Actual: 2, Desired: 2, Deleting: 1}, state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + }, actions) + }, + }, + // With 3 prebuilds desired, 1 running, and 2 starting, no creations should occur since the builds are in progress. + { + name: fmt.Sprintf("%s-inhibit", database.WorkspaceTransitionStart), + transition: database.WorkspaceTransitionStart, + desired: 3, + running: 1, + inProgress: 2, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + validateState(t, prebuilds.ReconciliationState{Actual: 1, Desired: 3, Starting: 2}, state) + validateActions(t, prebuilds.ReconciliationActions{ActionType: prebuilds.ActionTypeCreate, Create: 0}, actions) + }, + }, + // With 3 prebuilds desired, 5 running, and 2 deleting, no deletions should occur since the builds are in progress. + { + name: fmt.Sprintf("%s-inhibit", database.WorkspaceTransitionDelete), + transition: database.WorkspaceTransitionDelete, + desired: 3, + running: 5, + inProgress: 2, + checkFn: func(state prebuilds.ReconciliationState, actions prebuilds.ReconciliationActions) { + expectedState := prebuilds.ReconciliationState{Actual: 5, Desired: 3, Deleting: 2, Extraneous: 2} + expectedActions := prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeDelete, + } + + validateState(t, expectedState, state) + assert.EqualValuesf(t, expectedActions.ActionType, actions.ActionType, "'ActionType' did not match expectation") + assert.Len(t, actions.DeleteIDs, 2, "'deleteIDs' did not match expectation") + assert.EqualValuesf(t, expectedActions.Create, actions.Create, "'create' did not match expectation") + assert.EqualValuesf(t, expectedActions.BackoffUntil, actions.BackoffUntil, "'BackoffUntil' did not match expectation") + }, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // GIVEN: a preset. + defaultPreset := preset(true, tc.desired, current) + presets := []database.GetTemplatePresetsWithPrebuildsRow{ + defaultPreset, + } + + // GIVEN: running prebuilt workspaces for the preset. + running := make([]database.GetRunningPrebuiltWorkspacesRow, 0, tc.running) + for range tc.running { + name, err := prebuilds.GenerateName() + require.NoError(t, err) + running = append(running, database.GetRunningPrebuiltWorkspacesRow{ + ID: uuid.New(), + Name: name, + TemplateID: current.templateID, + TemplateVersionID: current.templateVersionID, + CurrentPresetID: uuid.NullUUID{UUID: current.presetID, Valid: true}, + Ready: false, + CreatedAt: clock.Now(), + }) + } + + // GIVEN: some prebuilds for the preset which are currently transitioning. + inProgress := []database.CountInProgressPrebuildsRow{ + { + TemplateID: current.templateID, + TemplateVersionID: current.templateVersionID, + Transition: tc.transition, + Count: tc.inProgress, + PresetID: uuid.NullUUID{ + UUID: defaultPreset.ID, + Valid: true, + }, + }, + } + + // WHEN: calculating the current preset's state. + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + ps, err := snapshot.FilterByPreset(current.presetID) + require.NoError(t, err) + + // THEN: we should identify that this prebuild is in progress. + state := ps.CalculateState() + actions, err := ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + tc.checkFn(*state, *actions) + }) + } +} + +// Additional prebuilds exist for a given preset configuration; these must be deleted. +func TestExtraneous(t *testing.T) { + t.Parallel() + current := opts[optionSet0] + clock := quartz.NewMock(t) + + // GIVEN: a preset with 1 desired prebuild. + presets := []database.GetTemplatePresetsWithPrebuildsRow{ + preset(true, 1, current), + } + + var older uuid.UUID + // GIVEN: 2 running prebuilds for the preset. + running := []database.GetRunningPrebuiltWorkspacesRow{ + prebuiltWorkspace(current, clock, func(row database.GetRunningPrebuiltWorkspacesRow) database.GetRunningPrebuiltWorkspacesRow { + // The older of the running prebuilds will be deleted in order to maintain freshness. + row.CreatedAt = clock.Now().Add(-time.Hour) + older = row.ID + return row + }), + prebuiltWorkspace(current, clock, func(row database.GetRunningPrebuiltWorkspacesRow) database.GetRunningPrebuiltWorkspacesRow { + row.CreatedAt = clock.Now() + return row + }), + } + + // GIVEN: NO prebuilds in progress. + var inProgress []database.CountInProgressPrebuildsRow + + // WHEN: calculating the current preset's state. + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + ps, err := snapshot.FilterByPreset(current.presetID) + require.NoError(t, err) + + // THEN: an extraneous prebuild is detected and marked for deletion. + state := ps.CalculateState() + actions, err := ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + validateState(t, prebuilds.ReconciliationState{ + Actual: 2, Desired: 1, Extraneous: 1, Eligible: 2, + }, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeDelete, + DeleteIDs: []uuid.UUID{older}, + }, *actions) +} + +// A template marked as deprecated will not have prebuilds running. +func TestDeprecated(t *testing.T) { + t.Parallel() + current := opts[optionSet0] + clock := quartz.NewMock(t) + + // GIVEN: a preset with 1 desired prebuild. + presets := []database.GetTemplatePresetsWithPrebuildsRow{ + preset(true, 1, current, func(row database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow { + row.Deprecated = true + return row + }), + } + + // GIVEN: 1 running prebuilds for the preset. + running := []database.GetRunningPrebuiltWorkspacesRow{ + prebuiltWorkspace(current, clock), + } + + // GIVEN: NO prebuilds in progress. + var inProgress []database.CountInProgressPrebuildsRow + + // WHEN: calculating the current preset's state. + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, nil) + ps, err := snapshot.FilterByPreset(current.presetID) + require.NoError(t, err) + + // THEN: all running prebuilds should be deleted because the template is deprecated. + state := ps.CalculateState() + actions, err := ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + validateState(t, prebuilds.ReconciliationState{}, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeDelete, + DeleteIDs: []uuid.UUID{current.prebuiltWorkspaceID}, + }, *actions) +} + +// If the latest build failed, backoff exponentially with the given interval. +func TestLatestBuildFailed(t *testing.T) { + t.Parallel() + current := opts[optionSet0] + other := opts[optionSet1] + clock := quartz.NewMock(t) + + // GIVEN: two presets. + presets := []database.GetTemplatePresetsWithPrebuildsRow{ + preset(true, 1, current), + preset(true, 1, other), + } + + // GIVEN: running prebuilds only for one preset (the other will be failing, as evidenced by the backoffs below). + running := []database.GetRunningPrebuiltWorkspacesRow{ + prebuiltWorkspace(other, clock), + } + + // GIVEN: NO prebuilds in progress. + var inProgress []database.CountInProgressPrebuildsRow + + // GIVEN: a backoff entry. + lastBuildTime := clock.Now() + numFailed := 1 + backoffs := []database.GetPresetsBackoffRow{ + { + TemplateVersionID: current.templateVersionID, + PresetID: current.presetID, + NumFailed: int32(numFailed), + LastBuildAt: lastBuildTime, + }, + } + + // WHEN: calculating the current preset's state. + snapshot := prebuilds.NewGlobalSnapshot(presets, running, inProgress, backoffs) + psCurrent, err := snapshot.FilterByPreset(current.presetID) + require.NoError(t, err) + + // THEN: reconciliation should backoff. + state := psCurrent.CalculateState() + actions, err := psCurrent.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + validateState(t, prebuilds.ReconciliationState{ + Actual: 0, Desired: 1, + }, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeBackoff, + BackoffUntil: lastBuildTime.Add(time.Duration(numFailed) * backoffInterval), + }, *actions) + + // WHEN: calculating the other preset's state. + psOther, err := snapshot.FilterByPreset(other.presetID) + require.NoError(t, err) + + // THEN: it should NOT be in backoff because all is OK. + state = psOther.CalculateState() + actions, err = psOther.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + validateState(t, prebuilds.ReconciliationState{ + Actual: 1, Desired: 1, Eligible: 1, + }, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + BackoffUntil: time.Time{}, + }, *actions) + + // WHEN: the clock is advanced a backoff interval. + clock.Advance(backoffInterval + time.Microsecond) + + // THEN: a new prebuild should be created. + psCurrent, err = snapshot.FilterByPreset(current.presetID) + require.NoError(t, err) + state = psCurrent.CalculateState() + actions, err = psCurrent.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + validateState(t, prebuilds.ReconciliationState{ + Actual: 0, Desired: 1, + }, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 1, // <--- NOTE: we're now able to create a new prebuild because the interval has elapsed. + + }, *actions) +} + +func TestMultiplePresetsPerTemplateVersion(t *testing.T) { + t.Parallel() + + templateID := uuid.New() + templateVersionID := uuid.New() + presetOpts1 := options{ + templateID: templateID, + templateVersionID: templateVersionID, + presetID: uuid.New(), + presetName: "my-preset-1", + prebuiltWorkspaceID: uuid.New(), + workspaceName: "prebuilds1", + } + presetOpts2 := options{ + templateID: templateID, + templateVersionID: templateVersionID, + presetID: uuid.New(), + presetName: "my-preset-2", + prebuiltWorkspaceID: uuid.New(), + workspaceName: "prebuilds2", + } + + clock := quartz.NewMock(t) + + presets := []database.GetTemplatePresetsWithPrebuildsRow{ + preset(true, 1, presetOpts1), + preset(true, 1, presetOpts2), + } + + inProgress := []database.CountInProgressPrebuildsRow{ + { + TemplateID: templateID, + TemplateVersionID: templateVersionID, + Transition: database.WorkspaceTransitionStart, + Count: 1, + PresetID: uuid.NullUUID{ + UUID: presetOpts1.presetID, + Valid: true, + }, + }, + } + + snapshot := prebuilds.NewGlobalSnapshot(presets, nil, inProgress, nil) + + // Nothing has to be created for preset 1. + { + ps, err := snapshot.FilterByPreset(presetOpts1.presetID) + require.NoError(t, err) + + state := ps.CalculateState() + actions, err := ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + + validateState(t, prebuilds.ReconciliationState{ + Starting: 1, + Desired: 1, + }, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 0, + }, *actions) + } + + // One prebuild has to be created for preset 2. Make sure preset 1 doesn't block preset 2. + { + ps, err := snapshot.FilterByPreset(presetOpts2.presetID) + require.NoError(t, err) + + state := ps.CalculateState() + actions, err := ps.CalculateActions(clock, backoffInterval) + require.NoError(t, err) + + validateState(t, prebuilds.ReconciliationState{ + Starting: 0, + Desired: 1, + }, *state) + validateActions(t, prebuilds.ReconciliationActions{ + ActionType: prebuilds.ActionTypeCreate, + Create: 1, + }, *actions) + } +} + +func preset(active bool, instances int32, opts options, muts ...func(row database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow) database.GetTemplatePresetsWithPrebuildsRow { + entry := database.GetTemplatePresetsWithPrebuildsRow{ + TemplateID: opts.templateID, + TemplateVersionID: opts.templateVersionID, + ID: opts.presetID, + UsingActiveVersion: active, + Name: opts.presetName, + DesiredInstances: sql.NullInt32{ + Valid: true, + Int32: instances, + }, + Deleted: false, + Deprecated: false, + } + + for _, mut := range muts { + entry = mut(entry) + } + return entry +} + +func prebuiltWorkspace( + opts options, + clock quartz.Clock, + muts ...func(row database.GetRunningPrebuiltWorkspacesRow) database.GetRunningPrebuiltWorkspacesRow, +) database.GetRunningPrebuiltWorkspacesRow { + entry := database.GetRunningPrebuiltWorkspacesRow{ + ID: opts.prebuiltWorkspaceID, + Name: opts.workspaceName, + TemplateID: opts.templateID, + TemplateVersionID: opts.templateVersionID, + CurrentPresetID: uuid.NullUUID{UUID: opts.presetID, Valid: true}, + Ready: true, + CreatedAt: clock.Now(), + } + + for _, mut := range muts { + entry = mut(entry) + } + return entry +} + +func validateState(t *testing.T, expected, actual prebuilds.ReconciliationState) { + require.Equal(t, expected, actual) +} + +// validateActions is a convenience func to make tests more readable; it exploits the fact that the default states for +// prebuilds align with zero values. +func validateActions(t *testing.T, expected, actual prebuilds.ReconciliationActions) { + require.Equal(t, expected, actual) +} diff --git a/coderd/prebuilds/util.go b/coderd/prebuilds/util.go new file mode 100644 index 0000000000000..2cc5311d5ed99 --- /dev/null +++ b/coderd/prebuilds/util.go @@ -0,0 +1,26 @@ +package prebuilds + +import ( + "crypto/rand" + "encoding/base32" + "fmt" + "strings" +) + +// GenerateName generates a 20-byte prebuild name which should safe to use without truncation in most situations. +// UUIDs may be too long for a resource name in cloud providers (since this ID will be used in the prebuild's name). +// +// We're generating a 9-byte suffix (72 bits of entropy): +// 1 - e^(-1e9^2 / (2 * 2^72)) = ~0.01% likelihood of collision in 1 billion IDs. +// See https://en.wikipedia.org/wiki/Birthday_attack. +func GenerateName() (string, error) { + b := make([]byte, 9) + + _, err := rand.Read(b) + if err != nil { + return "", err + } + + // Encode the bytes to Base32 (A-Z2-7), strip any '=' padding + return fmt.Sprintf("prebuild-%s", strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b))), nil +} diff --git a/coderd/util/slice/slice.go b/coderd/util/slice/slice.go index b4ee79291d73f..f3811650786b7 100644 --- a/coderd/util/slice/slice.go +++ b/coderd/util/slice/slice.go @@ -90,6 +90,17 @@ func Find[T any](haystack []T, cond func(T) bool) (T, bool) { return empty, false } +// Filter returns all elements that satisfy the condition. +func Filter[T any](haystack []T, cond func(T) bool) []T { + out := make([]T, 0, len(haystack)) + for _, hay := range haystack { + if cond(hay) { + out = append(out, hay) + } + } + return out +} + // Overlap returns if the 2 sets have any overlap (element(s) in common) func Overlap[T comparable](a []T, b []T) bool { return OverlapCompare(a, b, func(a, b T) bool { diff --git a/coderd/util/slice/slice_test.go b/coderd/util/slice/slice_test.go index df8d119273652..006337794faee 100644 --- a/coderd/util/slice/slice_test.go +++ b/coderd/util/slice/slice_test.go @@ -2,6 +2,7 @@ package slice_test import ( "math/rand" + "strings" "testing" "github.com/google/uuid" @@ -82,6 +83,64 @@ func TestContains(t *testing.T) { ) } +func TestFilter(t *testing.T) { + t.Parallel() + + type testCase[T any] struct { + haystack []T + cond func(T) bool + expected []T + } + + { + testCases := []*testCase[int]{ + { + haystack: []int{1, 2, 3, 4, 5}, + cond: func(num int) bool { + return num%2 == 1 + }, + expected: []int{1, 3, 5}, + }, + { + haystack: []int{1, 2, 3, 4, 5}, + cond: func(num int) bool { + return num%2 == 0 + }, + expected: []int{2, 4}, + }, + } + + for _, tc := range testCases { + actual := slice.Filter(tc.haystack, tc.cond) + require.Equal(t, tc.expected, actual) + } + } + + { + testCases := []*testCase[string]{ + { + haystack: []string{"hello", "hi", "bye"}, + cond: func(str string) bool { + return strings.HasPrefix(str, "h") + }, + expected: []string{"hello", "hi"}, + }, + { + haystack: []string{"hello", "hi", "bye"}, + cond: func(str string) bool { + return strings.HasPrefix(str, "b") + }, + expected: []string{"bye"}, + }, + } + + for _, tc := range testCases { + actual := slice.Filter(tc.haystack, tc.cond) + require.Equal(t, tc.expected, actual) + } + } +} + func TestOverlap(t *testing.T) { t.Parallel() diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 9db5a030ebc18..8b447e2c96e06 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -791,6 +791,19 @@ type NotificationsWebhookConfig struct { Endpoint serpent.URL `json:"endpoint" typescript:",notnull"` } +type PrebuildsConfig struct { + // ReconciliationInterval defines how often the workspace prebuilds state should be reconciled. + ReconciliationInterval serpent.Duration `json:"reconciliation_interval" typescript:",notnull"` + + // ReconciliationBackoffInterval specifies the amount of time to increase the backoff interval + // when errors occur during reconciliation. + ReconciliationBackoffInterval serpent.Duration `json:"reconciliation_backoff_interval" typescript:",notnull"` + + // ReconciliationBackoffLookback determines the time window to look back when calculating + // the number of failed prebuilds, which influences the backoff strategy. + ReconciliationBackoffLookback serpent.Duration `json:"reconciliation_backoff_lookback" typescript:",notnull"` +} + const ( annotationFormatDuration = "format_duration" annotationEnterpriseKey = "enterprise" diff --git a/enterprise/coderd/prebuilds/reconcile.go b/enterprise/coderd/prebuilds/reconcile.go new file mode 100644 index 0000000000000..f74e019207c18 --- /dev/null +++ b/enterprise/coderd/prebuilds/reconcile.go @@ -0,0 +1,541 @@ +package prebuilds + +import ( + "context" + "database/sql" + "fmt" + "math" + "sync/atomic" + "time" + + "github.com/hashicorp/go-multierror" + + "github.com/coder/quartz" + + "github.com/coder/coder/v2/coderd/audit" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/database/provisionerjobs" + "github.com/coder/coder/v2/coderd/database/pubsub" + "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" + + "cdr.dev/slog" + + "github.com/google/uuid" + "golang.org/x/sync/errgroup" + "golang.org/x/xerrors" +) + +type StoreReconciler struct { + store database.Store + cfg codersdk.PrebuildsConfig + pubsub pubsub.Pubsub + logger slog.Logger + clock quartz.Clock + + cancelFn context.CancelCauseFunc + stopped atomic.Bool + done chan struct{} +} + +var _ prebuilds.ReconciliationOrchestrator = &StoreReconciler{} + +func NewStoreReconciler( + store database.Store, + ps pubsub.Pubsub, + cfg codersdk.PrebuildsConfig, + logger slog.Logger, + clock quartz.Clock, +) *StoreReconciler { + return &StoreReconciler{ + store: store, + pubsub: ps, + logger: logger, + cfg: cfg, + clock: clock, + done: make(chan struct{}, 1), + } +} + +func (c *StoreReconciler) RunLoop(ctx context.Context) { + reconciliationInterval := c.cfg.ReconciliationInterval.Value() + if reconciliationInterval <= 0 { // avoids a panic + reconciliationInterval = 5 * time.Minute + } + + c.logger.Info(ctx, "starting reconciler", + slog.F("interval", reconciliationInterval), + slog.F("backoff_interval", c.cfg.ReconciliationBackoffInterval.String()), + slog.F("backoff_lookback", c.cfg.ReconciliationBackoffLookback.String())) + + ticker := c.clock.NewTicker(reconciliationInterval) + defer ticker.Stop() + defer func() { + c.done <- struct{}{} + }() + + // nolint:gocritic // Reconciliation Loop needs Prebuilds Orchestrator permissions. + ctx, cancel := context.WithCancelCause(dbauthz.AsPrebuildsOrchestrator(ctx)) + c.cancelFn = cancel + + for { + select { + // TODO: implement pubsub listener to allow reconciling a specific template imperatively once it has been changed, + // instead of waiting for the next reconciliation interval + case <-ticker.C: + // Trigger a new iteration on each tick. + err := c.ReconcileAll(ctx) + if err != nil { + c.logger.Error(context.Background(), "reconciliation failed", slog.Error(err)) + } + case <-ctx.Done(): + // nolint:gocritic // it's okay to use slog.F() for an error in this case + // because we want to differentiate two different types of errors: ctx.Err() and context.Cause() + c.logger.Warn( + context.Background(), + "reconciliation loop exited", + slog.Error(ctx.Err()), + slog.F("cause", context.Cause(ctx)), + ) + return + } + } +} + +func (c *StoreReconciler) Stop(ctx context.Context, cause error) { + if cause != nil { + c.logger.Error(context.Background(), "stopping reconciler due to an error", slog.Error(cause)) + } else { + c.logger.Info(context.Background(), "gracefully stopping reconciler") + } + + if c.isStopped() { + return + } + c.stopped.Store(true) + if c.cancelFn != nil { + c.cancelFn(cause) + } + + select { + // Give up waiting for control loop to exit. + case <-ctx.Done(): + // nolint:gocritic // it's okay to use slog.F() for an error in this case + // because we want to differentiate two different types of errors: ctx.Err() and context.Cause() + c.logger.Error( + context.Background(), + "reconciler stop exited prematurely", + slog.Error(ctx.Err()), + slog.F("cause", context.Cause(ctx)), + ) + // Wait for the control loop to exit. + case <-c.done: + c.logger.Info(context.Background(), "reconciler stopped") + } +} + +func (c *StoreReconciler) isStopped() bool { + return c.stopped.Load() +} + +// ReconcileAll will attempt to resolve the desired vs actual state of all templates which have presets with prebuilds configured. +// +// NOTE: +// +// This function will kick of n provisioner jobs, based on the calculated state modifications. +// +// These provisioning jobs are fire-and-forget. We DO NOT wait for the prebuilt workspaces to complete their +// provisioning. As a consequence, it's possible that another reconciliation run will occur, which will mean that +// multiple preset versions could be reconciling at once. This may mean some temporary over-provisioning, but the +// reconciliation loop will bring these resources back into their desired numbers in an EVENTUALLY-consistent way. +// +// For example: we could decide to provision 1 new instance in this reconciliation. +// While that workspace is being provisioned, another template version is created which means this same preset will +// be reconciled again, leading to another workspace being provisioned. Two workspace builds will be occurring +// simultaneously for the same preset, but once both jobs have completed the reconciliation loop will notice the +// extraneous instance and delete it. +func (c *StoreReconciler) ReconcileAll(ctx context.Context) error { + logger := c.logger.With(slog.F("reconcile_context", "all")) + + select { + case <-ctx.Done(): + logger.Warn(context.Background(), "reconcile exiting prematurely; context done", slog.Error(ctx.Err())) + return nil + default: + } + + logger.Debug(ctx, "starting reconciliation") + + err := c.WithReconciliationLock(ctx, logger, func(ctx context.Context, db database.Store) error { + snapshot, err := c.SnapshotState(ctx, db) + if err != nil { + return xerrors.Errorf("determine current snapshot: %w", err) + } + if len(snapshot.Presets) == 0 { + logger.Debug(ctx, "no templates found with prebuilds configured") + return nil + } + + var eg errgroup.Group + // Reconcile presets in parallel. Each preset in its own goroutine. + for _, preset := range snapshot.Presets { + ps, err := snapshot.FilterByPreset(preset.ID) + if err != nil { + logger.Warn(ctx, "failed to find preset snapshot", slog.Error(err), slog.F("preset_id", preset.ID.String())) + continue + } + + eg.Go(func() error { + // Pass outer context. + err = c.ReconcilePreset(ctx, *ps) + if err != nil { + logger.Error( + ctx, + "failed to reconcile prebuilds for preset", + slog.Error(err), + slog.F("preset_id", preset.ID), + ) + } + // DO NOT return error otherwise the tx will end. + return nil + }) + } + + // Release lock only when all preset reconciliation goroutines are finished. + return eg.Wait() + }) + if err != nil { + logger.Error(ctx, "failed to reconcile", slog.Error(err)) + } + + return err +} + +// SnapshotState captures the current state of all prebuilds across templates. +func (c *StoreReconciler) SnapshotState(ctx context.Context, store database.Store) (*prebuilds.GlobalSnapshot, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + var state prebuilds.GlobalSnapshot + + err := store.InTx(func(db database.Store) error { + // TODO: implement template-specific reconciliations later + presetsWithPrebuilds, err := db.GetTemplatePresetsWithPrebuilds(ctx, uuid.NullUUID{}) + if err != nil { + return xerrors.Errorf("failed to get template presets with prebuilds: %w", err) + } + if len(presetsWithPrebuilds) == 0 { + return nil + } + allRunningPrebuilds, err := db.GetRunningPrebuiltWorkspaces(ctx) + if err != nil { + return xerrors.Errorf("failed to get running prebuilds: %w", err) + } + + allPrebuildsInProgress, err := db.CountInProgressPrebuilds(ctx) + if err != nil { + return xerrors.Errorf("failed to get prebuilds in progress: %w", err) + } + + presetsBackoff, err := db.GetPresetsBackoff(ctx, c.clock.Now().Add(-c.cfg.ReconciliationBackoffLookback.Value())) + if err != nil { + return xerrors.Errorf("failed to get backoffs for presets: %w", err) + } + + state = prebuilds.NewGlobalSnapshot(presetsWithPrebuilds, allRunningPrebuilds, allPrebuildsInProgress, presetsBackoff) + return nil + }, &database.TxOptions{ + Isolation: sql.LevelRepeatableRead, // This mirrors the MVCC snapshotting Postgres does when using CTEs + ReadOnly: true, + TxIdentifier: "prebuilds_state_determination", + }) + + return &state, err +} + +func (c *StoreReconciler) ReconcilePreset(ctx context.Context, ps prebuilds.PresetSnapshot) error { + logger := c.logger.With( + slog.F("template_id", ps.Preset.TemplateID.String()), + slog.F("template_name", ps.Preset.TemplateName), + slog.F("template_version_id", ps.Preset.TemplateVersionID), + slog.F("template_version_name", ps.Preset.TemplateVersionName), + slog.F("preset_id", ps.Preset.ID), + slog.F("preset_name", ps.Preset.Name), + ) + + state := ps.CalculateState() + actions, err := c.CalculateActions(ctx, ps) + if err != nil { + logger.Error(ctx, "failed to calculate actions for preset", slog.Error(err), slog.F("preset_id", ps.Preset.ID)) + return nil + } + + // nolint:gocritic // ReconcilePreset needs Prebuilds Orchestrator permissions. + prebuildsCtx := dbauthz.AsPrebuildsOrchestrator(ctx) + + levelFn := logger.Debug + switch { + case actions.ActionType == prebuilds.ActionTypeBackoff: + levelFn = logger.Warn + // Log at info level when there's a change to be effected. + case actions.ActionType == prebuilds.ActionTypeCreate && actions.Create > 0: + levelFn = logger.Info + case actions.ActionType == prebuilds.ActionTypeDelete && len(actions.DeleteIDs) > 0: + levelFn = logger.Info + } + + fields := []any{ + slog.F("action_type", actions.ActionType), + slog.F("create_count", actions.Create), slog.F("delete_count", len(actions.DeleteIDs)), + slog.F("to_delete", actions.DeleteIDs), + slog.F("desired", state.Desired), slog.F("actual", state.Actual), + slog.F("extraneous", state.Extraneous), slog.F("starting", state.Starting), + slog.F("stopping", state.Stopping), slog.F("deleting", state.Deleting), + slog.F("eligible", state.Eligible), + } + + levelFn(ctx, "calculated reconciliation actions for preset", fields...) + + switch actions.ActionType { + case prebuilds.ActionTypeBackoff: + // If there is anything to backoff for (usually a cycle of failed prebuilds), then log and bail out. + levelFn(ctx, "template prebuild state retrieved, backing off", + append(fields, + slog.F("backoff_until", actions.BackoffUntil.Format(time.RFC3339)), + slog.F("backoff_secs", math.Round(actions.BackoffUntil.Sub(c.clock.Now()).Seconds())), + )...) + + return nil + + case prebuilds.ActionTypeCreate: + // Unexpected things happen (i.e. bugs or bitflips); let's defend against disastrous outcomes. + // See https://blog.robertelder.org/causes-of-bit-flips-in-computer-memory/. + // This is obviously not comprehensive protection against this sort of problem, but this is one essential check. + desired := ps.Preset.DesiredInstances.Int32 + if actions.Create > desired { + logger.Critical(ctx, "determined excessive count of prebuilds to create; clamping to desired count", + slog.F("create_count", actions.Create), slog.F("desired_count", desired)) + + actions.Create = desired + } + + var multiErr multierror.Error + + for range actions.Create { + if err := c.createPrebuiltWorkspace(prebuildsCtx, uuid.New(), ps.Preset.TemplateID, ps.Preset.ID); err != nil { + logger.Error(ctx, "failed to create prebuild", slog.Error(err)) + multiErr.Errors = append(multiErr.Errors, err) + } + } + + return multiErr.ErrorOrNil() + + case prebuilds.ActionTypeDelete: + var multiErr multierror.Error + + for _, id := range actions.DeleteIDs { + if err := c.deletePrebuiltWorkspace(prebuildsCtx, id, ps.Preset.TemplateID, ps.Preset.ID); err != nil { + logger.Error(ctx, "failed to delete prebuild", slog.Error(err)) + multiErr.Errors = append(multiErr.Errors, err) + } + } + + return multiErr.ErrorOrNil() + + default: + return xerrors.Errorf("unknown action type: %v", actions.ActionType) + } +} + +func (c *StoreReconciler) CalculateActions(ctx context.Context, snapshot prebuilds.PresetSnapshot) (*prebuilds.ReconciliationActions, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + return snapshot.CalculateActions(c.clock, c.cfg.ReconciliationBackoffInterval.Value()) +} + +func (c *StoreReconciler) WithReconciliationLock( + ctx context.Context, + logger slog.Logger, + fn func(ctx context.Context, db database.Store) error, +) error { + // This tx holds a global lock, which prevents any other coderd replica from starting a reconciliation and + // possibly getting an inconsistent view of the state. + // + // The lock MUST be held until ALL modifications have been effected. + // + // It is run with RepeatableRead isolation, so it's effectively snapshotting the data at the start of the tx. + // + // This is a read-only tx, so returning an error (i.e. causing a rollback) has no impact. + return c.store.InTx(func(db database.Store) error { + start := c.clock.Now() + + // Try to acquire the lock. If we can't get it, another replica is handling reconciliation. + acquired, err := db.TryAcquireLock(ctx, database.LockIDReconcilePrebuilds) + if err != nil { + // This is a real database error, not just lock contention + logger.Error(ctx, "failed to acquire reconciliation lock due to database error", slog.Error(err)) + return err + } + if !acquired { + // Normal case: another replica has the lock + return nil + } + + logger.Debug(ctx, + "acquired top-level reconciliation lock", + slog.F("acquire_wait_secs", fmt.Sprintf("%.4f", c.clock.Since(start).Seconds())), + ) + + return fn(ctx, db) + }, &database.TxOptions{ + Isolation: sql.LevelRepeatableRead, + ReadOnly: true, + TxIdentifier: "prebuilds", + }) +} + +func (c *StoreReconciler) createPrebuiltWorkspace(ctx context.Context, prebuiltWorkspaceID uuid.UUID, templateID uuid.UUID, presetID uuid.UUID) error { + name, err := prebuilds.GenerateName() + if err != nil { + return xerrors.Errorf("failed to generate unique prebuild ID: %w", err) + } + + return c.store.InTx(func(db database.Store) error { + template, err := db.GetTemplateByID(ctx, templateID) + if err != nil { + return xerrors.Errorf("failed to get template: %w", err) + } + + now := c.clock.Now() + + minimumWorkspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{ + ID: prebuiltWorkspaceID, + CreatedAt: now, + UpdatedAt: now, + OwnerID: prebuilds.SystemUserID, + OrganizationID: template.OrganizationID, + TemplateID: template.ID, + Name: name, + LastUsedAt: c.clock.Now(), + AutomaticUpdates: database.AutomaticUpdatesNever, + AutostartSchedule: sql.NullString{}, + Ttl: sql.NullInt64{}, + NextStartAt: sql.NullTime{}, + }) + if err != nil { + return xerrors.Errorf("insert workspace: %w", err) + } + + // We have to refetch the workspace for the joined in fields. + workspace, err := db.GetWorkspaceByID(ctx, minimumWorkspace.ID) + if err != nil { + return xerrors.Errorf("get workspace by ID: %w", err) + } + + c.logger.Info(ctx, "attempting to create prebuild", slog.F("name", name), + slog.F("workspace_id", prebuiltWorkspaceID.String()), slog.F("preset_id", presetID.String())) + + return c.provision(ctx, db, prebuiltWorkspaceID, template, presetID, database.WorkspaceTransitionStart, workspace) + }, &database.TxOptions{ + Isolation: sql.LevelRepeatableRead, + ReadOnly: false, + }) +} + +func (c *StoreReconciler) deletePrebuiltWorkspace(ctx context.Context, prebuiltWorkspaceID uuid.UUID, templateID uuid.UUID, presetID uuid.UUID) error { + return c.store.InTx(func(db database.Store) error { + workspace, err := db.GetWorkspaceByID(ctx, prebuiltWorkspaceID) + if err != nil { + return xerrors.Errorf("get workspace by ID: %w", err) + } + + template, err := db.GetTemplateByID(ctx, templateID) + if err != nil { + return xerrors.Errorf("failed to get template: %w", err) + } + + if workspace.OwnerID != prebuilds.SystemUserID { + return xerrors.Errorf("prebuilt workspace is not owned by prebuild user anymore, probably it was claimed") + } + + c.logger.Info(ctx, "attempting to delete prebuild", + slog.F("workspace_id", prebuiltWorkspaceID.String()), slog.F("preset_id", presetID.String())) + + return c.provision(ctx, db, prebuiltWorkspaceID, template, presetID, database.WorkspaceTransitionDelete, workspace) + }, &database.TxOptions{ + Isolation: sql.LevelRepeatableRead, + ReadOnly: false, + }) +} + +func (c *StoreReconciler) provision( + ctx context.Context, + db database.Store, + prebuildID uuid.UUID, + template database.Template, + presetID uuid.UUID, + transition database.WorkspaceTransition, + workspace database.Workspace, +) error { + tvp, err := db.GetPresetParametersByTemplateVersionID(ctx, template.ActiveVersionID) + if err != nil { + return xerrors.Errorf("fetch preset details: %w", err) + } + + var params []codersdk.WorkspaceBuildParameter + for _, param := range tvp { + // TODO: don't fetch in the first place. + if param.TemplateVersionPresetID != presetID { + continue + } + + params = append(params, codersdk.WorkspaceBuildParameter{ + Name: param.Name, + Value: param.Value, + }) + } + + builder := wsbuilder.New(workspace, transition). + Reason(database.BuildReasonInitiator). + Initiator(prebuilds.SystemUserID). + VersionID(template.ActiveVersionID). + MarkPrebuild(). + TemplateVersionPresetID(presetID) + + // We only inject the required params when the prebuild is being created. + // This mirrors the behavior of regular workspace deletion (see cli/delete.go). + if transition != database.WorkspaceTransitionDelete { + builder = builder.RichParameterValues(params) + } + + _, provisionerJob, _, err := builder.Build( + ctx, + db, + func(_ policy.Action, _ rbac.Objecter) bool { + return true // TODO: harden? + }, + audit.WorkspaceBuildBaggage{}, + ) + if err != nil { + return xerrors.Errorf("provision workspace: %w", err) + } + + err = provisionerjobs.PostJob(c.pubsub, *provisionerJob) + if err != nil { + // Client probably doesn't care about this error, so just log it. + c.logger.Error(ctx, "failed to post provisioner job to pubsub", slog.Error(err)) + } + + c.logger.Info(ctx, "prebuild job scheduled", slog.F("transition", transition), + slog.F("prebuild_id", prebuildID.String()), slog.F("preset_id", presetID.String()), + slog.F("job_id", provisionerJob.ID)) + + return nil +} diff --git a/enterprise/coderd/prebuilds/reconcile_test.go b/enterprise/coderd/prebuilds/reconcile_test.go new file mode 100644 index 0000000000000..a5bd4a728a4ea --- /dev/null +++ b/enterprise/coderd/prebuilds/reconcile_test.go @@ -0,0 +1,1027 @@ +package prebuilds_test + +import ( + "context" + "database/sql" + "fmt" + "sync" + "testing" + "time" + + "github.com/coder/coder/v2/coderd/util/slice" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "tailscale.com/types/ptr" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/quartz" + + "github.com/coder/serpent" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + "github.com/coder/coder/v2/coderd/database/pubsub" + agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/prebuilds" + "github.com/coder/coder/v2/testutil" +) + +func TestNoReconciliationActionsIfNoPresets(t *testing.T) { + // Scenario: No reconciliation actions are taken if there are no presets + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + clock := quartz.NewMock(t) + ctx := testutil.Context(t, testutil.WaitLong) + db, ps := dbtestutil.NewDB(t) + cfg := codersdk.PrebuildsConfig{ + ReconciliationInterval: serpent.Duration(testutil.WaitLong), + } + logger := testutil.Logger(t) + controller := prebuilds.NewStoreReconciler(db, ps, cfg, logger, quartz.NewMock(t)) + + // given a template version with no presets + org := dbgen.Organization(t, db, database.Organization{}) + user := dbgen.User(t, db, database.User{}) + template := dbgen.Template(t, db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + // verify that the db state is correct + gotTemplateVersion, err := db.GetTemplateVersionByID(ctx, templateVersion.ID) + require.NoError(t, err) + require.Equal(t, templateVersion, gotTemplateVersion) + + // when we trigger the reconciliation loop for all templates + require.NoError(t, controller.ReconcileAll(ctx)) + + // then no reconciliation actions are taken + // because without presets, there are no prebuilds + // and without prebuilds, there is nothing to reconcile + jobs, err := db.GetProvisionerJobsCreatedAfter(ctx, clock.Now().Add(earlier)) + require.NoError(t, err) + require.Empty(t, jobs) +} + +func TestNoReconciliationActionsIfNoPrebuilds(t *testing.T) { + // Scenario: No reconciliation actions are taken if there are no prebuilds + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + clock := quartz.NewMock(t) + ctx := testutil.Context(t, testutil.WaitLong) + db, ps := dbtestutil.NewDB(t) + cfg := codersdk.PrebuildsConfig{ + ReconciliationInterval: serpent.Duration(testutil.WaitLong), + } + logger := testutil.Logger(t) + controller := prebuilds.NewStoreReconciler(db, ps, cfg, logger, quartz.NewMock(t)) + + // given there are presets, but no prebuilds + org := dbgen.Organization(t, db, database.Organization{}) + user := dbgen.User(t, db, database.User{}) + template := dbgen.Template(t, db, database.Template{ + CreatedBy: user.ID, + OrganizationID: org.ID, + }) + templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + preset, err := db.InsertPreset(ctx, database.InsertPresetParams{ + TemplateVersionID: templateVersion.ID, + Name: "test", + }) + require.NoError(t, err) + _, err = db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{ + TemplateVersionPresetID: preset.ID, + Names: []string{"test"}, + Values: []string{"test"}, + }) + require.NoError(t, err) + + // verify that the db state is correct + presetParameters, err := db.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID) + require.NoError(t, err) + require.NotEmpty(t, presetParameters) + + // when we trigger the reconciliation loop for all templates + require.NoError(t, controller.ReconcileAll(ctx)) + + // then no reconciliation actions are taken + // because without prebuilds, there is nothing to reconcile + // even if there are presets + jobs, err := db.GetProvisionerJobsCreatedAfter(ctx, clock.Now().Add(earlier)) + require.NoError(t, err) + require.Empty(t, jobs) +} + +func TestPrebuildReconciliation(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + type testCase struct { + name string + prebuildLatestTransitions []database.WorkspaceTransition + prebuildJobStatuses []database.ProvisionerJobStatus + templateVersionActive []bool + templateDeleted []bool + shouldCreateNewPrebuild *bool + shouldDeleteOldPrebuild *bool + } + + testCases := []testCase{ + { + name: "never create prebuilds for inactive template versions", + prebuildLatestTransitions: allTransitions, + prebuildJobStatuses: allJobStatuses, + templateVersionActive: []bool{false}, + shouldCreateNewPrebuild: ptr.To(false), + templateDeleted: []bool{false}, + }, + { + name: "no need to create a new prebuild if one is already running", + prebuildLatestTransitions: []database.WorkspaceTransition{ + database.WorkspaceTransitionStart, + }, + prebuildJobStatuses: []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusSucceeded, + }, + templateVersionActive: []bool{true}, + shouldCreateNewPrebuild: ptr.To(false), + templateDeleted: []bool{false}, + }, + { + name: "don't create a new prebuild if one is queued to build or already building", + prebuildLatestTransitions: []database.WorkspaceTransition{ + database.WorkspaceTransitionStart, + }, + prebuildJobStatuses: []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusPending, + database.ProvisionerJobStatusRunning, + }, + templateVersionActive: []bool{true}, + shouldCreateNewPrebuild: ptr.To(false), + templateDeleted: []bool{false}, + }, + { + name: "create a new prebuild if one is in a state that disqualifies it from ever being claimed", + prebuildLatestTransitions: []database.WorkspaceTransition{ + database.WorkspaceTransitionStop, + database.WorkspaceTransitionDelete, + }, + prebuildJobStatuses: []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusPending, + database.ProvisionerJobStatusRunning, + database.ProvisionerJobStatusCanceling, + database.ProvisionerJobStatusSucceeded, + }, + templateVersionActive: []bool{true}, + shouldCreateNewPrebuild: ptr.To(true), + templateDeleted: []bool{false}, + }, + { + // See TestFailedBuildBackoff for the start/failed case. + name: "create a new prebuild if one is in any kind of exceptional state", + prebuildLatestTransitions: []database.WorkspaceTransition{ + database.WorkspaceTransitionStop, + database.WorkspaceTransitionDelete, + }, + prebuildJobStatuses: []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusCanceled, + }, + templateVersionActive: []bool{true}, + shouldCreateNewPrebuild: ptr.To(true), + templateDeleted: []bool{false}, + }, + { + name: "never attempt to interfere with active builds", + // The workspace builder does not allow scheduling a new build if there is already a build + // pending, running, or canceling. As such, we should never attempt to start, stop or delete + // such prebuilds. Rather, we should wait for the existing build to complete and reconcile + // again in the next cycle. + prebuildLatestTransitions: allTransitions, + prebuildJobStatuses: []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusPending, + database.ProvisionerJobStatusRunning, + database.ProvisionerJobStatusCanceling, + }, + templateVersionActive: []bool{true, false}, + shouldDeleteOldPrebuild: ptr.To(false), + templateDeleted: []bool{false}, + }, + { + name: "never delete prebuilds in an exceptional state", + // We don't want to destroy evidence that might be useful to operators + // when troubleshooting issues. So we leave these prebuilds in place. + // Operators are expected to manually delete these prebuilds. + prebuildLatestTransitions: allTransitions, + prebuildJobStatuses: []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusCanceled, + database.ProvisionerJobStatusFailed, + }, + templateVersionActive: []bool{true, false}, + shouldDeleteOldPrebuild: ptr.To(false), + templateDeleted: []bool{false}, + }, + { + name: "delete running prebuilds for inactive template versions", + // We only support prebuilds for active template versions. + // If a template version is inactive, we should delete any prebuilds + // that are running. + prebuildLatestTransitions: []database.WorkspaceTransition{ + database.WorkspaceTransitionStart, + }, + prebuildJobStatuses: []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusSucceeded, + }, + templateVersionActive: []bool{false}, + shouldDeleteOldPrebuild: ptr.To(true), + templateDeleted: []bool{false}, + }, + { + name: "don't delete running prebuilds for active template versions", + prebuildLatestTransitions: []database.WorkspaceTransition{ + database.WorkspaceTransitionStart, + }, + prebuildJobStatuses: []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusSucceeded, + }, + templateVersionActive: []bool{true}, + shouldDeleteOldPrebuild: ptr.To(false), + templateDeleted: []bool{false}, + }, + { + name: "don't delete stopped or already deleted prebuilds", + // We don't ever stop prebuilds. A stopped prebuild is an exceptional state. + // As such we keep it, to allow operators to investigate the cause. + prebuildLatestTransitions: []database.WorkspaceTransition{ + database.WorkspaceTransitionStop, + database.WorkspaceTransitionDelete, + }, + prebuildJobStatuses: []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusSucceeded, + }, + templateVersionActive: []bool{true, false}, + shouldDeleteOldPrebuild: ptr.To(false), + templateDeleted: []bool{false}, + }, + { + name: "delete prebuilds for deleted templates", + prebuildLatestTransitions: []database.WorkspaceTransition{database.WorkspaceTransitionStart}, + prebuildJobStatuses: []database.ProvisionerJobStatus{database.ProvisionerJobStatusSucceeded}, + templateVersionActive: []bool{true, false}, + shouldDeleteOldPrebuild: ptr.To(true), + templateDeleted: []bool{true}, + }, + } + for _, tc := range testCases { + tc := tc // capture for parallel + for _, templateVersionActive := range tc.templateVersionActive { + for _, prebuildLatestTransition := range tc.prebuildLatestTransitions { + for _, prebuildJobStatus := range tc.prebuildJobStatuses { + for _, templateDeleted := range tc.templateDeleted { + t.Run(fmt.Sprintf("%s - %s - %s", tc.name, prebuildLatestTransition, prebuildJobStatus), func(t *testing.T) { + t.Parallel() + t.Cleanup(func() { + if t.Failed() { + t.Logf("failed to run test: %s", tc.name) + t.Logf("templateVersionActive: %t", templateVersionActive) + t.Logf("prebuildLatestTransition: %s", prebuildLatestTransition) + t.Logf("prebuildJobStatus: %s", prebuildJobStatus) + } + }) + clock := quartz.NewMock(t) + ctx := testutil.Context(t, testutil.WaitShort) + cfg := codersdk.PrebuildsConfig{} + logger := slogtest.Make( + t, &slogtest.Options{IgnoreErrors: true}, + ).Leveled(slog.LevelDebug) + db, pubSub := dbtestutil.NewDB(t) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t)) + + ownerID := uuid.New() + dbgen.User(t, db, database.User{ + ID: ownerID, + }) + 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(), + ) + prebuild := setupTestDBPrebuild( + t, + clock, + db, + pubSub, + prebuildLatestTransition, + prebuildJobStatus, + org.ID, + preset, + template.ID, + templateVersionID, + ) + + if !templateVersionActive { + // Create a new template version and mark it as active + // This marks the template version that we care about as inactive + setupTestDBTemplateVersion(ctx, t, clock, db, pubSub, org.ID, ownerID, template.ID) + } + + // Run the reconciliation multiple times to ensure idempotency + // 8 was arbitrary, but large enough to reasonably trust the result + for i := 1; i <= 8; i++ { + require.NoErrorf(t, controller.ReconcileAll(ctx), "failed on iteration %d", i) + + if tc.shouldCreateNewPrebuild != nil { + newPrebuildCount := 0 + workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID) + require.NoError(t, err) + for _, workspace := range workspaces { + if workspace.ID != prebuild.ID { + newPrebuildCount++ + } + } + // This test configures a preset that desires one prebuild. + // In cases where new prebuilds should be created, there should be exactly one. + require.Equal(t, *tc.shouldCreateNewPrebuild, newPrebuildCount == 1) + } + + if tc.shouldDeleteOldPrebuild != nil { + builds, err := db.GetWorkspaceBuildsByWorkspaceID(ctx, database.GetWorkspaceBuildsByWorkspaceIDParams{ + WorkspaceID: prebuild.ID, + }) + require.NoError(t, err) + if *tc.shouldDeleteOldPrebuild { + require.Equal(t, 2, len(builds)) + require.Equal(t, database.WorkspaceTransitionDelete, builds[0].Transition) + } else { + require.Equal(t, 1, len(builds)) + require.Equal(t, prebuildLatestTransition, builds[0].Transition) + } + } + } + }) + } + } + } + } + } +} + +func TestMultiplePresetsPerTemplateVersion(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + prebuildLatestTransition := database.WorkspaceTransitionStart + prebuildJobStatus := database.ProvisionerJobStatusRunning + templateDeleted := false + + clock := quartz.NewMock(t) + ctx := testutil.Context(t, testutil.WaitShort) + cfg := codersdk.PrebuildsConfig{} + logger := slogtest.Make( + t, &slogtest.Options{IgnoreErrors: true}, + ).Leveled(slog.LevelDebug) + db, pubSub := dbtestutil.NewDB(t) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t)) + + ownerID := uuid.New() + dbgen.User(t, db, database.User{ + ID: ownerID, + }) + org, template := setupTestDBTemplate(t, db, ownerID, templateDeleted) + templateVersionID := setupTestDBTemplateVersion( + ctx, + t, + clock, + db, + pubSub, + org.ID, + ownerID, + template.ID, + ) + preset := setupTestDBPreset( + t, + db, + templateVersionID, + 4, + uuid.New().String(), + ) + preset2 := setupTestDBPreset( + t, + db, + templateVersionID, + 10, + uuid.New().String(), + ) + prebuildIDs := make([]uuid.UUID, 0) + for i := 0; i < int(preset.DesiredInstances.Int32); i++ { + prebuild := setupTestDBPrebuild( + t, + clock, + db, + pubSub, + prebuildLatestTransition, + prebuildJobStatus, + org.ID, + preset, + template.ID, + templateVersionID, + ) + prebuildIDs = append(prebuildIDs, prebuild.ID) + } + + // Run the reconciliation multiple times to ensure idempotency + // 8 was arbitrary, but large enough to reasonably trust the result + for i := 1; i <= 8; i++ { + require.NoErrorf(t, controller.ReconcileAll(ctx), "failed on iteration %d", i) + + newPrebuildCount := 0 + workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID) + require.NoError(t, err) + for _, workspace := range workspaces { + if slice.Contains(prebuildIDs, workspace.ID) { + continue + } + newPrebuildCount++ + } + + // NOTE: preset1 doesn't block creation of instances in preset2 + require.Equal(t, preset2.DesiredInstances.Int32, int32(newPrebuildCount)) // nolint:gosec + } +} + +func TestInvalidPreset(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + templateDeleted := false + + clock := quartz.NewMock(t) + ctx := testutil.Context(t, testutil.WaitShort) + cfg := codersdk.PrebuildsConfig{} + logger := slogtest.Make( + t, &slogtest.Options{IgnoreErrors: true}, + ).Leveled(slog.LevelDebug) + db, pubSub := dbtestutil.NewDB(t) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t)) + + ownerID := uuid.New() + dbgen.User(t, db, database.User{ + ID: ownerID, + }) + org, template := setupTestDBTemplate(t, db, ownerID, templateDeleted) + templateVersionID := setupTestDBTemplateVersion( + ctx, + t, + clock, + db, + pubSub, + org.ID, + ownerID, + template.ID, + ) + // Add required param, which is not set in preset. It means that creating of prebuild will constantly fail. + dbgen.TemplateVersionParameter(t, db, database.TemplateVersionParameter{ + TemplateVersionID: templateVersionID, + Name: "required-param", + Description: "required param to make sure creating prebuild will fail", + Type: "bool", + DefaultValue: "", + Required: true, + }) + setupTestDBPreset( + t, + db, + templateVersionID, + 1, + uuid.New().String(), + ) + + // Run the reconciliation multiple times to ensure idempotency + // 8 was arbitrary, but large enough to reasonably trust the result + for i := 1; i <= 8; i++ { + require.NoErrorf(t, controller.ReconcileAll(ctx), "failed on iteration %d", i) + + workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID) + require.NoError(t, err) + newPrebuildCount := len(workspaces) + + // NOTE: we don't have any new prebuilds, because their creation constantly fails. + require.Equal(t, int32(0), int32(newPrebuildCount)) // nolint:gosec + } +} + +func TestRunLoop(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + prebuildLatestTransition := database.WorkspaceTransitionStart + prebuildJobStatus := database.ProvisionerJobStatusRunning + templateDeleted := false + + clock := quartz.NewMock(t) + ctx := testutil.Context(t, testutil.WaitShort) + backoffInterval := time.Minute + cfg := codersdk.PrebuildsConfig{ + // Given: explicitly defined backoff configuration to validate timings. + ReconciliationBackoffLookback: serpent.Duration(muchEarlier * -10), // Has to be positive. + ReconciliationBackoffInterval: serpent.Duration(backoffInterval), + ReconciliationInterval: serpent.Duration(time.Second), + } + logger := slogtest.Make( + t, &slogtest.Options{IgnoreErrors: true}, + ).Leveled(slog.LevelDebug) + db, pubSub := dbtestutil.NewDB(t) + controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock) + + ownerID := uuid.New() + dbgen.User(t, db, database.User{ + ID: ownerID, + }) + org, template := setupTestDBTemplate(t, db, ownerID, templateDeleted) + templateVersionID := setupTestDBTemplateVersion( + ctx, + t, + clock, + db, + pubSub, + org.ID, + ownerID, + template.ID, + ) + preset := setupTestDBPreset( + t, + db, + templateVersionID, + 4, + uuid.New().String(), + ) + preset2 := setupTestDBPreset( + t, + db, + templateVersionID, + 10, + uuid.New().String(), + ) + prebuildIDs := make([]uuid.UUID, 0) + for i := 0; i < int(preset.DesiredInstances.Int32); i++ { + prebuild := setupTestDBPrebuild( + t, + clock, + db, + pubSub, + prebuildLatestTransition, + prebuildJobStatus, + org.ID, + preset, + template.ID, + templateVersionID, + ) + prebuildIDs = append(prebuildIDs, prebuild.ID) + } + getNewPrebuildCount := func() int32 { + newPrebuildCount := 0 + workspaces, err := db.GetWorkspacesByTemplateID(ctx, template.ID) + require.NoError(t, err) + for _, workspace := range workspaces { + if slice.Contains(prebuildIDs, workspace.ID) { + continue + } + newPrebuildCount++ + } + + return int32(newPrebuildCount) // nolint:gosec + } + + // we need to wait until ticker is initialized, and only then use clock.Advance() + // otherwise clock.Advance() will be ignored + trap := clock.Trap().NewTicker() + go controller.RunLoop(ctx) + // wait until ticker is initialized + trap.MustWait(ctx).Release() + // start 1st iteration of ReconciliationLoop + // NOTE: at this point MustWait waits that iteration is started (ReconcileAll is called), but it doesn't wait until it completes + clock.Advance(cfg.ReconciliationInterval.Value()).MustWait(ctx) + + // wait until ReconcileAll is completed + // TODO: is it possible to avoid Eventually and replace it with quartz? + // Ideally to have all control on test-level, and be able to advance loop iterations from the test. + require.Eventually(t, func() bool { + newPrebuildCount := getNewPrebuildCount() + + // NOTE: preset1 doesn't block creation of instances in preset2 + return preset2.DesiredInstances.Int32 == newPrebuildCount + }, testutil.WaitShort, testutil.IntervalFast) + + // setup one more preset with 5 prebuilds + preset3 := setupTestDBPreset( + t, + db, + templateVersionID, + 5, + uuid.New().String(), + ) + newPrebuildCount := getNewPrebuildCount() + // nothing changed, because we didn't trigger a new iteration of a loop + require.Equal(t, preset2.DesiredInstances.Int32, newPrebuildCount) + + // start 2nd iteration of ReconciliationLoop + // NOTE: at this point MustWait waits that iteration is started (ReconcileAll is called), but it doesn't wait until it completes + clock.Advance(cfg.ReconciliationInterval.Value()).MustWait(ctx) + + // wait until ReconcileAll is completed + require.Eventually(t, func() bool { + newPrebuildCount := getNewPrebuildCount() + + // both prebuilds for preset2 and preset3 were created + return preset2.DesiredInstances.Int32+preset3.DesiredInstances.Int32 == newPrebuildCount + }, testutil.WaitShort, testutil.IntervalFast) + + // gracefully stop the reconciliation loop + controller.Stop(ctx, nil) +} + +func TestFailedBuildBackoff(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) + backoffInterval := time.Minute + cfg := codersdk.PrebuildsConfig{ + // Given: explicitly defined backoff configuration to validate timings. + ReconciliationBackoffLookback: serpent.Duration(muchEarlier * -10), // Has to be positive. + ReconciliationBackoffInterval: serpent.Duration(backoffInterval), + ReconciliationInterval: serpent.Duration(time.Second), + } + logger := slogtest.Make( + t, &slogtest.Options{IgnoreErrors: true}, + ).Leveled(slog.LevelDebug) + db, ps := dbtestutil.NewDB(t) + reconciler := prebuilds.NewStoreReconciler(db, ps, cfg, logger, clock) + + // Given: an active template version with presets and prebuilds configured. + const desiredInstances = 2 + 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, desiredInstances, "test") + for range desiredInstances { + _ = 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. + snapshot, err := reconciler.SnapshotState(ctx, db) + require.NoError(t, err) + require.Len(t, snapshot.Presets, 1) + presetState, err := snapshot.FilterByPreset(preset.ID) + require.NoError(t, err) + state := presetState.CalculateState() + actions, err := reconciler.CalculateActions(ctx, *presetState) + require.NoError(t, err) + + // Then: the backoff time is in the future, no prebuilds are running, and we won't create any new prebuilds. + require.EqualValues(t, 0, state.Actual) + require.EqualValues(t, 0, actions.Create) + require.EqualValues(t, desiredInstances, state.Desired) + require.True(t, clock.Now().Before(actions.BackoffUntil)) + + // Then: the backoff time is as expected based on the number of failed builds. + require.NotNil(t, presetState.Backoff) + require.EqualValues(t, desiredInstances, presetState.Backoff.NumFailed) + require.EqualValues(t, backoffInterval*time.Duration(presetState.Backoff.NumFailed), clock.Until(actions.BackoffUntil).Truncate(backoffInterval)) + + // When: advancing to the next tick which is still within the backoff time. + clock.Advance(cfg.ReconciliationInterval.Value()) + + // Then: the backoff interval will not have changed. + snapshot, err = reconciler.SnapshotState(ctx, db) + require.NoError(t, err) + presetState, err = snapshot.FilterByPreset(preset.ID) + require.NoError(t, err) + newState := presetState.CalculateState() + newActions, err := reconciler.CalculateActions(ctx, *presetState) + require.NoError(t, err) + require.EqualValues(t, 0, newState.Actual) + require.EqualValues(t, 0, newActions.Create) + require.EqualValues(t, desiredInstances, newState.Desired) + require.EqualValues(t, actions.BackoffUntil, newActions.BackoffUntil) + + // When: advancing beyond the backoff time. + clock.Advance(clock.Until(actions.BackoffUntil.Add(time.Second))) + + // Then: we will attempt to create a new prebuild. + snapshot, err = reconciler.SnapshotState(ctx, db) + require.NoError(t, err) + presetState, err = snapshot.FilterByPreset(preset.ID) + require.NoError(t, err) + state = presetState.CalculateState() + actions, err = reconciler.CalculateActions(ctx, *presetState) + require.NoError(t, err) + require.EqualValues(t, 0, state.Actual) + require.EqualValues(t, desiredInstances, state.Desired) + require.EqualValues(t, desiredInstances, actions.Create) + + // When: the desired number of new prebuild are provisioned, but one fails again. + for i := 0; i < desiredInstances; i++ { + status := database.ProvisionerJobStatusFailed + if i == 1 { + status = database.ProvisionerJobStatusSucceeded + } + _ = 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. + snapshot, err = reconciler.SnapshotState(ctx, db) + require.NoError(t, err) + presetState, err = snapshot.FilterByPreset(preset.ID) + require.NoError(t, err) + state = presetState.CalculateState() + actions, err = reconciler.CalculateActions(ctx, *presetState) + require.NoError(t, err) + require.EqualValues(t, 1, state.Actual) + require.EqualValues(t, desiredInstances, state.Desired) + require.EqualValues(t, 0, actions.Create) + require.EqualValues(t, 3, presetState.Backoff.NumFailed) + require.EqualValues(t, backoffInterval*time.Duration(presetState.Backoff.NumFailed), clock.Until(actions.BackoffUntil).Truncate(backoffInterval)) +} + +func TestReconciliationLock(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + ctx := testutil.Context(t, testutil.WaitSuperLong) + logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + db, ps := dbtestutil.NewDB(t) + + wg := sync.WaitGroup{} + mutex := sync.Mutex{} + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + reconciler := prebuilds.NewStoreReconciler( + db, + ps, + codersdk.PrebuildsConfig{}, + slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug), + quartz.NewMock(t), + ) + 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. + // If this mutex is ever locked at this point, then that means that the postgres lock is not being held while we're + // inside WithReconciliationLock, which is meant to hold the lock. + require.True(t, lockObtained) + // Sleep a bit to give reconcilers more time to contend for the lock + time.Sleep(time.Second) + defer mutex.Unlock() + return nil + }) + }() + } + wg.Wait() +} + +// nolint:revive // It's a control flag, but this is a test. +func setupTestDBTemplate( + t *testing.T, + db database.Store, + userID uuid.UUID, + templateDeleted bool, +) ( + database.Organization, + database.Template, +) { + t.Helper() + org := dbgen.Organization(t, db, database.Organization{}) + + template := dbgen.Template(t, db, database.Template{ + 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 org, template +} + +const ( + earlier = -time.Hour + muchEarlier = -time.Hour * 2 +) + +func setupTestDBTemplateVersion( + ctx context.Context, + t *testing.T, + clock quartz.Clock, + db database.Store, + ps pubsub.Pubsub, + orgID uuid.UUID, + userID uuid.UUID, + templateID uuid.UUID, +) uuid.UUID { + t.Helper() + templateVersionJob := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + CreatedAt: clock.Now().Add(muchEarlier), + CompletedAt: sql.NullTime{Time: clock.Now().Add(earlier), Valid: true}, + OrganizationID: orgID, + InitiatorID: userID, + }) + templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{UUID: templateID, Valid: true}, + OrganizationID: orgID, + CreatedBy: userID, + JobID: templateVersionJob.ID, + CreatedAt: time.Now().Add(muchEarlier), + }) + require.NoError(t, db.UpdateTemplateActiveVersionByID(ctx, database.UpdateTemplateActiveVersionByIDParams{ + ID: templateID, + ActiveVersionID: templateVersion.ID, + })) + return templateVersion.ID +} + +func setupTestDBPreset( + t *testing.T, + db database.Store, + templateVersionID uuid.UUID, + desiredInstances int32, + presetName string, +) database.TemplateVersionPreset { + t.Helper() + preset := dbgen.Preset(t, db, database.InsertPresetParams{ + TemplateVersionID: templateVersionID, + Name: presetName, + DesiredInstances: sql.NullInt32{ + Valid: true, + Int32: desiredInstances, + }, + }) + dbgen.PresetParameter(t, db, database.InsertPresetParametersParams{ + TemplateVersionPresetID: preset.ID, + Names: []string{"test"}, + Values: []string{"test"}, + }) + return preset +} + +func setupTestDBPrebuild( + t *testing.T, + clock quartz.Clock, + db database.Store, + ps pubsub.Pubsub, + transition database.WorkspaceTransition, + prebuildStatus database.ProvisionerJobStatus, + orgID uuid.UUID, + preset database.TemplateVersionPreset, + templateID uuid.UUID, + templateVersionID uuid.UUID, +) database.WorkspaceTable { + t.Helper() + return setupTestDBWorkspace(t, clock, db, ps, transition, prebuildStatus, orgID, preset, templateID, templateVersionID, agplprebuilds.SystemUserID, agplprebuilds.SystemUserID) +} + +func setupTestDBWorkspace( + t *testing.T, + clock quartz.Clock, + db database.Store, + ps pubsub.Pubsub, + transition database.WorkspaceTransition, + prebuildStatus database.ProvisionerJobStatus, + orgID uuid.UUID, + preset database.TemplateVersionPreset, + templateID uuid.UUID, + templateVersionID uuid.UUID, + initiatorID uuid.UUID, + ownerID uuid.UUID, +) database.WorkspaceTable { + t.Helper() + cancelledAt := sql.NullTime{} + completedAt := sql.NullTime{} + + startedAt := sql.NullTime{} + if prebuildStatus != database.ProvisionerJobStatusPending { + startedAt = sql.NullTime{Time: clock.Now().Add(muchEarlier), Valid: true} + } + + buildError := sql.NullString{} + if prebuildStatus == database.ProvisionerJobStatusFailed { + completedAt = sql.NullTime{Time: clock.Now().Add(earlier), Valid: true} + buildError = sql.NullString{String: "build failed", Valid: true} + } + + switch prebuildStatus { + case database.ProvisionerJobStatusCanceling: + cancelledAt = sql.NullTime{Time: clock.Now().Add(earlier), Valid: true} + case database.ProvisionerJobStatusCanceled: + completedAt = sql.NullTime{Time: clock.Now().Add(earlier), Valid: true} + cancelledAt = sql.NullTime{Time: clock.Now().Add(earlier), Valid: true} + case database.ProvisionerJobStatusSucceeded: + completedAt = sql.NullTime{Time: clock.Now().Add(earlier), Valid: true} + default: + } + + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + TemplateID: templateID, + OrganizationID: orgID, + OwnerID: ownerID, + Deleted: false, + }) + job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{ + InitiatorID: initiatorID, + CreatedAt: clock.Now().Add(muchEarlier), + StartedAt: startedAt, + CompletedAt: completedAt, + CanceledAt: cancelledAt, + OrganizationID: orgID, + Error: buildError, + }) + dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + WorkspaceID: workspace.ID, + InitiatorID: initiatorID, + TemplateVersionID: templateVersionID, + JobID: job.ID, + TemplateVersionPresetID: uuid.NullUUID{UUID: preset.ID, Valid: true}, + Transition: transition, + CreatedAt: clock.Now(), + }) + + return workspace +} + +var allTransitions = []database.WorkspaceTransition{ + database.WorkspaceTransitionStart, + database.WorkspaceTransitionStop, + database.WorkspaceTransitionDelete, +} + +var allJobStatuses = []database.ProvisionerJobStatus{ + database.ProvisionerJobStatusPending, + database.ProvisionerJobStatusRunning, + database.ProvisionerJobStatusSucceeded, + database.ProvisionerJobStatusFailed, + database.ProvisionerJobStatusCanceled, + database.ProvisionerJobStatusCanceling, +} + +// TODO (sasswart): test mutual exclusion diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 38e8e91ac8c1a..f01cb9c98dc64 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1695,6 +1695,13 @@ export interface PprofConfig { readonly address: string; } +// From codersdk/deployment.go +export interface PrebuildsConfig { + readonly reconciliation_interval: number; + readonly reconciliation_backoff_interval: number; + readonly reconciliation_backoff_lookback: number; +} + // From codersdk/presets.go export interface Preset { readonly ID: string; From aa02c9ffb8337e337bd2a63507109b7c88562144 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 17 Apr 2025 10:48:23 -0300 Subject: [PATCH 129/384] chore: reduce storybook flakes (#17427) A few storybook tests have been false positives quite frequently. To reduce this noise, I'm implementing a few hacks to avoid that. We can always rollback these changes if we notice they were leading to a lack in the tests. --- .../OverviewPage/OverviewPageView.stories.tsx | 5 +++++ .../OverviewPage/UserEngagementChart.tsx | 1 + .../PermissionPillsList.stories.tsx | 7 +++++++ .../JobRow.tsx | 2 +- .../ProvisionerRow.tsx | 4 ++-- .../TerminalPage/TerminalPage.stories.tsx | 19 +++++++++++++++++-- .../WorkspacePage/WorkspaceTopbar.stories.tsx | 5 +++++ site/src/utils/schedule.tsx | 8 ++++---- 8 files changed, 42 insertions(+), 9 deletions(-) diff --git a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx index b3398f8b1f204..3535d4ffd1d47 100644 --- a/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/OverviewPage/OverviewPageView.stories.tsx @@ -39,6 +39,11 @@ const meta: Meta = { invalidExperiments: [], safeExperiments: [], }, + parameters: { + chromatic: { + diffThreshold: 0.5, + }, + }, }; export default meta; diff --git a/site/src/pages/DeploymentSettingsPage/OverviewPage/UserEngagementChart.tsx b/site/src/pages/DeploymentSettingsPage/OverviewPage/UserEngagementChart.tsx index 585088f02db1d..711e57242ce88 100644 --- a/site/src/pages/DeploymentSettingsPage/OverviewPage/UserEngagementChart.tsx +++ b/site/src/pages/DeploymentSettingsPage/OverviewPage/UserEngagementChart.tsx @@ -156,6 +156,7 @@ export const UserEngagementChart: FC = ({ data }) => { = { title: "pages/OrganizationCustomRolesPage/PermissionPillsList", component: PermissionPillsList, + decorators: [ + (Story) => ( +
    + +
    + ), + ], }; export default meta; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx index 94d4687565275..3e20863b25d51 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/JobRow.tsx @@ -121,7 +121,7 @@ export const JobRow: FC = ({ job }) => {
    {job.metadata.workspace_name ?? "null"}
    Creation time:
    -
    {job.created_at}
    +
    {job.created_at}
    Queue:
    diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerRow.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerRow.tsx index 2e40fe4d5388e..2c47578f67a6a 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerRow.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/ProvisionerRow.tsx @@ -111,10 +111,10 @@ export const ProvisionerRow: FC = ({ ])} >
    Last seen:
    -
    {provisioner.last_seen_at}
    +
    {provisioner.last_seen_at}
    Creation time:
    -
    {provisioner.created_at}
    +
    {provisioner.created_at}
    Version:
    diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index aa24485353894..2eed419423c12 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -14,6 +14,7 @@ import { MockAuthMethodsAll, MockBuildInfo, MockDefaultOrganization, + MockDeploymentConfig, MockEntitlements, MockExperiments, MockUser, @@ -78,13 +79,27 @@ const meta = { data: { editWorkspaceProxies: true }, }, { key: ["me", "appearance"], data: MockUserAppearanceSettings }, + { + key: ["deployment", "config"], + data: { + ...MockDeploymentConfig, + config: { + ...MockDeploymentConfig.config, + web_terminal_renderer: "canvas", + }, + }, + }, ], - chromatic: { delay: 300 }, + chromatic: { + diffThreshold: 0.3, + }, }, decorators: [ (Story) => ( - +
    + +
    ), ], diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index f5706a4facc3b..482abc9d6fad1 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -316,6 +316,11 @@ export const TemplateInfoPopover: Story = { ); }); }, + parameters: { + chromatic: { + diffThreshold: 0.3, + }, + }, }; export const TemplateInfoPopoverWithoutDisplayName: Story = { diff --git a/site/src/utils/schedule.tsx b/site/src/utils/schedule.tsx index 97479c021fe8c..21a112137ade0 100644 --- a/site/src/utils/schedule.tsx +++ b/site/src/utils/schedule.tsx @@ -148,7 +148,7 @@ export const autostopDisplay = ( if (template.autostop_requirement && template.allow_user_autostop) { title = Autostop schedule; reason = ( - <> + {" "} because this workspace has enabled autostop. You can disable autostop from this workspace's{" "} @@ -156,18 +156,18 @@ export const autostopDisplay = ( schedule settings . - + ); } return { message: `Stop ${deadline.fromNow()}`, tooltip: ( - <> + {title} This workspace will be stopped on{" "} {deadline.format("MMMM D [at] h:mm A")} {reason} - + ), danger: isShutdownSoon(workspace), }; From c8edadae10989c6aa667ef252abfb1cf585fdbc1 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 17 Apr 2025 10:57:02 -0300 Subject: [PATCH 130/384] refactor: redesign workspace status on workspaces table (#17425) Closes https://github.com/coder/coder/issues/17310 **Before:** Screenshot 2025-04-16 at 11 49 52 **After:** Screenshot 2025-04-16 at 11 49 19 **Notice!** - I've create a new size variation for the badge, `xs`. Since we reduced the line-height for the `text-xs` to be 16px instead of 18px, having a smaller badge, reducing the vertical size and horizontal paddings, just worked better. - I have to update Figma to reflect these changes. I tried, but I was not able to get it working and updated correctly. I'm going to take a pause during this week to learn that. - Updated the destructive, and warning badges to use borders as defined in the designs [here](https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=489-3472&t=gfnYeLOIFUqHx6qv-0). --- site/src/components/Badge/Badge.tsx | 5 +- site/src/index.css | 6 +- .../WorkspaceDormantBadge.tsx | 12 +-- .../pages/WorkspacesPage/WorkspacesTable.tsx | 99 +++++++++++++------ site/src/utils/workspace.tsx | 37 ++++++- site/tailwind.config.js | 1 + 6 files changed, 119 insertions(+), 41 deletions(-) diff --git a/site/src/components/Badge/Badge.tsx b/site/src/components/Badge/Badge.tsx index 7ee7cc4f94fe0..e405966c8c235 100644 --- a/site/src/components/Badge/Badge.tsx +++ b/site/src/components/Badge/Badge.tsx @@ -17,9 +17,12 @@ export const badgeVariants = cva( default: "border-transparent bg-surface-secondary text-content-secondary shadow", warning: - "border-transparent bg-surface-orange text-content-warning shadow", + "border border-solid border-border-warning bg-surface-orange text-content-warning shadow", + destructive: + "border border-solid border-border-destructive bg-surface-red text-content-highlight-red shadow", }, size: { + xs: "text-2xs font-regular h-5 [&_svg]:hidden rounded px-1.5", sm: "text-2xs font-regular h-5.5 [&_svg]:size-icon-xs", md: "text-xs font-medium [&_svg]:size-icon-sm", }, diff --git a/site/src/index.css b/site/src/index.css index fe8699bc62b07..e2b71d7be6516 100644 --- a/site/src/index.css +++ b/site/src/index.css @@ -28,11 +28,13 @@ --surface-grey: 240 5% 96%; --surface-orange: 34 100% 92%; --surface-sky: 201 94% 86%; + --surface-red: 0 93% 94%; --border-default: 240 6% 90%; --border-success: 142 76% 36%; --border-warning: 30.66, 97.16%, 72.35%; --border-destructive: 0 84% 60%; - --border-hover: 240, 5%, 34%; + --border-warning: 27 96% 61%; + --border-hover: 240 5% 34%; --overlay-default: 240 5% 84% / 80%; --radius: 0.5rem; --highlight-purple: 262 83% 58%; @@ -66,10 +68,12 @@ --surface-grey: 240 6% 10%; --surface-orange: 13 81% 15%; --surface-sky: 204 80% 16%; + --surface-red: 0 75% 15%; --border-default: 240 4% 16%; --border-success: 142 76% 36%; --border-warning: 30.66, 97.16%, 72.35%; --border-destructive: 0 91% 71%; + --border-warning: 31 97% 72%; --border-hover: 240, 5%, 34%; --overlay-default: 240 10% 4% / 80%; --highlight-purple: 252 95% 85%; diff --git a/site/src/modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge.tsx b/site/src/modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge.tsx index 9c87cd4eae01c..f3c9c80d085fd 100644 --- a/site/src/modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge.tsx +++ b/site/src/modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge.tsx @@ -1,8 +1,6 @@ -import AutoDeleteIcon from "@mui/icons-material/AutoDelete"; -import RecyclingIcon from "@mui/icons-material/Recycling"; import Tooltip from "@mui/material/Tooltip"; import type { Workspace } from "api/typesGenerated"; -import { Pill } from "components/Pill/Pill"; +import { Badge } from "components/Badge/Badge"; import { formatDistanceToNow } from "date-fns"; import type { FC } from "react"; @@ -35,9 +33,9 @@ export const WorkspaceDormantBadge: FC = ({ } > - } type="error"> + Deletion Pending - + ) : ( = ({ } > - } type="warning"> + Dormant - + ); }; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 9fe72c23910e5..a9d585fccf58c 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -13,6 +13,11 @@ import { AvatarData } from "components/Avatar/AvatarData"; import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton"; import { InfoTooltip } from "components/InfoTooltip/InfoTooltip"; import { Stack } from "components/Stack/Stack"; +import { + StatusIndicator, + StatusIndicatorDot, + type StatusIndicatorProps, +} from "components/StatusIndicator/StatusIndicator"; import { Table, TableBody, @@ -25,19 +30,26 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; import { useClickableTableRow } from "hooks/useClickableTableRow"; import { useDashboard } from "modules/dashboard/useDashboard"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge"; import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; -import { WorkspaceStatusBadge } from "modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge"; -import { LastUsed } from "pages/WorkspacesPage/LastUsed"; import { type FC, type ReactNode, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { cn } from "utils/cn"; -import { getDisplayWorkspaceTemplateName } from "utils/workspace"; +import { + type DisplayWorkspaceStatusType, + getDisplayWorkspaceStatus, + getDisplayWorkspaceTemplateName, + lastUsedMessage, +} from "utils/workspace"; import { WorkspacesEmpty } from "./WorkspacesEmpty"; +dayjs.extend(relativeTime); + export interface WorkspacesTableProps { workspaces?: readonly Workspace[]; checkedWorkspaces: readonly Workspace[]; @@ -125,8 +137,7 @@ export const WorkspacesTable: FC = ({ {hasAppStatus && Activity} Template - Last used - Status + Status @@ -248,26 +259,7 @@ export const WorkspacesTable: FC = ({ /> - - - - - -
    - - {workspace.latest_build.status === "running" && - !workspace.health.healthy && ( - - )} - {workspace.dormant_at && ( - - )} -
    -
    +
    @@ -345,14 +337,11 @@ const TableLoader: FC = ({ canCheckWorkspaces }) => { - - - - - + + - + @@ -362,3 +351,51 @@ const TableLoader: FC = ({ canCheckWorkspaces }) => { const cantBeChecked = (workspace: Workspace) => { return ["deleting", "pending"].includes(workspace.latest_build.status); }; + +type WorkspaceStatusCellProps = { + workspace: Workspace; +}; + +const variantByStatusType: Record< + DisplayWorkspaceStatusType, + StatusIndicatorProps["variant"] +> = { + 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.dormant_at && ( + + )} + + + {lastUsedMessage(workspace.last_used_at)} + +
    +
    + ); +}; diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 963adf58a7270..32fd6ce153d0e 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -168,14 +168,29 @@ export const getDisplayWorkspaceTemplateName = ( : workspace.template_name; }; +export type DisplayWorkspaceStatusType = + | "success" + | "active" + | "inactive" + | "error" + | "warning" + | "danger"; + +type DisplayWorkspaceStatus = { + text: string; + type: DisplayWorkspaceStatusType; + icon: React.ReactNode; +}; + export const getDisplayWorkspaceStatus = ( workspaceStatus: TypesGen.WorkspaceStatus, provisionerJob?: TypesGen.ProvisionerJob, -) => { +): DisplayWorkspaceStatus => { switch (workspaceStatus) { case undefined: return { text: "Loading", + type: "active", icon: , } as const; case "running": @@ -307,3 +322,23 @@ const FALLBACK_ICON = "/icon/widgets.svg"; export const getResourceIconPath = (resourceType: string): string => { return BUILT_IN_ICON_PATHS[resourceType] ?? FALLBACK_ICON; }; + +export const lastUsedMessage = (lastUsedAt: string | Date): string => { + const t = dayjs(lastUsedAt); + const now = dayjs(); + let message = t.fromNow(); + + if (t.isAfter(now.subtract(1, "hour"))) { + message = "Now"; + } else if (t.isAfter(now.subtract(3, "day"))) { + message = t.fromNow(); + } else if (t.isAfter(now.subtract(1, "month"))) { + message = t.fromNow(); + } else if (t.isAfter(now.subtract(100, "year"))) { + message = t.fromNow(); + } else { + message = "Never"; + } + + return message; +}; diff --git a/site/tailwind.config.js b/site/tailwind.config.js index 3e612408596f5..142a4711b56f3 100644 --- a/site/tailwind.config.js +++ b/site/tailwind.config.js @@ -49,6 +49,7 @@ module.exports = { grey: "hsl(var(--surface-grey))", orange: "hsl(var(--surface-orange))", sky: "hsl(var(--surface-sky))", + red: "hsl(var(--surface-red))", }, border: { DEFAULT: "hsl(var(--border-default))", From 5e4050e529130ad1ec164dce1f4244796c8ac5bf Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 17 Apr 2025 11:08:13 -0300 Subject: [PATCH 131/384] chore: fix additional storybook flakes (#17450) Fix new storybook flakes catch by https://www.chromatic.com/test?appId=624de63c6aacee003aa84340&id=680107825818a9747e57236c --- .../CustomRolesPage/PermissionPillsList.stories.tsx | 5 +++++ site/src/pages/TerminalPage/TerminalPage.stories.tsx | 2 +- site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx index 5a4d896c2c425..56eb382067d84 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/PermissionPillsList.stories.tsx @@ -13,6 +13,11 @@ const meta: Meta = {
    ), ], + parameters: { + chromatic: { + diffThreshold: 0.5, + }, + }, }; export default meta; diff --git a/site/src/pages/TerminalPage/TerminalPage.stories.tsx b/site/src/pages/TerminalPage/TerminalPage.stories.tsx index 2eed419423c12..7a34d57fbf83d 100644 --- a/site/src/pages/TerminalPage/TerminalPage.stories.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.stories.tsx @@ -91,7 +91,7 @@ const meta = { }, ], chromatic: { - diffThreshold: 0.3, + diffThreshold: 0.5, }, }, decorators: [ diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index 482abc9d6fad1..1ae3ff9e2ebc9 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -38,6 +38,9 @@ const meta: Meta = { parameters: { layout: "fullscreen", features: ["advanced_template_scheduling"], + chromatic: { + diffThreshold: 0.3, + }, }, }; From 8723fe99f5b9512f1931db7731ac40721ca9d159 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 17 Apr 2025 17:40:37 +0100 Subject: [PATCH 132/384] feat: add slider to dynamic parameters (#17453) This adds the slider to the dynamic parameters component and does some additional styling cleanup for the dynamic parameters form Screenshot 2025-04-17 at 16 54 05 --- site/src/components/Checkbox/Checkbox.tsx | 6 ++--- .../DynamicParameter/DynamicParameter.tsx | 25 ++++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/site/src/components/Checkbox/Checkbox.tsx b/site/src/components/Checkbox/Checkbox.tsx index 6bc1338955122..1278fb12ea899 100644 --- a/site/src/components/Checkbox/Checkbox.tsx +++ b/site/src/components/Checkbox/Checkbox.tsx @@ -18,7 +18,7 @@ export const Checkbox = React.forwardRef<
    {(props.checked === true || props.defaultChecked === true) && ( - + )} {props.checked === "indeterminate" && ( - + )}
    diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index e1e79bdcd7a06..223c541bcfe9b 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -22,6 +22,7 @@ import { SelectTrigger, SelectValue, } from "components/Select/Select"; +import { Slider } from "components/Slider/Slider"; import { Switch } from "components/Switch/Switch"; import { Tooltip, @@ -91,7 +92,7 @@ const ParameterLabel: FC = ({ parameter, isPreset }) => { )} -
    +
    {hasDescription && ( @@ -278,6 +284,23 @@ const ParameterField: FC = ({
    ); + + case "slider": + return ( + onChange(value.toString())} + min={parameter.validations[0]?.validation_min ?? 0} + max={parameter.validations[0]?.validation_max ?? 100} + disabled={disabled} + /> + ); + case "input": { const inputType = parameter.type === "number" ? "number" : "text"; const inputProps: Record = {}; From 144c60dd87e314afef664360e8a2a371551839f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Thu, 17 Apr 2025 10:17:09 -0700 Subject: [PATCH 133/384] chore: upgrade fish to v4 (#17440) --- .../files/etc/apt/sources.list.d/ppa.list | 2 +- .../files/usr/share/keyrings/fish-shell.gpg | Bin 371 -> 1163 bytes dogfood/coder/update-keys.sh | 8 ++++---- site/src/modules/resources/AgentLogs/mocks.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dogfood/coder/files/etc/apt/sources.list.d/ppa.list b/dogfood/coder/files/etc/apt/sources.list.d/ppa.list index a0d67bd17895a..fbdbef53ea60a 100644 --- a/dogfood/coder/files/etc/apt/sources.list.d/ppa.list +++ b/dogfood/coder/files/etc/apt/sources.list.d/ppa.list @@ -1,6 +1,6 @@ deb [signed-by=/usr/share/keyrings/ansible.gpg] https://ppa.launchpadcontent.net/ansible/ansible/ubuntu jammy main -deb [signed-by=/usr/share/keyrings/fish-shell.gpg] https://ppa.launchpadcontent.net/fish-shell/release-3/ubuntu/ jammy main +deb [signed-by=/usr/share/keyrings/fish-shell.gpg] https://ppa.launchpadcontent.net/fish-shell/release-4/ubuntu/ jammy main deb [signed-by=/usr/share/keyrings/git-core.gpg] https://ppa.launchpadcontent.net/git-core/ppa/ubuntu jammy main diff --git a/dogfood/coder/files/usr/share/keyrings/fish-shell.gpg b/dogfood/coder/files/usr/share/keyrings/fish-shell.gpg index 58ed31417d174aa9af164185a087044442ff9de0..bcaac170cb9d7fd284af9280110ba00cedba818d 100644 GIT binary patch delta 1132 zcmV-y1e5#o0*eWM#=%VlW;Bbz0T2MY^RR@UM%(osrGpfsz)b6XL5kRQUs8mhDk6w3 zRX?Zc@N^e(%%g@9*>*d{xI^NZlK~qKiDe|C#fLb>*e8$Lq)K=5%cdr1DiACD)O(Ut zFM6S|9*TFldmR{DY&QL~(NQudyWuu63>GS=l)jO#9ooW*~@btqy%}v0gZ4Q=;Xcc2#Gy{w{aYRUl?cEijQaxl(;e(4aZ9+f|JGj({eJSDZ1;bo$0507`Ks$w7N*|H$h zA|<(yt(l4Knf11m>Pg4lJe4%-J-UU&(5`*HuJP@EL0(mPxcwx`uPW&|m~QiuF<>xy zDnF>pm0GMt;GbizmAQi8;*F-#y@mS=`(^Q0VZf>^?3}YVPtxF6 z7GbSm0TENPTx}N~xy~ZmO;w&;panfZy77}9grJ+(Ezrhge56Z;MrQgf@Wi^YRUj+} z@YyF1dzPn#>?ci9Py6Herg3UxJmetVr1fY$_C2JeK##91ujXD-lRj^C*8gkpM_& za-9wkBp=h!6_~`$K(Dm#VQ}TL z5*VJUl!H{(`H9d11*VE4WviWVV&X$7wT4We>~%Pnf!&iO3SUUVO)nhuPbo&Q>A}(D zD2?e6npIl%wa&?pfrZii6heQujMd}$a9?Gj3T6QWXIGza_L=ZLIM>9+cmsYqb6f;G z4xeV-u*d>yWoV>h9j{d=r*y*JiyvZGn%;JmOehliB31tk(wYjcW6k;dd=aCJrUQ{n z%dJJ6)t%At;xTe(>QxTiG$&qxZd$rACWd9HPo3f)$;^1R><>LP`v`vm)Of@+SMjo^ zPIt=sEiVfJ3!;XMtDwb9;B6^s?Pa1oM%Z1K<8QJVq}hsI+pm8hX!5%^iN~{MMJARx zb|-Gzg(4JX4R+!drt2g(2g4s-M_jfglnO(gx_Xqj7l&({o+y#ia8(Cmnexhh{V$bE zXQm~=iT$Z<75uy3oo!T!nzhqZSj2_(C=zWMa!(WnrKj5m(rN_A*rk;7vLH8e1?>U3 y5+cL_KtB!kpa%#e3Y0}3#Z-A}jEy-Ev)LmD`7a5}#~Pm?Sy-kvTw|J$nX>s%^&bcT delta 335 zcmV-V0kHmy3G)Jf#*GA06gHs&1OTyA)UL$VZY7%RSYKH5W=zM4Afe#OR}OwmNs6f; zpLD-+H~%WPQm7Pm%m|aq|L+WFibhLF@Bm?UI!mf1pnip+A}B^~$n_Y>wToM&Mb4Ph zOz3?50xg^kqdYUQ&|h|t2Q;g+y=#`wsR+KqT*5}cIvkh_nLcOJ3DkvOz*gy#3j#2I zxC9dc0stZf0#Xz3qRs=o66^;V@Avlz0%VqdFDBy7BV5$y;&Ujmd=`KJMt^Jc$W^uqCwyGUL hbHXcyS^i>AHHuYNG$;&v69qdjViXA|p;^rN_OJG>i$MSY diff --git a/dogfood/coder/update-keys.sh b/dogfood/coder/update-keys.sh index 10b2660b5f58b..4d45f348bfcda 100755 --- a/dogfood/coder/update-keys.sh +++ b/dogfood/coder/update-keys.sh @@ -18,7 +18,7 @@ gpg_flags=( pushd "$PROJECT_ROOT/dogfood/coder/files/usr/share/keyrings" # Ansible PPA signing key -curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x6125e2a8c77f2818fb7bd15b93c4a3fd7bb9c367" | +curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0X6125E2A8C77F2818FB7BD15B93C4A3FD7BB9C367" | gpg "${gpg_flags[@]}" --output="ansible.gpg" # Upstream Docker signing key @@ -26,7 +26,7 @@ curl "${curl_flags[@]}" "https://download.docker.com/linux/ubuntu/gpg" | gpg "${gpg_flags[@]}" --output="docker.gpg" # Fish signing key -curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x59fda1ce1b84b3fad89366c027557f056dc33ca5" | +curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x88421E703EDC7AF54967DED473C9FCC9E2BB48DA" | gpg "${gpg_flags[@]}" --output="fish-shell.gpg" # Git-Core signing key @@ -50,7 +50,7 @@ curl "${curl_flags[@]}" "https://apt.releases.hashicorp.com/gpg" | gpg "${gpg_flags[@]}" --output="hashicorp.gpg" # Helix signing key -curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x27642b9fd7f1a161fc2524e3355a4fa515d7c855" | +curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x27642B9FD7F1A161FC2524E3355A4FA515D7C855" | gpg "${gpg_flags[@]}" --output="helix.gpg" # Microsoft repository signing key (Edge) @@ -58,7 +58,7 @@ curl "${curl_flags[@]}" "https://packages.microsoft.com/keys/microsoft.asc" | gpg "${gpg_flags[@]}" --output="microsoft.gpg" # Neovim signing key -curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9dbb0be9366964f134855e2255f96fcf8231b6dd" | +curl "${curl_flags[@]}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9DBB0BE9366964F134855E2255F96FCF8231B6DD" | gpg "${gpg_flags[@]}" --output="neovim.gpg" # NodeSource signing key diff --git a/site/src/modules/resources/AgentLogs/mocks.tsx b/site/src/modules/resources/AgentLogs/mocks.tsx index 059e01fdbad64..de08e816614c0 100644 --- a/site/src/modules/resources/AgentLogs/mocks.tsx +++ b/site/src/modules/resources/AgentLogs/mocks.tsx @@ -612,7 +612,7 @@ export const MockLogs = [ id: 3295813, level: "info", output: - "Hit:16 https://ppa.launchpadcontent.net/fish-shell/release-3/ubuntu jammy InRelease", + "Hit:16 https://ppa.launchpadcontent.net/fish-shell/release-4/ubuntu jammy InRelease", time: "2024-03-14T11:31:07.827832Z", sourceId: "d9475581-8a42-4bce-b4d0-e4d2791d5c98", }, From 183146e2c981223747eb38be5e16ef00e1c97587 Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Thu, 17 Apr 2025 13:43:24 -0400 Subject: [PATCH 134/384] fix: add minor fix to reconciliation loop (#17454) Follow-up PR to https://github.com/coder/coder/pull/17261 I noticed that 1 metrics-related test fails in `dk/prebuilds` after merging my PR into `dk/prebuilds`. --- coderd/prebuilds/preset_snapshot.go | 5 +++-- coderd/prebuilds/preset_snapshot_test.go | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/coderd/prebuilds/preset_snapshot.go b/coderd/prebuilds/preset_snapshot.go index b6f05e588a6c0..2db9694f7f376 100644 --- a/coderd/prebuilds/preset_snapshot.go +++ b/coderd/prebuilds/preset_snapshot.go @@ -91,9 +91,10 @@ func (p PresetSnapshot) CalculateState() *ReconciliationState { extraneous int32 ) + // #nosec G115 - Safe conversion as p.Running slice length is expected to be within int32 range + actual = int32(len(p.Running)) + if p.isActive() { - // #nosec G115 - Safe conversion as p.Running slice length is expected to be within int32 range - actual = int32(len(p.Running)) desired = p.Preset.DesiredInstances.Int32 eligible = p.countEligible() extraneous = max(actual-desired, 0) diff --git a/coderd/prebuilds/preset_snapshot_test.go b/coderd/prebuilds/preset_snapshot_test.go index cce8ea67cb05c..a5acb40e5311f 100644 --- a/coderd/prebuilds/preset_snapshot_test.go +++ b/coderd/prebuilds/preset_snapshot_test.go @@ -146,7 +146,9 @@ func TestOutdatedPrebuilds(t *testing.T) { state := ps.CalculateState() actions, err := ps.CalculateActions(clock, backoffInterval) require.NoError(t, err) - validateState(t, prebuilds.ReconciliationState{}, *state) + validateState(t, prebuilds.ReconciliationState{ + Actual: 1, + }, *state) validateActions(t, prebuilds.ReconciliationActions{ ActionType: prebuilds.ActionTypeDelete, DeleteIDs: []uuid.UUID{outdated.prebuiltWorkspaceID}, @@ -208,6 +210,7 @@ func TestDeleteOutdatedPrebuilds(t *testing.T) { actions, err := ps.CalculateActions(clock, backoffInterval) require.NoError(t, err) validateState(t, prebuilds.ReconciliationState{ + Actual: 1, Deleting: 1, }, *state) @@ -530,7 +533,9 @@ func TestDeprecated(t *testing.T) { state := ps.CalculateState() actions, err := ps.CalculateActions(clock, backoffInterval) require.NoError(t, err) - validateState(t, prebuilds.ReconciliationState{}, *state) + validateState(t, prebuilds.ReconciliationState{ + Actual: 1, + }, *state) validateActions(t, prebuilds.ReconciliationActions{ ActionType: prebuilds.ActionTypeDelete, DeleteIDs: []uuid.UUID{current.prebuiltWorkspaceID}, From 90eacc17de890517cf270dc200f95f2e48e645c7 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 17 Apr 2025 21:16:08 +0100 Subject: [PATCH 135/384] fix: fix issues with dynamic parameters in the state (#17459) --- .../CreateWorkspacePageViewExperimental.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index 3674884c1fb37..1e0fbbf2281ff 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -117,8 +117,8 @@ export const CreateWorkspacePageViewExperimental: FC< rich_parameter_values: useValidationSchemaForDynamicParameters(parameters), }), - enableReinitialize: true, - validateOnChange: false, + enableReinitialize: false, + validateOnChange: true, validateOnBlur: true, onSubmit: (request) => { if (!hasAllRequiredExternalAuth) { From ea65ddc17d208e9b0a9215eeb83882bcd81790fe Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 17 Apr 2025 15:42:23 -0500 Subject: [PATCH 136/384] fix: correct user roles being passed into terraform context (#17460) Roles were being passed into the workspace context incorrectly. Site wide scopes were being org scoped. Roles outside the org should also not be sent. --- .../provisionerdserver/provisionerdserver.go | 19 ++++++++---- .../provisionerdserver_test.go | 31 +++++++++++++++++-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index a4e28741ce988..78f597fa55369 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -595,17 +595,24 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo }) } - roles, err := s.Database.GetAuthorizationUserRoles(ctx, owner.ID) + allUserRoles, err := s.Database.GetAuthorizationUserRoles(ctx, owner.ID) if err != nil { return nil, failJob(fmt.Sprintf("get owner authorization roles: %s", err)) } ownerRbacRoles := []*sdkproto.Role{} - for _, role := range roles.Roles { - if s.OrganizationID == uuid.Nil { - ownerRbacRoles = append(ownerRbacRoles, &sdkproto.Role{Name: role, OrgId: ""}) - continue + roles, err := allUserRoles.RoleNames() + if err == nil { + for _, role := range roles { + if role.OrganizationID != uuid.Nil && role.OrganizationID != s.OrganizationID { + continue // Only include site wide and org specific roles + } + + orgID := role.OrganizationID.String() + if role.OrganizationID == uuid.Nil { + orgID = "" + } + ownerRbacRoles = append(ownerRbacRoles, &sdkproto.Role{Name: role.Name, OrgId: orgID}) } - ownerRbacRoles = append(ownerRbacRoles, &sdkproto.Role{Name: role, OrgId: s.OrganizationID.String()}) } protoJob.Type = &proto.AcquiredJob_WorkspaceBuild_{ diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 9a9eb91ac8b73..caeef8a9793b7 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "io" "net/url" + "slices" "strconv" "strings" "sync" @@ -22,6 +23,7 @@ import ( "storj.io/drpc" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/quartz" "github.com/coder/serpent" @@ -203,6 +205,20 @@ func TestAcquireJob(t *testing.T) { GroupID: group1.ID, }) require.NoError(t, err) + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + UserID: user.ID, + OrganizationID: pd.OrganizationID, + Roles: []string{rbac.RoleOrgAuditor()}, + }) + + // Add extra erronous roles + secondOrg := dbgen.Organization(t, db, database.Organization{}) + dbgen.OrganizationMember(t, db, database.OrganizationMember{ + UserID: user.ID, + OrganizationID: secondOrg.ID, + Roles: []string{rbac.RoleOrgAuditor()}, + }) + link := dbgen.UserLink(t, db, database.UserLink{ LoginType: database.LoginTypeOIDC, UserID: user.ID, @@ -350,7 +366,7 @@ func TestAcquireJob(t *testing.T) { WorkspaceOwnerEmail: user.Email, WorkspaceOwnerName: user.Name, WorkspaceOwnerOidcAccessToken: link.OAuthAccessToken, - WorkspaceOwnerGroups: []string{group1.Name}, + WorkspaceOwnerGroups: []string{"Everyone", group1.Name}, WorkspaceId: workspace.ID.String(), WorkspaceOwnerId: user.ID.String(), TemplateId: template.ID.String(), @@ -361,11 +377,15 @@ func TestAcquireJob(t *testing.T) { WorkspaceOwnerSshPrivateKey: sshKey.PrivateKey, WorkspaceBuildId: build.ID.String(), WorkspaceOwnerLoginType: string(user.LoginType), - WorkspaceOwnerRbacRoles: []*sdkproto.Role{{Name: "member", OrgId: pd.OrganizationID.String()}}, + 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 } + + slices.SortFunc(wantedMetadata.WorkspaceOwnerRbacRoles, func(a, b *sdkproto.Role) int { + return strings.Compare(a.Name+a.OrgId, b.Name+b.OrgId) + }) want, err := json.Marshal(&proto.AcquiredJob_WorkspaceBuild_{ WorkspaceBuild: &proto.AcquiredJob_WorkspaceBuild{ WorkspaceBuildId: build.ID.String(), @@ -467,6 +487,13 @@ func TestAcquireJob(t *testing.T) { job, err := tc.acquire(ctx, srv) require.NoError(t, err) + // sort + if wk, ok := job.Type.(*proto.AcquiredJob_WorkspaceBuild_); ok { + slices.SortFunc(wk.WorkspaceBuild.Metadata.WorkspaceOwnerRbacRoles, func(a, b *sdkproto.Role) int { + return strings.Compare(a.Name+a.OrgId, b.Name+b.OrgId) + }) + } + got, err := json.Marshal(job.Type) require.NoError(t, err) From 2cc56ab5156287b83049ab6977215832c390a907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Thu, 17 Apr 2025 13:51:50 -0700 Subject: [PATCH 137/384] chore: fill out workspace owner data for dynamic parameters (#17366) --- coderd/apidoc/docs.go | 66 +++-- coderd/apidoc/swagger.json | 62 +++-- coderd/coderd.go | 15 +- coderd/parameters.go | 250 ++++++++++++++++++ coderd/parameters_test.go | 134 ++++++++++ coderd/templateversions.go | 133 ---------- coderd/templateversions_test.go | 72 ----- .../groups/main.tf | 4 - .../groups/plan.json | 24 +- coderd/testdata/parameters/public_key/main.tf | 14 + .../testdata/parameters/public_key/plan.json | 80 ++++++ codersdk/parameters.go | 28 ++ codersdk/templateversions.go | 18 -- docs/reference/api/templates.md | 53 ++-- go.mod | 7 +- go.sum | 16 +- site/src/api/typesGenerated.ts | 4 +- 17 files changed, 635 insertions(+), 345 deletions(-) create mode 100644 coderd/parameters.go create mode 100644 coderd/parameters_test.go rename coderd/testdata/{dynamicparameters => parameters}/groups/main.tf (85%) rename coderd/testdata/{dynamicparameters => parameters}/groups/plan.json (76%) create mode 100644 coderd/testdata/parameters/public_key/main.tf create mode 100644 coderd/testdata/parameters/public_key/plan.json create mode 100644 codersdk/parameters.go diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index dcb7eba98b653..268cfd7a894ba 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -5686,35 +5686,6 @@ const docTemplate = `{ } } }, - "/templateversions/{templateversion}/dynamic-parameters": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "tags": [ - "Templates" - ], - "summary": "Open dynamic parameters WebSocket by template version", - "operationId": "open-dynamic-parameters-websocket-by-template-version", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Template version ID", - "name": "templateversion", - "in": "path", - "required": true - } - ], - "responses": { - "101": { - "description": "Switching Protocols" - } - } - } - }, "/templateversions/{templateversion}/external-auth": { "get": { "security": [ @@ -7570,6 +7541,43 @@ const docTemplate = `{ } } }, + "/users/{user}/templateversions/{templateversion}/parameters": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": [ + "Templates" + ], + "summary": "Open dynamic parameters WebSocket by template version", + "operationId": "open-dynamic-parameters-websocket-by-template-version", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "user", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "templateversion", + "in": "path", + "required": true + } + ], + "responses": { + "101": { + "description": "Switching Protocols" + } + } + } + }, "/users/{user}/webpush/subscription": { "post": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0464733070ef3..e973f11849547 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5029,33 +5029,6 @@ } } }, - "/templateversions/{templateversion}/dynamic-parameters": { - "get": { - "security": [ - { - "CoderSessionToken": [] - } - ], - "tags": ["Templates"], - "summary": "Open dynamic parameters WebSocket by template version", - "operationId": "open-dynamic-parameters-websocket-by-template-version", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "Template version ID", - "name": "templateversion", - "in": "path", - "required": true - } - ], - "responses": { - "101": { - "description": "Switching Protocols" - } - } - } - }, "/templateversions/{templateversion}/external-auth": { "get": { "security": [ @@ -6693,6 +6666,41 @@ } } }, + "/users/{user}/templateversions/{templateversion}/parameters": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "tags": ["Templates"], + "summary": "Open dynamic parameters WebSocket by template version", + "operationId": "open-dynamic-parameters-websocket-by-template-version", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "user", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "Template version ID", + "name": "templateversion", + "in": "path", + "required": true + } + ], + "responses": { + "101": { + "description": "Switching Protocols" + } + } + } + }, "/users/{user}/webpush/subscription": { "post": { "security": [ diff --git a/coderd/coderd.go b/coderd/coderd.go index 72ebce81120fa..e9d7a15a53059 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1108,10 +1108,6 @@ func New(options *Options) *API { // The idea is to return an empty [], so that the coder CLI won't get blocked accidentally. r.Get("/schema", templateVersionSchemaDeprecated) r.Get("/parameters", templateVersionParametersDeprecated) - r.Group(func(r chi.Router) { - r.Use(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentDynamicParameters)) - r.Get("/dynamic-parameters", api.templateVersionDynamicParameters) - }) r.Get("/rich-parameters", api.templateVersionRichParameters) r.Get("/external-auth", api.templateVersionExternalAuth) r.Get("/variables", api.templateVersionVariables) @@ -1177,6 +1173,17 @@ func New(options *Options) *API { // organization member. This endpoint should match the authz story of // postWorkspacesByOrganization r.Post("/workspaces", api.postUserWorkspaces) + + // Similarly to creating a workspace, evaluating parameters for a + // new workspace should also match the authz story of + // postWorkspacesByOrganization + r.Route("/templateversions/{templateversion}", func(r chi.Router) { + r.Use( + httpmw.ExtractTemplateVersionParam(options.Database), + httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentDynamicParameters), + ) + r.Get("/parameters", api.templateVersionDynamicParameters) + }) }) r.Group(func(r chi.Router) { diff --git a/coderd/parameters.go b/coderd/parameters.go new file mode 100644 index 0000000000000..78126789429d2 --- /dev/null +++ b/coderd/parameters.go @@ -0,0 +1,250 @@ +package coderd + +import ( + "context" + "database/sql" + "encoding/json" + "net/http" + "time" + + "github.com/google/uuid" + "golang.org/x/sync/errgroup" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/codersdk/wsjson" + "github.com/coder/preview" + previewtypes "github.com/coder/preview/types" + "github.com/coder/websocket" +) + +// @Summary Open dynamic parameters WebSocket by template version +// @ID open-dynamic-parameters-websocket-by-template-version +// @Security CoderSessionToken +// @Tags Templates +// @Param user path string true "Template version ID" format(uuid) +// @Param templateversion path string true "Template version ID" format(uuid) +// @Success 101 +// @Router /users/{user}/templateversions/{templateversion}/parameters [get] +func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 30*time.Minute) + defer cancel() + user := httpmw.UserParam(r) + templateVersion := httpmw.TemplateVersionParam(r) + + // Check that the job has completed successfully + job, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching provisioner job.", + Detail: err.Error(), + }) + return + } + if !job.CompletedAt.Valid { + httpapi.Write(ctx, rw, http.StatusTooEarly, codersdk.Response{ + Message: "Template version job has not finished", + }) + return + } + + // nolint:gocritic // We need to fetch the templates files for the Terraform + // evaluator, and the user likely does not have permission. + fileCtx := dbauthz.AsProvisionerd(ctx) + fileID, err := api.Database.GetFileIDByTemplateVersionID(fileCtx, templateVersion.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error finding template version Terraform.", + Detail: err.Error(), + }) + return + } + + fs, err := api.FileCache.Acquire(fileCtx, fileID) + defer api.FileCache.Release(fileID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ + Message: "Internal error fetching template version Terraform.", + Detail: err.Error(), + }) + return + } + + // Having the Terraform plan available for the evaluation engine is helpful + // for populating values from data blocks, but isn't strictly required. If + // we don't have a cached plan available, we just use an empty one instead. + plan := json.RawMessage("{}") + tf, err := api.Database.GetTemplateVersionTerraformValues(ctx, templateVersion.ID) + if err == nil { + plan = tf.CachedPlan + } else if !xerrors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Failed to retrieve Terraform values for template version", + Detail: err.Error(), + }) + return + } + + owner, err := api.getWorkspaceOwnerData(ctx, user, templateVersion.OrganizationID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error fetching workspace owner.", + Detail: err.Error(), + }) + return + } + + input := preview.Input{ + PlanJSON: plan, + ParameterValues: map[string]string{}, + Owner: owner, + } + + conn, err := websocket.Accept(rw, r, nil) + if err != nil { + httpapi.Write(ctx, rw, http.StatusUpgradeRequired, codersdk.Response{ + Message: "Failed to accept WebSocket.", + Detail: err.Error(), + }) + return + } + stream := wsjson.NewStream[codersdk.DynamicParametersRequest, codersdk.DynamicParametersResponse]( + conn, + websocket.MessageText, + websocket.MessageText, + api.Logger, + ) + + // Send an initial form state, computed without any user input. + result, diagnostics := preview.Preview(ctx, input, fs) + response := codersdk.DynamicParametersResponse{ + ID: -1, + Diagnostics: previewtypes.Diagnostics(diagnostics), + } + if result != nil { + response.Parameters = result.Parameters + } + err = stream.Send(response) + if err != nil { + stream.Drop() + return + } + + // As the user types into the form, reprocess the state using their input, + // and respond with updates. + updates := stream.Chan() + for { + select { + case <-ctx.Done(): + stream.Close(websocket.StatusGoingAway) + return + case update, ok := <-updates: + if !ok { + // The connection has been closed, so there is no one to write to + return + } + input.ParameterValues = update.Inputs + result, diagnostics := preview.Preview(ctx, input, fs) + response := codersdk.DynamicParametersResponse{ + ID: update.ID, + Diagnostics: previewtypes.Diagnostics(diagnostics), + } + if result != nil { + response.Parameters = result.Parameters + } + err = stream.Send(response) + if err != nil { + stream.Drop() + return + } + } + } +} + +func (api *API) getWorkspaceOwnerData( + ctx context.Context, + user database.User, + organizationID uuid.UUID, +) (previewtypes.WorkspaceOwner, error) { + var g errgroup.Group + + var ownerRoles []previewtypes.WorkspaceOwnerRBACRole + g.Go(func() error { + // nolint:gocritic // This is kind of the wrong query to use here, but it + // matches how the provisioner currently works. We should figure out + // something that needs less escalation but has the correct behavior. + row, err := api.Database.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), user.ID) + if err != nil { + return err + } + roles, err := row.RoleNames() + if err != nil { + return err + } + ownerRoles = make([]previewtypes.WorkspaceOwnerRBACRole, 0, len(roles)) + for _, it := range roles { + if it.OrganizationID != uuid.Nil && it.OrganizationID != organizationID { + continue + } + var orgID string + if it.OrganizationID != uuid.Nil { + orgID = it.OrganizationID.String() + } + ownerRoles = append(ownerRoles, previewtypes.WorkspaceOwnerRBACRole{ + Name: it.Name, + OrgID: orgID, + }) + } + return nil + }) + + var publicKey string + g.Go(func() error { + key, err := api.Database.GetGitSSHKey(ctx, user.ID) + if err != nil { + return err + } + publicKey = key.PublicKey + return nil + }) + + var groupNames []string + g.Go(func() error { + groups, err := api.Database.GetGroups(ctx, database.GetGroupsParams{ + OrganizationID: organizationID, + HasMemberID: user.ID, + }) + if err != nil { + return err + } + groupNames = make([]string, 0, len(groups)) + for _, it := range groups { + groupNames = append(groupNames, it.Group.Name) + } + return nil + }) + + err := g.Wait() + if err != nil { + return previewtypes.WorkspaceOwner{}, err + } + + return previewtypes.WorkspaceOwner{ + ID: user.ID.String(), + Name: user.Username, + FullName: user.Name, + Email: user.Email, + LoginType: string(user.LoginType), + RBACRoles: ownerRoles, + SSHPublicKey: publicKey, + Groups: groupNames, + }, nil +} diff --git a/coderd/parameters_test.go b/coderd/parameters_test.go new file mode 100644 index 0000000000000..60189e9aeaa33 --- /dev/null +++ b/coderd/parameters_test.go @@ -0,0 +1,134 @@ +package coderd_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/coder/v2/testutil" + "github.com/coder/websocket" +) + +func TestDynamicParametersOwnerGroups(t *testing.T) { + t.Parallel() + + cfg := coderdtest.DeploymentValues(t) + cfg.Experiments = []string{string(codersdk.ExperimentDynamicParameters)} + ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: cfg}) + owner := coderdtest.CreateFirstUser(t, ownerClient) + templateAdmin, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/groups/main.tf") + require.NoError(t, err) + dynamicParametersTerraformPlan, err := os.ReadFile("testdata/parameters/groups/plan.json") + require.NoError(t, err) + + files := echo.WithExtraFiles(map[string][]byte{ + "main.tf": dynamicParametersTerraformSource, + }) + files.ProvisionPlan = []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Plan: dynamicParametersTerraformPlan, + }, + }, + }} + + version := coderdtest.CreateTemplateVersion(t, templateAdmin, owner.OrganizationID, files) + coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID) + _ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) + + ctx := testutil.Context(t, testutil.WaitShort) + stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, templateAdminUser.ID, version.ID) + require.NoError(t, err) + defer stream.Close(websocket.StatusGoingAway) + + previews := stream.Chan() + + // Should automatically send a form state with all defaulted/empty values + preview := testutil.RequireReceive(ctx, t, previews) + require.Equal(t, -1, preview.ID) + require.Empty(t, preview.Diagnostics) + require.Equal(t, "group", preview.Parameters[0].Name) + require.True(t, preview.Parameters[0].Value.Valid()) + require.Equal(t, "Everyone", preview.Parameters[0].Value.Value.AsString()) + + // Send a new value, and see it reflected + err = stream.Send(codersdk.DynamicParametersRequest{ + ID: 1, + Inputs: map[string]string{"group": "Bloob"}, + }) + require.NoError(t, err) + preview = testutil.RequireReceive(ctx, t, previews) + require.Equal(t, 1, preview.ID) + require.Empty(t, preview.Diagnostics) + require.Equal(t, "group", preview.Parameters[0].Name) + require.True(t, preview.Parameters[0].Value.Valid()) + require.Equal(t, "Bloob", preview.Parameters[0].Value.Value.AsString()) + + // Back to default + err = stream.Send(codersdk.DynamicParametersRequest{ + ID: 3, + Inputs: map[string]string{}, + }) + require.NoError(t, err) + preview = testutil.RequireReceive(ctx, t, previews) + require.Equal(t, 3, preview.ID) + require.Empty(t, preview.Diagnostics) + require.Equal(t, "group", preview.Parameters[0].Name) + require.True(t, preview.Parameters[0].Value.Valid()) + require.Equal(t, "Everyone", preview.Parameters[0].Value.Value.AsString()) +} + +func TestDynamicParametersOwnerSSHPublicKey(t *testing.T) { + t.Parallel() + + cfg := coderdtest.DeploymentValues(t) + cfg.Experiments = []string{string(codersdk.ExperimentDynamicParameters)} + ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: cfg}) + owner := coderdtest.CreateFirstUser(t, ownerClient) + templateAdmin, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/public_key/main.tf") + require.NoError(t, err) + dynamicParametersTerraformPlan, err := os.ReadFile("testdata/parameters/public_key/plan.json") + require.NoError(t, err) + sshKey, err := templateAdmin.GitSSHKey(t.Context(), "me") + require.NoError(t, err) + + files := echo.WithExtraFiles(map[string][]byte{ + "main.tf": dynamicParametersTerraformSource, + }) + files.ProvisionPlan = []*proto.Response{{ + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Plan: dynamicParametersTerraformPlan, + }, + }, + }} + + version := coderdtest.CreateTemplateVersion(t, templateAdmin, owner.OrganizationID, files) + coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID) + _ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) + + ctx := testutil.Context(t, testutil.WaitShort) + stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, templateAdminUser.ID, version.ID) + require.NoError(t, err) + defer stream.Close(websocket.StatusGoingAway) + + previews := stream.Chan() + + // Should automatically send a form state with all defaulted/empty values + preview := testutil.RequireReceive(ctx, t, previews) + require.Equal(t, -1, preview.ID) + require.Empty(t, preview.Diagnostics) + require.Equal(t, "public_key", preview.Parameters[0].Name) + require.True(t, preview.Parameters[0].Value.Valid()) + require.Equal(t, sshKey.PublicKey, preview.Parameters[0].Value.Value.AsString()) +} diff --git a/coderd/templateversions.go b/coderd/templateversions.go index a60897ddb725a..7b682eac14ea0 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -35,14 +35,10 @@ import ( "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/codersdk/wsjson" "github.com/coder/coder/v2/examples" "github.com/coder/coder/v2/provisioner/terraform/tfparse" "github.com/coder/coder/v2/provisionersdk" sdkproto "github.com/coder/coder/v2/provisionersdk/proto" - "github.com/coder/preview" - previewtypes "github.com/coder/preview/types" - "github.com/coder/websocket" ) // @Summary Get template version by ID @@ -270,135 +266,6 @@ func (api *API) patchCancelTemplateVersion(rw http.ResponseWriter, r *http.Reque }) } -// @Summary Open dynamic parameters WebSocket by template version -// @ID open-dynamic-parameters-websocket-by-template-version -// @Security CoderSessionToken -// @Tags Templates -// @Param templateversion path string true "Template version ID" format(uuid) -// @Success 101 -// @Router /templateversions/{templateversion}/dynamic-parameters [get] -func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - templateVersion := httpmw.TemplateVersionParam(r) - - // Check that the job has completed successfully - job, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID) - if httpapi.Is404Error(err) { - httpapi.ResourceNotFound(rw) - return - } - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching provisioner job.", - Detail: err.Error(), - }) - return - } - if !job.CompletedAt.Valid { - httpapi.Write(ctx, rw, http.StatusTooEarly, codersdk.Response{ - Message: "Template version job has not finished", - }) - return - } - - // Having the Terraform plan available for the evaluation engine is helpful - // for populating values from data blocks, but isn't strictly required. If - // we don't have a cached plan available, we just use an empty one instead. - plan := json.RawMessage("{}") - tf, err := api.Database.GetTemplateVersionTerraformValues(ctx, templateVersion.ID) - if err == nil { - plan = tf.CachedPlan - } - - input := preview.Input{ - PlanJSON: plan, - ParameterValues: map[string]string{}, - // TODO: write a db query that fetches all of the data needed to fill out - // this owner value - Owner: previewtypes.WorkspaceOwner{ - Groups: []string{"Everyone"}, - }, - } - - // nolint:gocritic // We need to fetch the templates files for the Terraform - // evaluator, and the user likely does not have permission. - fileCtx := dbauthz.AsProvisionerd(ctx) - fileID, err := api.Database.GetFileIDByTemplateVersionID(fileCtx, templateVersion.ID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error finding template version Terraform.", - Detail: err.Error(), - }) - return - } - - fs, err := api.FileCache.Acquire(fileCtx, fileID) - defer api.FileCache.Release(fileID) - if err != nil { - httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ - Message: "Internal error fetching template version Terraform.", - Detail: err.Error(), - }) - return - } - - conn, err := websocket.Accept(rw, r, nil) - if err != nil { - httpapi.Write(ctx, rw, http.StatusUpgradeRequired, codersdk.Response{ - Message: "Failed to accept WebSocket.", - Detail: err.Error(), - }) - return - } - - stream := wsjson.NewStream[codersdk.DynamicParametersRequest, codersdk.DynamicParametersResponse](conn, websocket.MessageText, websocket.MessageText, api.Logger) - - // Send an initial form state, computed without any user input. - result, diagnostics := preview.Preview(ctx, input, fs) - response := codersdk.DynamicParametersResponse{ - ID: -1, - Diagnostics: previewtypes.Diagnostics(diagnostics), - } - if result != nil { - response.Parameters = result.Parameters - } - err = stream.Send(response) - if err != nil { - stream.Drop() - return - } - - // As the user types into the form, reprocess the state using their input, - // and respond with updates. - updates := stream.Chan() - for { - select { - case <-ctx.Done(): - stream.Close(websocket.StatusGoingAway) - return - case update, ok := <-updates: - if !ok { - // The connection has been closed, so there is no one to write to - return - } - input.ParameterValues = update.Inputs - result, diagnostics := preview.Preview(ctx, input, fs) - response := codersdk.DynamicParametersResponse{ - ID: update.ID, - Diagnostics: previewtypes.Diagnostics(diagnostics), - } - if result != nil { - response.Parameters = result.Parameters - } - err = stream.Send(response) - if err != nil { - stream.Drop() - return - } - } - } -} - // @Summary Get rich parameters by template version // @ID get-rich-parameters-by-template-version // @Security CoderSessionToken diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 83a5fd67a9761..e4027a1f14605 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "net/http" - "os" "regexp" "strings" "testing" @@ -28,7 +27,6 @@ import ( "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/provisionersdk/proto" "github.com/coder/coder/v2/testutil" - "github.com/coder/websocket" ) func TestTemplateVersion(t *testing.T) { @@ -2134,73 +2132,3 @@ func TestTemplateArchiveVersions(t *testing.T) { require.NoError(t, err, "fetch all versions") require.Len(t, remaining, totalVersions-len(expArchived)-len(allFailed)+1, "remaining versions") } - -func TestTemplateVersionDynamicParameters(t *testing.T) { - t.Parallel() - - cfg := coderdtest.DeploymentValues(t) - cfg.Experiments = []string{string(codersdk.ExperimentDynamicParameters)} - ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: cfg}) - owner := coderdtest.CreateFirstUser(t, ownerClient) - templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin()) - - dynamicParametersTerraformSource, err := os.ReadFile("testdata/dynamicparameters/groups/main.tf") - require.NoError(t, err) - dynamicParametersTerraformPlan, err := os.ReadFile("testdata/dynamicparameters/groups/plan.json") - require.NoError(t, err) - - files := echo.WithExtraFiles(map[string][]byte{ - "main.tf": dynamicParametersTerraformSource, - }) - files.ProvisionPlan = []*proto.Response{{ - Type: &proto.Response_Plan{ - Plan: &proto.PlanComplete{ - Plan: dynamicParametersTerraformPlan, - }, - }, - }} - - version := coderdtest.CreateTemplateVersion(t, templateAdmin, owner.OrganizationID, files) - coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID) - _ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID) - - ctx := testutil.Context(t, testutil.WaitShort) - stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID) - require.NoError(t, err) - defer stream.Close(websocket.StatusGoingAway) - - previews := stream.Chan() - - // Should automatically send a form state with all defaulted/empty values - preview := testutil.TryReceive(ctx, t, previews) - require.Empty(t, preview.Diagnostics) - require.Equal(t, "group", preview.Parameters[0].Name) - require.True(t, preview.Parameters[0].Value.Valid()) - require.Equal(t, "Everyone", preview.Parameters[0].Value.Value.AsString()) - - // Send a new value, and see it reflected - err = stream.Send(codersdk.DynamicParametersRequest{ - ID: 1, - Inputs: map[string]string{"group": "Bloob"}, - }) - require.NoError(t, err) - preview = testutil.TryReceive(ctx, t, previews) - require.Equal(t, 1, preview.ID) - require.Empty(t, preview.Diagnostics) - require.Equal(t, "group", preview.Parameters[0].Name) - require.True(t, preview.Parameters[0].Value.Valid()) - require.Equal(t, "Bloob", preview.Parameters[0].Value.Value.AsString()) - - // Back to default - err = stream.Send(codersdk.DynamicParametersRequest{ - ID: 3, - Inputs: map[string]string{}, - }) - require.NoError(t, err) - preview = testutil.TryReceive(ctx, t, previews) - require.Equal(t, 3, preview.ID) - require.Empty(t, preview.Diagnostics) - require.Equal(t, "group", preview.Parameters[0].Name) - require.True(t, preview.Parameters[0].Value.Valid()) - require.Equal(t, "Everyone", preview.Parameters[0].Value.Value.AsString()) -} diff --git a/coderd/testdata/dynamicparameters/groups/main.tf b/coderd/testdata/parameters/groups/main.tf similarity index 85% rename from coderd/testdata/dynamicparameters/groups/main.tf rename to coderd/testdata/parameters/groups/main.tf index a69b0463bb653..9356cc2840e91 100644 --- a/coderd/testdata/dynamicparameters/groups/main.tf +++ b/coderd/testdata/parameters/groups/main.tf @@ -8,10 +8,6 @@ terraform { data "coder_workspace_owner" "me" {} -output "groups" { - value = data.coder_workspace_owner.me.groups -} - data "coder_parameter" "group" { name = "group" default = try(data.coder_workspace_owner.me.groups[0], "") diff --git a/coderd/testdata/dynamicparameters/groups/plan.json b/coderd/testdata/parameters/groups/plan.json similarity index 76% rename from coderd/testdata/dynamicparameters/groups/plan.json rename to coderd/testdata/parameters/groups/plan.json index 8242f0dc43c58..1a6c45b40b7ab 100644 --- a/coderd/testdata/dynamicparameters/groups/plan.json +++ b/coderd/testdata/parameters/groups/plan.json @@ -17,12 +17,12 @@ "provider_name": "registry.terraform.io/coder/coder", "schema_version": 0, "values": { - "id": "25e81ec3-0eb9-4ee3-8b6d-738b8552f7a9", - "name": "default", - "email": "default@example.com", + "id": "", + "name": "", + "email": "", "groups": [], - "full_name": "default", - "login_type": null, + "full_name": "", + "login_type": "", "rbac_roles": [], "session_token": "", "ssh_public_key": "", @@ -74,19 +74,7 @@ "relevant_attributes": [ { "resource": "data.coder_workspace_owner.me", - "attribute": ["full_name"] - }, - { - "resource": "data.coder_workspace_owner.me", - "attribute": ["email"] - }, - { - "resource": "data.coder_workspace_owner.me", - "attribute": ["id"] - }, - { - "resource": "data.coder_workspace_owner.me", - "attribute": ["name"] + "attribute": ["groups"] } ] } diff --git a/coderd/testdata/parameters/public_key/main.tf b/coderd/testdata/parameters/public_key/main.tf new file mode 100644 index 0000000000000..6dd94d857d1fc --- /dev/null +++ b/coderd/testdata/parameters/public_key/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + } +} + +data "coder_workspace_owner" "me" {} + +data "coder_parameter" "public_key" { + name = "public_key" + default = data.coder_workspace_owner.me.ssh_public_key +} diff --git a/coderd/testdata/parameters/public_key/plan.json b/coderd/testdata/parameters/public_key/plan.json new file mode 100644 index 0000000000000..3ff57d34b1015 --- /dev/null +++ b/coderd/testdata/parameters/public_key/plan.json @@ -0,0 +1,80 @@ +{ + "terraform_version": "1.11.2", + "format_version": "1.2", + "checks": [], + "complete": true, + "timestamp": "2025-04-02T01:29:59Z", + "variables": {}, + "prior_state": { + "values": { + "root_module": { + "resources": [ + { + "mode": "data", + "name": "me", + "type": "coder_workspace_owner", + "address": "data.coder_workspace_owner.me", + "provider_name": "registry.terraform.io/coder/coder", + "schema_version": 0, + "values": { + "id": "", + "name": "", + "email": "", + "groups": [], + "full_name": "", + "login_type": "", + "rbac_roles": [], + "session_token": "", + "ssh_public_key": "", + "ssh_private_key": "", + "oidc_access_token": "" + }, + "sensitive_values": { + "groups": [], + "rbac_roles": [], + "ssh_private_key": true + } + } + ], + "child_modules": [] + } + }, + "format_version": "1.0", + "terraform_version": "1.11.2" + }, + "configuration": { + "root_module": { + "resources": [ + { + "mode": "data", + "name": "me", + "type": "coder_workspace_owner", + "address": "data.coder_workspace_owner.me", + "schema_version": 0, + "provider_config_key": "coder" + } + ], + "variables": {}, + "module_calls": {} + }, + "provider_config": { + "coder": { + "name": "coder", + "full_name": "registry.terraform.io/coder/coder" + } + } + }, + "planned_values": { + "root_module": { + "resources": [], + "child_modules": [] + } + }, + "resource_changes": [], + "relevant_attributes": [ + { + "resource": "data.coder_workspace_owner.me", + "attribute": ["ssh_public_key"] + } + ] +} diff --git a/codersdk/parameters.go b/codersdk/parameters.go new file mode 100644 index 0000000000000..881aaf99f573c --- /dev/null +++ b/codersdk/parameters.go @@ -0,0 +1,28 @@ +package codersdk + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/coder/coder/v2/codersdk/wsjson" + previewtypes "github.com/coder/preview/types" + "github.com/coder/websocket" +) + +// FriendlyDiagnostic is included to guarantee it is generated in the output +// types. This is used as the type override for `previewtypes.Diagnostic`. +type FriendlyDiagnostic = previewtypes.FriendlyDiagnostic + +// NullHCLString is included to guarantee it is generated in the output +// types. This is used as the type override for `previewtypes.HCLString`. +type NullHCLString = previewtypes.NullHCLString + +func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, userID, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) { + conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/users/%s/templateversions/%s/parameters", userID, version), nil) + if err != nil { + return nil, err + } + return wsjson.NewStream[DynamicParametersResponse, DynamicParametersRequest](conn, websocket.MessageText, websocket.MessageText, c.Logger()), nil +} diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go index 0bcc4b5463903..42b381fadebce 100644 --- a/codersdk/templateversions.go +++ b/codersdk/templateversions.go @@ -10,9 +10,7 @@ import ( "github.com/google/uuid" - "github.com/coder/coder/v2/codersdk/wsjson" previewtypes "github.com/coder/preview/types" - "github.com/coder/websocket" ) type TemplateVersionWarning string @@ -141,22 +139,6 @@ type DynamicParametersResponse struct { // TODO: Workspace tags } -// FriendlyDiagnostic is included to guarantee it is generated in the output -// types. This is used as the type override for `previewtypes.Diagnostic`. -type FriendlyDiagnostic = previewtypes.FriendlyDiagnostic - -// NullHCLString is included to guarantee it is generated in the output -// types. This is used as the type override for `previewtypes.HCLString`. -type NullHCLString = previewtypes.NullHCLString - -func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) { - conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/templateversions/%s/dynamic-parameters", version), nil) - if err != nil { - return nil, err - } - return wsjson.NewStream[DynamicParametersResponse, DynamicParametersRequest](conn, websocket.MessageText, websocket.MessageText, c.Logger()), nil -} - // TemplateVersionParameters returns parameters a template version exposes. func (c *Client) TemplateVersionRichParameters(ctx context.Context, version uuid.UUID) ([]TemplateVersionParameter, error) { res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/rich-parameters", version), nil) diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md index 0f21cfccac670..ef136764bf2c5 100644 --- a/docs/reference/api/templates.md +++ b/docs/reference/api/templates.md @@ -2541,32 +2541,6 @@ Status Code **200** To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Open dynamic parameters WebSocket by template version - -### Code samples - -```shell -# Example request using curl -curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/dynamic-parameters \ - -H 'Coder-Session-Token: API_KEY' -``` - -`GET /templateversions/{templateversion}/dynamic-parameters` - -### Parameters - -| Name | In | Type | Required | Description | -|-------------------|------|--------------|----------|---------------------| -| `templateversion` | path | string(uuid) | true | Template version ID | - -### Responses - -| Status | Meaning | Description | Schema | -|--------|--------------------------------------------------------------------------|---------------------|--------| -| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | | - -To perform this operation, you must be authenticated. [Learn more](authentication.md). - ## Get external auth by template version ### Code samples @@ -3325,3 +3299,30 @@ Status Code **200** | `type` | `bool` | To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Open dynamic parameters WebSocket by template version + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/users/{user}/templateversions/{templateversion}/parameters \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /users/{user}/templateversions/{templateversion}/parameters` + +### Parameters + +| Name | In | Type | Required | Description | +|-------------------|------|--------------|----------|---------------------| +| `user` | path | string(uuid) | true | Template version ID | +| `templateversion` | path | string(uuid) | true | Template version ID | + +### Responses + +| Status | Meaning | Description | Schema | +|--------|--------------------------------------------------------------------------|---------------------|--------| +| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/go.mod b/go.mod index 826d5cd2c0235..11da4f20eb80d 100644 --- a/go.mod +++ b/go.mod @@ -139,7 +139,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hc-install v0.9.1 + github.com/hashicorp/hc-install v0.9.2 github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f github.com/hashicorp/terraform-json v0.24.0 github.com/hashicorp/yamux v0.1.2 @@ -245,7 +245,7 @@ require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/ProtonMail/go-crypto v1.1.5 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/akutz/memconn v0.1.0 // indirect @@ -488,7 +488,7 @@ require ( ) require ( - github.com/coder/preview v0.0.0-20250409162646-62939c63c71a + github.com/coder/preview v0.0.0-20250417203026-7edcb9e02d99 github.com/kylecarbs/aisdk-go v0.0.5 github.com/mark3labs/mcp-go v0.20.1 ) @@ -514,7 +514,6 @@ require ( github.com/hashicorp/go-getter v1.7.8 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/liamg/memoryfs v1.6.0 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/openai/openai-go v0.1.0-beta.6 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect diff --git a/go.sum b/go.sum index 1943077cedafd..4bb24abd6be6b 100644 --- a/go.sum +++ b/go.sum @@ -680,8 +680,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4= -github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -907,8 +907,8 @@ github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048 h1:3jzYUlGH7ZELIH4XggX github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= -github.com/coder/preview v0.0.0-20250409162646-62939c63c71a h1:1fvDm7hpNwKDQhHpStp7p1W05/33nBwptGorugNaE94= -github.com/coder/preview v0.0.0-20250409162646-62939c63c71a/go.mod h1:H9BInar+i5VALTTQ9Ulxmn94Eo2fWEhoxd0S9WakDIs= +github.com/coder/preview v0.0.0-20250417203026-7edcb9e02d99 h1:ek8akG49hG304Dsj6T+k8qd4t4rEjUyJ97EiQ9xqkYA= +github.com/coder/preview v0.0.0-20250417203026-7edcb9e02d99/go.mod h1:nyq3UKfaBu4jmA6ddJH05kD5K6paHEMLpRmwLdYJctU= github.com/coder/quartz v0.1.2 h1:PVhc9sJimTdKd3VbygXtS4826EOCpB1fXoRlLnCrE+s= github.com/coder/quartz v0.1.2/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA= github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc= @@ -1369,16 +1369,16 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hc-install v0.9.1 h1:gkqTfE3vVbafGQo6VZXcy2v5yoz2bE0+nhZXruCuODQ= -github.com/hashicorp/hc-install v0.9.1/go.mod h1:pWWvN/IrfeBK4XPeXXYkL6EjMufHkCK5DvwxeLKuBf0= +github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= +github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8l+uRmZHj64= -github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= +github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= +github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index f01cb9c98dc64..d160b7683e92e 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -917,7 +917,7 @@ export const FeatureSets: FeatureSet[] = ["enterprise", "", "premium"]; // From codersdk/files.go export const FormatZip = "zip"; -// From codersdk/templateversions.go +// From codersdk/parameters.go export interface FriendlyDiagnostic { readonly severity: PreviewDiagnosticSeverityString; readonly summary: string; @@ -1401,7 +1401,7 @@ export interface NotificationsWebhookConfig { readonly endpoint: string; } -// From codersdk/templateversions.go +// From codersdk/parameters.go export interface NullHCLString { readonly value: string; readonly valid: boolean; From 5f0ce7f543f6b46b9db3f8e005dca55e45c5e160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Thu, 17 Apr 2025 15:01:44 -0700 Subject: [PATCH 138/384] fix: update url for parameters websocket endpoint (#17462) --- site/src/api/api.ts | 3 ++- .../CreateWorkspacePageExperimental.tsx | 22 +++++++++++------ .../CreateWorkspacePageViewExperimental.tsx | 24 +++++-------------- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index f7e0cd0889f70..fa62afadee608 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1010,6 +1010,7 @@ class ApiMethods { }; templateVersionDynamicParameters = ( + userId: string, versionId: string, { onMessage, @@ -1020,7 +1021,7 @@ class ApiMethods { }, ): WebSocket => { const socket = createWebSocket( - `/api/v2/templateversions/${versionId}/dynamic-parameters`, + `/api/v2/users/${userId}/templateversions/${versionId}/parameters`, ); socket.addEventListener("message", (event) => diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx index 27d76a23a83cd..5711517855ebd 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx @@ -57,6 +57,8 @@ const CreateWorkspacePageExperimental: FC = () => { const [mode, setMode] = useState(() => getWorkspaceMode(searchParams)); const [autoCreateError, setAutoCreateError] = useState(null); + const defaultOwner = me; + const [owner, setOwner] = useState(defaultOwner); const queryClient = useQueryClient(); const autoCreateWorkspaceMutation = useMutation( @@ -96,19 +98,23 @@ const CreateWorkspacePageExperimental: FC = () => { return; } - const socket = API.templateVersionDynamicParameters(realizedVersionId, { - onMessage, - onError: (error) => { - setWsError(error); + const socket = API.templateVersionDynamicParameters( + owner.id, + realizedVersionId, + { + onMessage, + onError: (error) => { + setWsError(error); + }, }, - }); + ); ws.current = socket; return () => { socket.close(); }; - }, [realizedVersionId, onMessage]); + }, [owner.id, realizedVersionId, onMessage]); const sendMessage = useCallback((formValues: Record) => { setWSResponseId((prevId) => { @@ -237,7 +243,9 @@ const CreateWorkspacePageExperimental: FC = () => { defaultName={defaultName} diagnostics={currentResponse?.diagnostics ?? []} disabledParams={disabledParams} - defaultOwner={me} + defaultOwner={defaultOwner} + owner={owner} + setOwner={setOwner} autofillParameters={autofillParameters} error={ wsError || diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index 1e0fbbf2281ff..0b33c27d57434 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -22,14 +22,7 @@ import { useValidationSchemaForDynamicParameters, } from "modules/workspaces/DynamicParameter/DynamicParameter"; import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; -import { - type FC, - useCallback, - useEffect, - useId, - useMemo, - useState, -} from "react"; +import { type FC, useCallback, useEffect, useId, useState } from "react"; import { getFormHelpers, nameValidator } from "utils/formUtils"; import type { AutofillBuildParameter } from "utils/richParameters"; import * as Yup from "yup"; @@ -65,6 +58,8 @@ export interface CreateWorkspacePageViewExperimentalProps { resetMutation: () => void; sendMessage: (message: Record) => void; startPollingExternalAuth: () => void; + owner: TypesGen.User; + setOwner: (user: TypesGen.User) => void; } export const CreateWorkspacePageViewExperimental: FC< @@ -91,8 +86,9 @@ export const CreateWorkspacePageViewExperimental: FC< resetMutation, sendMessage, startPollingExternalAuth, + owner, + setOwner, }) => { - const [owner, setOwner] = useState(defaultOwner); const [suggestedName, setSuggestedName] = useState(() => generateWorkspaceName(), ); @@ -140,14 +136,6 @@ export const CreateWorkspacePageViewExperimental: FC< error, ); - const autofillByName = useMemo( - () => - Object.fromEntries( - autofillParameters.map((param) => [param.name, param]), - ), - [autofillParameters], - ); - const [presetOptions, setPresetOptions] = useState([ { label: "None", value: "" }, ]); @@ -252,7 +240,7 @@ export const CreateWorkspacePageViewExperimental: FC< return ( <> -
    +

  • nj;3EqRdhz`8$^q}7W{)*2 z!M(D#<_cumQ{$FZ+Sk7i%!EfQ2l{p19YsuidgL08u=p%`rp8@G9U4#h-^JkiO#X|V zO#RfQ@!vf0M7=wftxr0bNNEHHs#fifDl`kmmOrZ{&khY8_DZw>0HIOW^Q351Y`knl zC6+H|b?Uj--#Z!L-|zW^wIXVP zg2|joS7O}u2_PQZoQs^xz7j|{269t%A=LZ=9X(0*eVU-jrSa;TXN@2>ciLd26l>8I z^`)*cO)*kH*WS3mxLw^5q;ktsL*r-koF*>s%}I?%EhxnOyB!9srzPuW6i?Z_wW{V* zOJX!YIa^LBjh2$sew}BT{>fs$#nEo)-S40-!Ly>ww%Z=i>k)iNBH1GR{37jz(~kiR z4ycEJ{~WMhH^XSnaTJyq@)n|(@e9y2iLqC|YJYtx8`RE^=F%j?SwEvA(RVg}e{|Si zGXcs3*bNUFfxNFve~o7Xa#lz+3n@9Da_J;^eboY#Ef^BGY)rDQ0%qTqkE<(cG4^^A zsLBF~MEA&JP=&F2Uhrg9ZBTdIqy9qZ_KRw|W!PZ{!g9E{chvLjQeZ86G$_R1*C_Ej z2FgAA_HDTvE30!SdqnOY+uxfoA}Ujb=VhJ-&9lwHjxdWDWF9Q0)F6ZrzssZ>^*( z5ItZLK7k6W5yI4pV(+g)TdKA)ORC+)k{)dUk$1LBEkiFOI&VttjXc={V1E}egE-E7 zr1WEC-mq==f=66~v&Gy8c6P~SgT{9=tCd?kRzRw|gljLyvFc~loWz=9m#E0`y0Bl! z6yTu$Af^+^Q%V4tzlR;80aC{w(^i2Zp=60sV-WnjdyIy_mW1P~j^k>XZRNQ1$Bt9b zOSB67qIwNb+%5C6UUn0E0YSLOlPsL;gy*Wp6}v-AK5CnQj=r}_J6+nXa zC?Uj{ynSzU&kF#kr>mxZYuyEgLew8)SVQ>}K>6bn##Maj)=xh#%}h5-d}+tv*I6|l*g!QoD@HcG(`^{t+}sN zBz5oGJ^v8&-=S6r1;9CegroFCZei)B_wKxiZ7T0{BA4ObuQI3G^U3$*a z?Z(YUUyFTEOk=!6-%iL=HhtA%c>oJHYmI31h-*6YW5owq4{LA>*ON$o2O=eMo~rGu zt1OHMuVUS+4;Pcmq>d*r6IdIdrmU6e+CVH^3Yfz2R+Tn0ZF#7Vw-XF>nxsHarj(-< zOV1_H4s|(sLwxk)sK)aqZdQlU5%fYW?{;5Z#tqY$y7hElV2n=G2W<6K(16qiuzWW^ zIr}G%dX(r(@{4e)F&_Vvs_NW>3-ekHo0o|8ISMLuZ-$i#+Nt3qs1K)08v1RRWhm4IP2$lI`AN!7~>bexi$e&qgi@2RPr8lK>! z{+Y2h0a^!=?tu!qa$;Jcd(q~x7c%MrU5;W?W@gv(AvSO&h+b*~H?^8*JG;-yoZvGU z!PPxDq<>t00%)(QU1jhvRu-^I4Vir+Wjy%|5T0lG z?`ahK(qlZnn~znpCW4i|S9+DFw8j1RrHrG&r8VZ zKqNO(({OWl=v%`W>P=D2?$9{<IZ|{KPPQP5gpP*>UFM(8)M$EzPs3YGND8g4B zZD2f&o=2R}JB}=XoL|$lxa2^&eS)PKGGZsxF)?gUzTPy+xi42GX(`6xSc9N76QRNxFtoy-jWCSO8krbV);eps+8ER@{36 z6wB(AIhGtHsj z$1c7Umtnh(BT^ElD(`W5mE>hTVr_Z%=Sz%4cLWeWZIB%UC)@%!F82IcZ=7dT&T9xq zf_QV3m+k*LB<0}5*LGa)-SWYI6jOCObgIW`gr-~C1Q{Jmy9ZM5Gp7g1XRX~{#c%D~ z7>@=NM!|kI1tP8Lyyy(}UegYz56Iim4qzc0HA-ly&l9_fr`L<8fS0z)O{f+f2=q2r-Rgdrh zoeS!Rroc4a${H@BJ>@_W&=ZKc%<=d5g~d6s6|#=8X>hERFXg;_N7s!xT@UECv2<#f zSRFu`2F$SztGJI;HglZ-Y5dQ~V=*QQijqs2ZrXN3dS%co!jTIVOJQRH9h7`c40&JcyAt<{8ff)82jf z-UHE!i+O%;BuqB9f2z5Tm!v3y!MNsTa+hSBgY!lO$3BAH(_zprBh>* zm$$H|MAW{=F#$r3o2~3hOjN#^0{{7L_wWupMf+4-RD^3ghd(A-t$3V0%uX(7KTENz z>D@F-a@#yQ2{B5(Hq75_Ctd_^C&!o-yyU%Z=$@0l|Gqc6If-VjHPVrUR?VCv%Aa_c zlVXtAs~EEqKuk-SygRV*EjMS!vQnG!xZsXaSE6nGW<+V-Q&nfrG34(_0o*$1@loH` z*T+kN<%!eWw)EJt*#rBAnSZX8p)y=e@sB~hOEfQxFbl14(T%z=S-NNO1@~+t<(XEe;BjPe^M* z4_d+;0sE)|EW>wci8Sg;9VVu%Nf5(!KBrTw>>qdgkK5cApC(`F+;CX|=e?Lm)%qnv zO-OESsHLQGGcM+BJI|3(i^pu-wxhWdB5!4dj1_b5?N{D>`qz`=mIX`D z*k6mD`#s`|U}yT_?fzAe(Iw{1HZXBK-nU-eY_C}WeM@$k?DU;y-sia)ft=$+AmwaA za~;T_+D=3@r;WNSnXh(QO4jpI&C}`$hoIfS67~_1r%uQ-A$U&}h70U}q#X|$*dBr6 zKk}rKU@QH?SC2UaXyrCR2aHircrcnbatYMH+X~I5#vcJbea(7AWTfgn&eBnkEIrX$ zJkGFMU!HrMD)0CvI-L!enroJ;;C~m58h6lzPOMKzko;_H&!N(Uc5EjvEA1(J6#Tz9 zZ<{+bJqsl&NJZ-wXW0Q- z)M3B8KIQe5_1ve`TwJUJpu(ZyssM1{9yL?4J9(TejoSrL@rD-oLIIg(#^Og>AUrwY zY#{b=)#SU|vJAY_E&0+`XVdKT1>c=Rzu9g2G9L4ZXZy}kZ#{gYq+McR4G5 z(NqxAExr}Zw>8?=Cxx9EYzON40iKQ9q=L!E;na(N1KkCp?KnO|nN!QY&ujVj1~T!D zV8f%5FAvaL8p##TTPDJ;yV`E#V#mOSl@C9=zO=I;{8PoIk}|#!v|=wYsKYyZINQrw zB%REr{F|n}EXHD;vOH_L-h(~pEs*`{f!v1?|Ts? z!(|BqLWp8}zrA=no*OT1k-e0X?n--gVv^~&ekcs3KTO9=72nWRECssOYBq_IzvUb5 z2W`g0q?z0C{0XGv&+V4&NbeFe{=GgX$0TmIv*p;4-vZJ7oI*C0rw_+6t4wTt;%DMiHqB(+q&2O(~ol=7@K6sR=&-(r6N4KyjpX1(ec;g0w2EFaQiIylzi2}*4&XWq2vPSCyG5> zjQs@r6OD6#ok@IGCz~F1_?mp?2S-(PC!JZ~{jy-!dP$`_pn>-7)5oZqmB88Qr+?1Z zw-7t7Z-wJ0WnD_ZU%sD`6D^BS;%fW&+-#WZyUr5zucSZrYlSNl&e||vmC*1dLf-`xK zh#=D++`ief!5)=Ex4}*c;DmP(ZGVB26wUJhYN_R`{wP}yJdHJ?g81?Ef8Ti%flz~h zLHzBQAlS5F_b3_8p3gqiOw*8q$FT&7Y}h1Ap$@wP&k0k`c`+E+OMo zJO98PIgk?~Ync&z8luPiXfa6gD|wQ|k8dOj&VSwl)Vt~Yeon~GXxCUo^RS&=svc=- z^uL##sRS;z7v;KgD+sX?Ejh4^T`BnV_|H6p-~~}Y*7KAr-u`88t_6srtveYUX;VPI zwLkB{_uP+IzxZswmpz5*?X%=B;{U#3fdRZn{zf(>rpQ5f;q~kLLHchNtN&aTbbi&w zRmA9;a>^82RhMzw-zNnCjt6*}PN}9v!$JI5>AoA&`PeT$|6Sz;xkCgZ2R51q_Nhbo zg&m)U}tsc#*Wb@*Jx}L)GD?l;k zT!}(Seh#qhlx#*&go36F(;lF5(0>J#ftnYuev$e!UBn2m-EJ6!Q5e}n>?rF8J;Y3+ zmJ~Q@X1EVztY{a)L zWn$Phfvi|Jh=FcnbZb1^M}Z{H`sS#&D@?ib zy~Hy4ktIk8lNR(X_2)XC>sW(>H&?sD!91fp%X19iHL{W$IeJy)k)3&lVrA~TgY45F z4vahx)XpPq3s5%%Ff&_w&JiSLVHE_RL=NY5*`UzMO>p+3?V-Y-4ViQP?t85`VqKWR zTqE^PxN;E??efFLZ$PS{X1r?lZtjMWO?-l+d%3KL!}~FSbd&;3&_;mWaCC3?zUVIn zueAL_|4cr8)^gT;b{u^Klvvls4!RsgCWC*{j#-r=S-$rH!6O=~C5^x9%Eb@Fo!by~ z(G?;*dNbdOxvF1>VopByRaq4Md;}!2<2ggIvZ!S1rht**F$XuD?BV9&5=&k! z$Km41nftfDJ^K3q<;>8|kk55AisH4B1v!L|BwQGCn@$+@Iz5qvl#Du5442z~ ztC-34v^V{5VAx+`X+gh9>aovKVm@44$p`CP*ajeJ8L;j0dm}&6Eo$8Nhy;rPfMVxM zNa{ePYZopCbPq;=FqO>RXhj`|L1RP3%QRfg`=e_)_86&CzXU$RO8qmpLgeSzZIHRG z2b~^*Rsma;Xm0dm&Q$8C46}w=1?*UB0GzzBvi<*o6Qxo6Y?r?W$|Gj)ku|kWrWmx6 zNr+m?uM7>pq`u}8XC6I0>n-lGxGBIyZG<~0f<{?30d#IwB0Rg@l?Pj+bKP-Mr*=P8 zRyWv_tZH`K8M;2WR_l9Kay~;6HF^`$$9Bkfi~PP&qwXB@s2G&sp8W{79kuj4659ic z;^lx%-pN~0S_{g6+&?DL5`Y0^H^%dR31+1QIak};>RAt#U*x!nPKVdLe#+CA_UL~g z?p?eCa;13boEsXVZz%C5SR&3u7vATn08VT7_N;}5_Q4OO)sF#;s+0Teox&Bql_{+B zVn@`D{iBZT#+rnnfl=IWrL(QxcfoGSrJPN(c&Y6=Qa`t(BDbUQQ-;vJF|%&mF1ysp z7wzK_+tz&YE5!%AQ2ljD&#fi1dSxMD5{C}0=5-sTXJn?W(l4h-#TJboWpP=jTsl7i zI3A>Qhu({u;h;9`rkFnCT8R^i@4D@2J+t@dD>7bM_2T!}TVx#e#5Pq>ry#28Xp6F$ z+;a#R!z<%Z;7N8l@NSdXN?~N2!%%^P^Y_Wd51Xy$YBP~;Nz{g~b9dQ|txo-tt-BtI ze=r)%pOgLWM??;)Rax>oqS(*P^j&tZM}^t5A^oM&OBp!py;{d(lg`{+nrSzNd*t5d z0FB1fur?8Yx0a3)d-c)>i?e=*N=1yDc7nJ;?FGl_1dP?$k*9F1^Oy%fhMng&S_`gH zv1UBYP*w@eP+NkwU!^h*G7FC3(wqd#(I!)PRO-{pDItTh+x&mFd(j)gMVDY~BD(ri zg3qFf`es;y^|RiLThMh+%fPc=`j;^?rLowv)6k3u8Zud1uS#SV1_leRUIB0cA;nip zi=Kk|kc4|l0d!KdaT(8119^2Fo4Z&`?e0X=l>biHOcp$B9_kL3u31mvL4ptk(e!6} zE&D71yyNfbJ&by{wsan@?9YlXpDVDb_JU>&v6XkGi#!~hL5M`6mhgaf(}xt_p%3}% zvua#KEN|@ihWJdzoWu&7+L&mR%9hYx)#gJZDnti+vKeycja4BqAC zeoO}39q+N5(5qc8*W0awt6INaAC5*?G(LWjTmc+jqwl#&6-j)EeFke>0$Kmrp=Cxz zqf(Mm*OYI;$yHI4lHBLBCRNEMl@EN=sRL?GcUIRNM_@0An=KE|#$u;CF_$-=ZjEHI zV6GX9cdb+tGCWvbHqe(!m2k0Bo#)wBy1UFtzA?PxkaVZDV{7+_B90hC>^zkeac-cu z?|5sVcZ)SbkqZkR?Vg5sRV6L?R?4?v-uK`=VuCG}E>pClVQ^VIU!Z4qouZj~{mxaT zf|K|e8DVCRT`9>awshR$GBVTw2(VeJk9H14p_@47v_G+$*FX|duo+r{>Kt`@Fndvuq~-R{z(3D+5&x^X z#Ax(E$?nb2kOymPcYQ~WkSUPGaKzfHDFm~g9<1gr@715~ds4sEv$a;_L+K_iugmR~ z1Jsm}u-{pfz;gQN)Ujqof7UuzyTqdkY2w%v+|SChRyw7+hd3&D-|99Wt}xPh+DfC) zc~4*<@#LVpY-;>aeGhcU6SKH--q)0D;t14*A2m0ga1Cs92k}Sd z;KBVTV^X%xsrqF3C1Qt}nt|qhb?;{AyQP6F*t?@K$Lf{UD&akXy0<};#?SwFliLvQ zLWAtSWt$;O3_fqh_4CvsB>7f<1VWSdFq1Ej-tfda`ASukzI#V3f9qDr0xxrshSADe zcFE|hs?(7aVZq?3NmU_YmfNu*fYcE^ohpFi#CTQw0+TV@zG19?m`{7xCQWR;_L7cg zjq=oQUsH!G##L0qrbhst;N7d8ryZ{x`5^45s&ORVx3_R9<4ilF{H;K&w+9IFrPq-S zg3*KXOKQ25Umx9_IS1|$81e^Kho4-wE!V>kekHiYmcmMToh8cGDsq+2p7Y>1Yvo&^H1$jkP+Q)(su&6YJW7Il08EHWEFnzZ;Te4(6&jIQzx^ibp zOe)={U#nZ<49{_NEq8Bb=}Sb+>Rv&A^3k!V#i>td!coWX;$cfi{e#7RI$oAQOOolq z&YH^E7au!N!WQ#RE(Fe5#!0rTBkNkr#2$TdREf}UXZrtPREzH})C-Ssm5QiM4rJcx ztHyZN)_APk>5DD0D-@j=a>v))nW`)<7OJ^pkT&dC(|?$be=>^1nAY4(J%VjpbWWVU zWp0qa=IBd6V$*(d`$K=W_KPP7D;g;eH;pOrmT@@^)zOc#0sgE^=I%#j=^lXu^%cM5 zD!3MYbJF2%V4i=GeEHwm3Xa#e5Y#3{gnlyS5(&sFsm-HozrD!bY{!2g%8liaQ$FBU zF$nN{{6O&Yi~byY)C`;43P88pvMHBg#pzcz>@F2*ho-3Z_m}0QInOB&&~Uts(-zdeO;RQ`qr}?(u4L$600eBew~kusM~y{)gesGn zv&ZN)G~T~2Z;*k72uI!ReFf5;Bd=IKJN*l}Zk&f)(vKzMLxESGVDX?A{?~2ghy(FP zYv#)Vm-9&tLp?mE=bK;98{9jPJ1g`#v04?KGC_EYDAA zL_F@w55AJ{%>K{g+=9*DX@srb{t~g}ViT$563e@}$l2o(+W1t^s(5drxYxGjoeKUN z=?kzCc(rYE=(=FWPqhIpxm2M)H@n3OT}Vv)_TYiT;di~YNzr#7y6hIU)AOE<3$7?X z$J3;eZzrFJHq&$DZ{B-ofAbI2k%@m#@#6z82eNLv{EW+XzpD9HC{nM#(ia3F)?L|b zf0qP==qKT0@n340V+;aCy#JYL*02Ml6stiy+4nEyc5DS9@w-n#-k-zj6c6)XOXHm? z7|g2GQObSf`STWT;tO21@Q142non6JdU;ab|CDRdV}QcI5(+8kjFYD`;DN`wUi-9nh%9qE+-&C8TsQYjmSzq&Q4!m} zKk$efPs`{rcf{t;WBlWc`$}!`p0T~Me-}y%uyP)sXkO}4am%J8(oE2YzEGKSf2rjk z3A950yw7Y^Fp%Ed3p@W}!1LJ!6b|>QxA^%y=xF$(z8f#u!v2r1p%noZpAt#MJzX;I zBoEgoiqb9rr~z^*ePGF&spzcS#rLMKWR+72qczH+Nc+1@o+WsVrM)o!wAoLGA1h&@ zAMmW5WQE_|g*)M?_P@mygFnlw(euq3`06hTLt@yW3KUsMGsY%cZMg zy4V(|UVN9`p#SnwQmIf|*)Es=L%2rrx@>a(v46K0!zf5aw@|-&pqD!WI2iPwY!8S}^`B&D;br|AaBb-F8>;obMDAT`Jtg04 za(t!nvgsno$BXbOO!HenI-bN*ZPLzZyRj&Q+MyC{RkE~-+OqX)s(;6@R}c&g-wnG? zemZ}tKj{Fp{Z=X6T~B^~j_fsid-pxLaDj7FQBL|`I8JG>@7zIk2TXHr~!oaZt}Wu`EQ+4;!d zS*0`RqoA$n!J03S4}%K@ttaJ#fy!ocDH$UYw{KF^-~`&#a{QX&B@2d6wKiEN<~5!H zx1H>4yUxU};P}N5?@-~=2eTyGLpjtoCu~NK`o)m?$P4;^2QcIgrMD11v7i&=I_{Dn z3bX|K>IyVWeIBhmM_37REh&>3buJ!CWvB{Yd&IH%Q1smSuKs zT(zEirQ-Hu4a8a!>jC5kc~e(q)CCNQZ&&x6cWs`yO3<+{P>srbO_iXl zav~0qkXyO=uyhtS2e&`@kauIECE)L7MI%0ty^@V(LP485=HV}6Pgoc)8WLGYQD#9% zgcjkSJ}bwO9W#kQX(7cq<_4N}vDx(6 z%MKPtHcMJ!#lli=cMF3CY4l%lC^aMWhV+Nn z+*MuGF%naTbLy1MFP#Uh>0!e;6{|PoYKYt?Wosj|B;4w~&4|CZ@w-wF6qyz;uZW1e zJ|kp|&Tq@^*d1u@7n>_o8>T*DaNZ||9ax%GzA-1xvUr=EZ9pR{iv0HJMziFsTvCy&MCe9mFwZHp z9IaPD@G8jd7-@;@(ja4WGZ)XM4}#+l>5b=zZ8gSRSd2g^F9$_8S``0M+4FUoCZ}=` zD&B4;YwqK59rB}U^)y2X|9oa623h%cj;2Fy=Ee(c7#MleJj5rMLzClR1&JN2%v#VU z2z(eSgTCoUY;?)nkHd&9_RztX${8)w_QiKVRwFKq1*7?V7s8KEgBMct9Rgq6G<6v{do94C_z1zgX%bLR|8^h|RTh&3sGe z+1+0rb7p{6r|w2~*n$aZ&9NC0IhSIGoF*f8p(BNX$#$Q(CzHgTZ8Ft@2MkFWMXXoi z=vQDY9yG#Bgfo1f%{8n}9-DZ+M-Ll|=dp}Qx*OyVV(Lt~3ByO=nN#=f{f~_I$^tJC z@i_s)fMZ({gx#TI;e#?kDEbI^wTrpXFuBXZmhd!DA;XuG!EQJbAA<0P&PO**6g6*n zK|YblG!4o4!RfM97MW3?ci}alui*MC$y#JZe;34L**tw8BE-NL52#eIKI&xO&(De zXR?VkRMudVt+4VIwC*ACc$F-KLIE7AEUz!|F#1)3%Rawql%T^lW+^me>Ed`Wqx@!#Gk&0%HfE#seBsuNYy0+}M1{Xks{epkYi1R~{&e(IMzKX@kvOty)|@b6BBOXvg=df6Z1DU2%N)kqMO(H{Vl1)AX%UAJ7uo-lexQEHv5^W2SM2Ta4eCuux* zaekyiO1OaRPKd24{kH(m;;OkTK`^2U&&=#XP z<$^m!;(6t+;RV<(T8Byy)%hao+0CE$lYUc@Xvr?iMA>BR9+ur{FJB^)*ZsHA=qcsdZhp=8j)5t+SMbOBQC!>i?cr@$1!i<_= z((;mlF25A^;X+Ub2m+s^c{)<0nb#h68_m*|W!|-MU%SwET}BxTcYwZ%+M2U<3vW(7 z8icAt9xmpGL!Uy}aO5X)^TRANP-Tet&`rh~I#VL`O)e=(k`RjhfB}O*ue;$&$>@ZA z=qpH>-KSiukhgb*I@HNCh|D(UsZPyzsXPa2$)#1s4T%*F-_R|HwKg#%ZIl{eh<8nb zB;1>M3+4*nG6hH=6+LOpOzdALBXcevDgxPKK*EOuQ5uDYy0IMMCNc*UP9~9(bS3Rb znkM1>yvGM*ZHTK{b(hjra0do9AWjkqPrg%ZF6*9vw!J=AlC!BNYkB8 z$+4ddSh!8d^_lJKefudMT6{*bVtMHdI~$is_n*Xw1P(V5d`4}*7w}_5DryI_)(^C8 zSOyk@Z;hny3{ep;!i9bY-oqJL$F7@>@>fnMRb+{N#%wq>Rj(sQ%08oGs5O}DVgwGA zk8r9rlh%PJ1N@aM_pp^&5+2a~U@86_t)d~pfmwn>-Wx#FyyNR9@&CEwluTiem*kuf z29q$D0SdyKpmNu{p5dYoRKA5t4koqF1TPd3<`0B^>;M}ESG~)qdW+ClsTpFGwB_{!49cK_B1?=*kQ*89{GDD0UNB#-J2v3j8ZF zZ;{_*X|xkXnv(-N?Pzwc_6pXp6;75rDC@eBv*U$mYy?sZ)-{QK3JT81-q&b0Tqc_I3}W+#HUi7UN%OxloO$d&njg0v1M{h)dCLaBs_^ zBe5!U3LMOlLQTN*U8k=rS+zX0PFT-d3ZQnbOu%GjothSX{J*!`#2UsPrjm&>3aGTZ zbyNI@Ly-u(WKc|%d7ltAAMU42_q0jif(b`l>vZ=%0T>4 z{C7(MwT#I&sZXeb26H$iaQ93ed`VEn={(T*NowK`bI`QkxSzG5&cQosVfo=FX>2}? z(r`1sz5R-&vkT)<9&*1&KEt7&b?vORuU}pFU$z?R&367|*%-F0W$`NEjqt7#+cxup z%Izef2MfhndSA?$5OMIPq`{~7ymQkD`_gvsm$sfVj?x+ewq{HAD%31dUohF+DtU2+ z$4{Hch;TfgV#IL*KG(7B*qxy2$9fN@jk=Zj?9qZBlr;pGQ1v(9s`QKb9rbee>_bum zC8mtTgVn^_6D)wJ;c4il6yY{7 zvAKLa%E$lx{{lo?Xv?8}GG|!(N>1Q)axSchU|n4C#}E+=auNOv`u>-ocS2~h9}xc< z)WhAHj7G}wHt%Bxb^X#06rM5j!P*ObB??+AQ|gn}Yf<@u#%4hh&f?!2l2Vl`xf+;& z!imcl{281HbIiWchRUeMw3gKCcE7tuiN-YhM#Y1E_Aa}zrxOXp$@?i1hxPJF);`>) zlYZ`%b(k%?PYlND<4sVp(ANP~tf<$RkfD?(S1oIf|g76nk9!B2wSULe5AI+slM;uXUU9lPl%7NMaFI_|X?R~gGygGFauim9D^g87 z`bYgc+v9?0>hNF*XW-};H0Kf_+Mqfesz@___`A3&6E+cUo2g=$`7h*dEb`E(pm}5H z)i>D+E+VW4{5UX>-ymCHWvbnR>}FpnhLYEQz#xe9(2JY)T41A5KEJVA{*sr5XcL3A z>a~4#$#3KZ#gb9wb1ro+%ITYuKz-pVUB=OCb10^fkla!Hz+h*Z~9YzQ`;cxW>AR70#!E%v7eRxWvgrT)h1_VwhYW1y_Be*g8f(S_&#FtHHT z+O&j~;iI1>K9{VKw4cJ=o={5-p~YxPDzraT(P=mXQ1W59-ICI3nd_OnW7 zL@Q5*kb7`jEdHoIU>YYoz)5Rw`|cV9SWeanpM);q>;GU?h7x%Qy9Jp7nP<+465$RUH zxIpA4Gz(3Fe`Dq?*{8R~ac||Qf3h)OGgRklWDYp|CF?awI??L?K&Ra9b*Xo#2h25= zetQ=?&mvF89Pct>r66%A%OI%;q%GZ;hG4-cD;9#bj%c?--&*4->c{d`M$9oT+aENM zi*b3ygUCfn;CMcuMaz%*5t16!2W+I#QQpMAIfv7E70e;KJsIHlVTb?az9)K+Z0Z6A zfM9QhzO+-38@0ai)tINXM~9nTou!O<-w<*br7ccpJGy?~OCU#>ta;!!RdQm3pLGGh zQRRA|XPcq}J?@0)@?ZMIIU+a%DTLEOBO#aNZcbjbMzY+@ui3!CJiTA z8oU4-{2(FeEfO0=wG%fDL~9pedld>C*oz>deETdfz`D zNh4WGAJkx|sBc1+$WB=j(rVvj%P#velT=1zD_et+l1LbXAEnvi6F-{XdQiP>u`8 z`gt$dL(VZAif6!{ltU)RBVE&8H}LUoH9;b-xE!tFZ#@I`EEd_Uw(;y4xJrQgGj8n6 z@O>Z3m5D!$>JeG^$-fo9d0h=2$$TEjEvHtA%^C&Z;fUK4ft)*_D2G47&%6QMfg(8~QK~Lpy;&jjj9C*RJoVuI{D%U~uiY@BK@r$26qUxx10)^MC zsxSc@DrwhvE#ish>W=S4rusF9CNm;*I9S_sAVK~7c`u*-DA(ZCwC06XAaiPt{xh`m zR)ipUc&JGxQs;*lcdS&x|4Q0r*9+s$UuH20r{x}vY9=V)*G}$u;^16cd72f@<8mYH zQ#gCatc;I7eq#cf%YQgovf>XZ5_=@bxUT}-M8I+ZtZ^OgkU+U+)8>{~HE3k?Xe1*S zFQUqbGEro>_>HH_zL$2Ui8s(Jrzh7;t6b_GljU zN$5tc$H?eXwY<%}Ze=6}0JoKw0TauCY78M~p#&%yBDqr7w!f5t*oJsd!4Kn*9PBy= z!9XLjgAM&cvtoF)bL~CG&r)CAPh{YBIx%!qf;ueqz#d)IOphjnmGl~*F z*I4+Y(ivs01_bP4{dE)~WAP~w=(jvV_=#)bI9CoZX&L?VX49~J7pnfG{3m)&Mknp} zVL%eNW!PQClOfxL8u4v{48x#vr0)Xpd8s$fPkynO8*oe%lvOi-RF8B2qkXsr-Klow z!4{yF^HT!a$q$vVpC@tP2w6zO{z|uN>ofY(MJ3ZdB=n&N-JWu@#&e0`lv!I>mVMTX zZ<-<`K&Z>I&umaC7SJAw!!;4sqYmw!7pCBHe0X@-)mUhTpbIrs<^WuadCUq$JMScM zBH42;!@Zq>mjyq`u&{9XkZJHt7nM=&m~Ya*QM^)I=RapY{O6N-xNqED&vw6IY=zzI z*T$5cFe20~T(WRs`yE8NJ=^yi>6Kp1FP|OK=&(FzP9Q1RGZ?@$`~Xz4=qKfGq1brpP8q7{3vu{++;1WgfUt?PIqi z5Sk;{4dO_SUOpqne+)5yOpy?Onj8Jp#E05}yXc1sV53;x0ODx6YbaM?lvL(2hfvxC{73COR?+>v0 zu0MvlOC6#}C<7}VX^VwMt1dA)Rb!nTT?d=!0=Cl)s|!9g$qR`x%*L65DUQkdjUh?3 zjt79f+9C>%%(X07dsDx$0t*y8wml92c_tF%T6(?;-#OzIWa;Nc0Gv5G449)$d@#=b zLmhEN;=CDayL=uu$JDC8MG)-m^&@*qAVBG>m5an4j>R_e2+#U3rv$9dh_i`0#;WH% zPZ0HdtpXB+IO%ePE{}7vqr2Bn$Hx|CJ)q90YmBR@Rz-ArwYR;C7e90{Hk8sl+>xbP zl`a5=eoE%eMbVgfc5)~;%J`EKv*M7iHW3toVY3Ei{&EcwsH!!*Si6wcn;E{SJVx(l z_K}|k=VZ)GedFxp>6*7C?|fO7IahxUMF()YX>LvC`WQ3v9P*S7Lv@tGy^#})d`w@MjJYaz1-bX z;r?^JtUZ#cbIMln-G8a_U4;Ayi?>P}mU11)bbFV(8?e@2jwZa4yl9NDP0(5_ixgXN zXsFmraRM;iOReqj4~C!4NCMcE$l2w@@I~XgrFpv^zsC51n!}K=E8jN@Fv3eWBL-BP z=;h8`wX4$AKNc#dauUo;tiAwpulY-ZtNl9RB0(uLJRvMyaB6xyj=mp{k1`d{JvnJA zOlO}Dt~0Gx9NXv=l2hvJL?~$n9McBE*N=v)r%_%Tzk;z~b(i=FW4PK9O&+=ZzV zMtk~BY=ObWkUxWL-GFN2o9xvPCap6OM*4~U@J30-G$r7!z4+Q*Q|12JC2L9W^EqF{ zepvbR9Bc$il%@Lq{-;MEjD-N^HBkFJu89o|y#t+}9~_ekN%L#|$85-f*o8#$k+QOu zshFu$*-y+d`OemM$@K?wX$jy6o-6ixg?)d~lt7c*a`$&%mU386L;*Gdr30mnx9Rtn z>NdTD+ofQEXkhNYuv9rWQKp$TA#aBFs6NY_tzWB1L{ z=Wgz&1#T&^l^497l~6l$nzirD<3{OosgfVtDK;;py3S+g4ef&L7%OHem%rQeO|Ulr z1r*$0A>=pIHa1ijeLeb%J0x9#=&P<>eqHJqg-M^`GO^MuGoM$y^*|NG?~$*ggUgjb3&Q+PZ z^A`G9wnw@VNBgSw(z^<$cXDhT;}grStrR?tZcp#<7!(kvwP_PNwt3P<-*f1iYkp}p*8)G1!jX=NqZmL(X!70@D&Yu|j7VQpqp zl*fZ6y`WB9{#I62k%vm%_SQ2kwjw>z0*<_>?Y}4Yf=9bp;)bhd~n@Wz_;5U z-MZb>gT=D>ALRLTbsu}lV`F^La8MRUw~#RO?c{QVFg+(Y$B6_q4a@bTRfj-i8_9XpyE4?_N{{Y*O&yW&@fYo-nI4N79QX915MYN!7 zM21VU!g%_5 z^p^x8FrGotTXJz@O)(z)%HVhY`+J`;P%aB?WYMRaQyvdi^76N0EQF^cQ6BAGzAz%xXUeWB<1*?huu~uK~a? zVsbByVzG!z&@8apgEG>*YXV@H@q0;i=ruuv4+V6|OGIAbK~{;!79YEe!!|}%c`)7! zr<8E8B#Dq@0_G;VFxvN@ebp6Qi-V0e(y_G6F@cXEzXFy4zd5C19w64K-f~|P7PEaw zoc1~vR3XHoaUC@I87dJ<8E^Iml-MJecBV44qnYUBtw($ll{cgg>kIFXes;0OrAIlY zvi_6TmAvQ8l>(-3VH~!?f&roLGY*-wdzR%KWg2`EMjx4#5B}T_KlV|K#p=4(7xK%&IA`3+30lV!4&bYf-kT>N93-qh!*s&s#_N`xkf;RvK=tyo<^sqgQr~C1;sr3}#p`kRYx%Swq{DA>)JIDpm~Jfe=vxtzyZ> zZDhpdanJ>y|9!f=eo$T_ubls1(*a zz3iIx73%d<%P!HU4DbV49(}lFqM<=K%S}pLFw-fBtmUF-7S@YWoVbTxu0pFb_$ra_ zCPL2+N8A*VRQ18g1}vDX1}z+lg>=3wrI*z!qx@kxz4C~)hLM&c#yGRrHiR{7f{^%|0fLTkA+ZNet92kBK-epX*3L< zf`*9EH>r-cPLh{>M&=Obam_5m4)t35@tvS2E%<1I>U@hq;F3q>|GVT8OjU+K{ATBa zp`}N9`T>DWKYP=J3GJSx070$XUf+?J+0=i*vM$e1jJ*ji;1B_(!z#c z)PO_lV)a#@KrbKqOys}E#nj8&l*-%lVb@7BjP`R$fB29BD$*^aIx8EJ6*LSfo!G@G z{Ap`8a{K4RF(jTj!MsB&SXjs!EvQ0uW^ic}9;9X?J};p3MP2i&ijByzT9U_qM}Lb^ zl>Fxcn#acM>QF?B=7}Sf&Q(Ky(Yi?xamj)Dp0?rMnYW5{0jFmmugJ`qcWHO#<<+H$ zC#^fk2{yMmA{>KET_B#S7{)Vd{?I=~cJ2*BGPJ|k8{KL5?3m5i_ni4L@c~;ijJdal zpUz?^>t@qe7C^N`Md?J?4nN{!idI75j5(WHlq010Dx_L4EAsb?!4K`l9c#asLFAvz z!C$lYJ$x1fR$Lc`8j@^+SNot;JH_|{KTJK@%tK~-+-}~oe6Ew)>{)yF!(1t)1WU7o zC!$-BD_6nPc$h52)Mc|+alDQwt)U2BAy(kN!QDoa*m?wl%`~rn>NXgv;PEsK=f7+1 z5?DT@w0WnUVD=@4T(Fg4BZMYrdYNXDGr7Xs(UI04sOa=*K@TUF&5UgFpoYsxYIbTI zmiB1t{i_lzQxlIz$~rlRR-Mz-kalr(W-!UH?ce(4b6r#m>z5Awzh<3NlU;`=BE?8| zRwk%xC*tnFzkhH55o5|NSPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR92)1U(Y1ONa40RR92Bme*a0Pw2+XaE2}07*naRCodGy$QG_$5rPUZ>zV| ztG!ApsicyulB~_LY)h8p1#jR*gKfq`8@|Q^joAmx%m;LXG3)dg2BrafprQGIarb;p zv$_YcvB5Upu(4%hyvUZkXt7q8w%V7szc>Hii9C7Z=FPk}?|t{ZdR29!DqloKoGngf zo_|JUWZt3QddK@#g3^#SE3iXKc4(_gkhSnu5qGF}J+68vFJ$H<`-sZhGr896LaZ(_ z49P+(t+)V~GbS>s(|vC$);oI(ZIRFgq`5Pvbv=}eVz8xHmy2~n-9FDDSV_8_(&fyF zTyIa>@?un8WkSQeSaL}zeS;J}oURhxx_% zFh4&ZmKT@8-b)UIiOJ2m@L_p*x%A|MWLevAPTO4YHqT=#=lqHc@!Vw$tt^L;iLtPC zdRv$npAa#8(8&TbJj#KNv@(R=lqR-Q=kgxxqKkp4v3h@w7vjy|tI zyfmcVseWXoIrQDJR z`iC!y{4FO^L3ulSqMB$`Jn>Z?$^^j3S!lb|r$!a5NR>3vr971lDN%Vjm7h|$8AZ|T zSGA@5)+s0ShEy207#)%m6vZ^LMI!#GO&Jm2LM0moxz>vZ{fU)^Z??*Mne43L@x1o9P9`#JA$P%3v6Di*rk1W_Tt{ zZrYUdMLP8*16Am#8hGf>eeRSG9}QNje`aC^JEp@h9hQDw+v2XXLhjrv4-* zHreI%o_Dkl!Y(>w%ZzaHOpd8YWO{R_9KNAUekG_wlWnE)@RK*BWv|O}_eDE1 zkpp-dU}ib&$rWQ|lObK(cI*zvjyxG=PMr(}rq&@i%2H+}Cwn@j;Kr*uPn5mhE4{r~ z@9k}K7YMTpa)#Ohi;P_haE4XpZb!jzukJ;xtxl6uQt6+JL04=sq>=AY}(1S zGz!)2sniDjxluAu5lHRWz1IN4K{D+f7hW2VQkrEh-6aj(2-G87j(#btw7jHiPJkuE z8eSQ(w#H~OUfxXG#?wH2j82yn$Zgc>7Wl|D%8jGX<#7DO(eTIv_Zrdu%dQMtwrm&Q za5!<|PfwWuX)j6m1mZ7#WR&(l^wGD~GfiFr}d1cioOs0z_wwHm&bWN$%Roy__^#F?B;) zb~;znZcLfVPRlNXe8ukA+Pd8?KL^fYjtsBMMA^BxXYZxqq5HlZ!PJ$f4)r?o01evm zI;3WaCa+KVy`8+OKBcKFBOC##B8a`-i>}0tfu>_}VllgSI`Z_BAn8Qfp-w8iuQ{Ff zgNl~MoT{k+S=wjq8>d92(wcaZofweXhoyx4tS+WUiH_tR&I)6jCNwhLY%o~X(Xs$x zas-4!09+4}g;RYvWx#r)2cTh>?WubmTg$ zAt?Kge*K=XsOP-21D@C2aEqvy!$S|=7Zw&Yx)uM!58M;3eb#d=U_AWry|G?Y52^l| zXF`vS01TsIX}6Inw+9hGvE1f>Wn9~s&l!iE)(e=bQ)NI!nUrwLP_B$~)hv>t%sDM; zTsnD1o6uNI#%h#RVlW}$YbvG%LQW~@DDMOo>7R!*zX<7Ruk=Vn9GPG(i+6x(y=tcfhGDzL|O<`Q(<&;T!0|p93Bb( z@b3Q_9(m}V@XBxd-f+>rD^$;zHk-mF5F3yxw`Y}@5T9Xc$&Zx*M~_d`<)hjxJaDAq zHP{%9j4-`c>sL z?33vB^w#YiPI-hC1R8f%oi=-aysm;R7e~ACR1vx&&vOQ^$a3n_ts%(AE;t|zF#!X_g$dx=L}h)s=i2f_Uv<4+I7;jDCy7M z@$2C|fB6UD&d+`MB7>l*| z2s?|L8`dIwy(_pzxLxH*bH%0`9Y&4T1x0tBd8ZqX#o0>ziI3&ad`sNxZjQOM)25px zrs|8^keuoC__r##TT}bIY<6bngb6b_$V#?MNo8L!Qhkk^{G#DvaZ_h;X_Ytg`eGjt zOUkaaxFdC(N8U#NrZ#U4qnKQl%Id{an#$_S_?;PgYcFGTqIS9SrAoZc^Rz9O<&9ZI zPNRAbSknoS(9)Fb=Hglkxxki|o+`Q)*Cj*Rr?hN-XsXD}FV+?Ji@l`XgIvRfU0PWb zy5{?CU)npbOO;!l{)~){*~Q4Xu`Ce|TiS->rTF!)eKBm+B8Ff6op*<0N1qCJ-+4#a ztk+wvz43YBjX(a@Fh6%XeC87$2uBY;8FuczPysXo6MB3hk&if$$c0TkK^ANB6}nhJ zKWa?8Dxf-CbD#=@UQyCSlwlRB=Q#KczkFI#Ri9(FVTO~lmO~<5ENR-&7snWiNEt+aJ z0iHKntR(wfzPr9a5p{^j(1{JN#1e+mM);P1;t4K=itF%Vc zT?s0cUfap#+*vJlc>k&hJGH}?_NSS?(b6cPGw#JulD1S?(S5y3ZMLUdIc40`x%8sE z5iK054tu~(l<45X!XlqS{n4L!TUcUWG8p-djBW}`dSCVK&wVmH`?)U(lhfOyI*G6m zrf7#X1$!j4JYI$nLO*yl)F(PQPcu}K_Q9X|pV6JHK6HPJT ziElx+5EZDjVNYBDB@Qhx((5rx3t?3H7grYbsCQJsL^ldz)4BqxE{rQ{!-Gx}On7R=NV(;WQZ>6-FvSt@i!O}Pw zod~N0bf|ZmYKwP<4sx?*sMjW8@Y<#_m)Ihr3bk>ytu8%*p4J9g8ew`)rT4$<|J4-l zABNMXj)g0(dS>{+pZxW3<4rFLpZ)X)!`0V5M_9HJ=JaAXiC1p`VSto0oQ*PBLJLR( zd=_AZY^G-fWk8(3Q=lo<04D)$Sfk<-N40AA;rqn55-!nn?q)sn(Zp>ysb{w=JYrUO z-=$ZEt=o54v^aj^NO<_`Up4(q|8Cx_)xVN6#OpC2vE*wYrA^?@sN2*tV&_S35_459 zO8`Fh!`7106NYlS@5HLJ>asSYae}DFh7`{0xr~6_7u|SuWnN}19RXpit(73wL?hF3 zQHOh?v^$Ni7+V?sFbXqM$cw_+PbyiBU6vqWHp58ENpd!Tu$07vWH$wg&YV$3#v3{5 z@;o+&s7RM49KHRwoc-H78*^ z0p$@znIV11Vwn-DOopy{6P@hRVAo_JuW94%0JpMXWJN-M4_&`bQsIHrd9_4Zf%U#ZA)?an?Y7Ss`s2?yTXGH!n_OzS?|Tj}H&~iU zSzcsY$hQ@JZpb#R6IjuPO&q4OjrW#^)hEj-ryKRY=@<@|Y#djPy)G2ARBm=^$#$$E ziNjKSJ{+$x%Ia0#2IxYUk4(j%C(c*DNw6~ed8-|UWG@z-uBD+yg~`qYkQGy^+uL*= zS)wDBQX&B{#V18sEyy4B^$p?HZ~7m?gZJMRe(m@FF8uTly(0YTo8Mq<|Kq=YQ@H)xzu$PNSl~65>||%P zuYGMust6o5RjVmiTco5qPF-miy{2NBf>vN&3Yr$yYFmPkMLmv?iipHcre(S2<8w47 zrcFp#USQ;@J@`Zio-M0C3SSEH`h=lCX;DFPWK?<-i~}1}$mo>*QB8j>Dlm_Z#1XZ1 zuJDI``9_oX)5y?G0sUESHQ8>g)L*kb%CS1_HznK8D$}wPMI})WWd-NbT)pZcl z+}zH-gfjJ$Ri}JZwChWQxtVI!#wP}@Pth%I^eC%Q7E2K@hjjNHr-Bp^xYL$@d`gy! zlEC?&fA5zJNZO0REiVaQzvqkA(Kr9v@Ii)0^zJC5qf0M4U~;*8-+%d)`rhbQu|8aK#Z{6`&xgV#mtGMb(WrS@ zvM+nuHP+5_F5o(gHzUv`2A+_eM0Rf&K~z`{;*Y743A1eQUHfN*SFYjq@~GO@n+BRnfCzYRHb(w94?l&{BoRxl{>}y3%Blv$j$#IIK;I zV2-!c5A>L0QwFm%sc4l)jlrAvOJOdeOq5~ltum~Up(d%N*{zBl6!bb#ae&l(2R zyPRh8y|$5K12k-APot@AshwPJbKov>6T+P?^QL8aU0dyHbi-5OX&FJj@3fs{uDZR0 zqS~G7#*kE}EAWn|a>K%SRkGQl-yK zUD-2E?qr9<=kEAmxcznC7jApew^~P+KpLf8doD8G{Rgh)O12LoeBvYT3ZE7INo@ zHLw*uppAUpU<@&Jr+r4u7-?W@-7Wmdf}~`{QtSETAs0samhMw$f!_Ez2G~%ZGEK%& zMqz=N2K-@%*8U`g3 zl1s>3u;)dgnIXmM+%H*siR84Z!QOfi)sv7)+X&@GkD9DWI-~1N7nG(_x#(&gb=DVIAC;&TNmr;d2qP;&neE(NXC2Oymfd9QQn!`4suv;8W|u|5ZS$g@Z8B>qk}A3StXu z)+Y%S0NQgb5&jqO>5gb3kSEdL?-o+K;rp zZMj5ePM->FN0&3yEO;jxG8?5RqS)=4g3l0HW^Jj_RO-%EuZxU~XyvK_r@({fk9wBL zYFwsC8JKZFzV!Bg9f2kTQb1?3UiQBKo;$-&eBVp$xg!_zvBOVAc(DTA6e->wUH0)ZgG_)(W=ST05B>wK#g_w=tPYyn6ecG%m!s9 zFk8dggr<=xDlo+Jj5g2Qgrc!IL7`A3r$E+zSIWB8TPd(ikk^$m)$M61b5>bYkjN}H zrTb~Qo%o*Gu%SXhVqQZuwBF!3tz|OG>Kc+#?ndnfFI5?x)!RrWyZiR%rc4)+?83C5 zC1OR3r4@iz)1tZnxj?_`hu#uC`o6!&zkACR>_rz}9$uxzM|Xbaqv598zB#=5^*sMuXKlCB3$eo{`H9!DV zuibK!_*Mj@U(u>vMxD#y5q;SHTE4+69DDSk`)xHZfCuk&H{NPfzyNUUvl+mSO*=1| zez0BobYr1aNEtzS#*G2VTuMaClgjbG)j$_RVop%65CunCz{M4&-00PR15@S2WFq7j zl9f|%E)tjSqqot(lzmyUk(lXMPfP5GJXu3!J5+cI9VJAjI`+lPD6@rgh7Ad2h9oj@ zX&L%wV^UdB26NFu?_{*H#8%hsxm2Yo0(q>l&s~~bDMeN`(dBkH^&;1L6A z>z|00WdpNKHV)N#14P>BRJnmEy1fg6 zG#&_Tx=9xv7otChp-kcxfnnYG0;m^j1o~Y+@UsFejZ-9?XD}n&6>G}$Mt!v5SvS8} z)4Q6~)yLr5p9@!UniB3T284P#w`7^rG~Di1tx>u>!C$M2b{F z6#HyNuk5#}E#YW+WBp0;*pmLu_{vm>jAP9rx7gh5^~h=Ca}((M znZ#TPeuh7OeJHO^3JtlGq%mpBie^Aja)Zo{ZN6J}WE*nI4q48s0zk>NNV4?>Si}!M z=vv~Y*^ar6o4xuZx<%1*{SWCH%15TwVH2T0(R;pUc}Z1M`)YS^RhbSu-4|wKTw7#pr^aDekA#MGB|f zn7OQNXRu$98BGiT@x4Am4`9g!2{6UG+88FdpN)$)z(T-KK+9dO4~ML zNHsGnG>h-hMR{UUxY32Jix<7fu<2FN4)Lz2L@eqnM(E{rn9*fc^l}Y|9$M)lyzRa0 zY2l#WFGbg-mtCb+tQ)p(_8xrj{s{E?xIQwlYrjDMA%2=oV10=`=D;g6{IjV_EjGd~ zyIBwbEb=-H`gIe)^f!U0ec~phJw;~%Ovu7&g{-{Vj$f-y3_3Yd_TP_jVpbCvR~U?c zO^R}pmGE=@z=oe=iu=?wC5hk|*|9P+?N36&et{=x&5N0hMee+vjw(f)8?|Z4-B6Sf zEs%|w8To$7oR!UQ*6-HFRBkLs@O4V>Wp-pMd)?@Rt_nBWg^-(|3L_FaUwmXUOEQS7 z4R~jlY?&F9YHmKHDVtMyo$r_BiJG$J z_I0U(qJ40t?JlW|u9PriNM)4ry5M#>LcFIgM0H->j+M65hDutOpAxvPw62{}Y^vrl z&iu~3Nasej+Tks!6&tN=eZ$f5e~WqX8jU^S;^qC(MS|&_w5m0ih(S37ZHOto9vNn*}OX?V5hk zGu7q#CBnxQ@M*j&ouaXTQao2goFua&*)J9`FDBXI#Uyv>o+nLtG386kIYEa)l%4)< zWr%X4jpdf=n5EZOg&`akJ20&(RHMddhJ29~3UslS<5_4(cB77@qEIyZInViLa+-z3 zW0q;MtksstVUkQvK}BYLCZkz=kyP%eBG5VN)bed!_B!R>*LW@_4TYjE=eXWM=e*tW z=)C7T=6vS|BJ3Sg%DP542zH%PmQp^h*`ho*CNE?fi~FiB>t6|(>1cIzlpE=3rQ}p9 zdsU+l8S=kQONHp8puDxRP>T+tRDLGScpCy5rDkmmdM*fs^)E^fjR&zQUuC zAs6dN0*=d?Qnd?Ma_kE>FtXT37c<|-8Y5CIdIC^KCo~dOndv9%rerJ$ z{FXIZ6`5!NXMk5DPOaj#rTqfLQ5Fm_B9?5MmiA+SjiTqQ%IFYsJxXeiT<6$0wz z1;)pqHGLk5BWjt+s5ahIn7QSDJknut;n=tt)=#))pTsu#pWT?OMMclOUU@1vEjN(> z{IQX1u$bbFLi~$8t&y{#rV7NO_)JHuiiXMx`4G@M_Xoc9IxFapizWBX+Zwr3A+J$x zv`0Pc+wn^qvT}@4k+-zJnOK`~aTT6tGNSnnpXMDa=*#=*9nWo$K>jDk%@#Zr`Pmm8 znxe-F1lg|LIO;mH53L9!`Opartcd5b8`Z~2cx)0a*14YXam|b_vfRC0a?S1p-*3mW z9&JTC+`dZPMFuB?v8v5!`jDfdXk$@ru^qKV8aHK~%yxBanyZi8tmoa&_M3E8`JsuZ z`<;eZl&1V~$NpMV+DJtlr2#+_`f>2NvAM7`4+!cq)BiY{WIBk^r~0Og9e`aCXxeI9 z-`5pNuvxI{Fpf$wggajYH$Jn+Kk4B)D}d?KO|)AOL4`~j9Ulu*vSmrK_yV7nEEzb) zSy-lBMJQbo47mXy0g+MXyq=3Pij08>{Q_6nyR3yn=n*y!II}d$4n|raTLf9x$8Q`n5xcJ*8!@Q*M4v_S+Q;F};~IKarcO*&`tP ziEch>CGqC!QKiX`O432Di?Z%XxaW2-ywP&;@;?KhY^RCBcGBZg(qZdLG$ZONN-zUB z-7;H??b^4NjLyJ0D>sp1r~Vu0bN(jD$(gmulQ}uxYbum$`Oh`XHH&2Y zY*W3uDQ1Sp{z%V?u1TX&`{d_oo%GFEvdt7M@`%`rq-K|2vpH|aM%w@q&eGg=)xcY0Qf zapFhG>@$A4AbCB+Mw60bA9P^q7C`EAjgu5acvp3Z zQLBJQX(LBd z|1$aU<{_R@E+ew}O%1iK(J7Q0e|Ds-oC!=wyjb3%TK;FwDx-1p0bI&uenp4(1%ZvS zYtQmbn9D4*BE_mA29{Vul{RZBX~sqiXCgCeD-`+7n&+mp@P|I|l++gMasDguh=jA9 zYj{Wt?R28+{ML=F^ziFR%og!)^uc?-WDH&Zp;K;j(n0H#_PwcFxrwOA>VmQc->s7C z{LrQ4D*E>Isf4tga(R7Pw|?bz+EZV(qgy{~>uSQ<)cOXW^+SKG)k*Ts>4N5}H}8%E>Yl=0ZFuJ5R-Pe`Jzt{cmKk5X7eBgnB2hLLu487pRFK>+`>*)-% zdr<6EZ&?q@WEOQfpVJoiYuO_v=PS~?T<;*aJyw@B6mR8n`l6tUeZO<5Xm+)xw5Y3! z=X|cW;}{QS`5|q-`;IPM371~HTX(I?;mIeTwC(88qv4usuFQ-;Zm|ac1LK#naAxo#v5_zpbe||F5V11uZN*b?Q_&apFXH^wCGd zm%n^>*s)_r*u8sqxbVUYo4PV|^)=UW)(z6Y1JMH>gSzoY)fp78k@k|D*RMOM>+rc$ z=lA_IuOp|0zojCTn3ZGjWQX8!?C6m&J3Fh7Ak2pwZ@e+wdh2cBi6@>2hmOR0zN^c} z8x)c4rSCdsl_))Hsw^J&&DuK0qmHuvc^z;)7gPF_=v?eyw|(N~jRi;C2v1E-h3(t7 zhYK#aAbjdmcZ4r|;qz8MF)@*g9lGeEi~4f5nvhJ=nK;?bwEoikD})0 zW(`bV{_+;BtKcgdv& zq>Mz%(f~}$ywa(zU*V~L*X6l-i4VZE)43Si$H&Km2BG>%!iU0_zVsyKzeR?V$h3$2L>MC8fSgP zb-L#7y6Y}`gkx(WcJAESuUo(K%F8|yWlnueU!4I=DSQ2S<&1NzvksPNw~BS_JDBdh z_~J0Vc{)6<=cMh@U}W0PJ5_D{3QvQPX_be)elRk1Pjt?S^DII-efnfLsK+{&UV3Tc z+q|9G2NXpQ_`_+y)JLNP*g-mXJWvLr^v>(~#g=Vbwua*;Po6uS9`t13fq@6wdVuSH zW@aXA*|IJC%fI}KjZX6q-M8f&G&Xqv;PjEGk46De-#pMZF%5RkdJm+>l``i6H&Rpj zSi)0>51;k@8+2gcfq@6=dw}WQy?ZY(ASD(Iz_h;ankXGiaU#D#$wq+nt%-PzcF;{q zgZvt)I%8tbWFO1?n%2eV<1C-oXZCpYr9-kg3(k>j#_?QQ*W0hj*oF)HEz#wze!ABs zVe9IpHF|s|OM@xi?!8aU=U0&!L=hLx7?_^PAlbd!nmKIcmY$FLs1!%ipES@mbJX8+ zzrax?4;nTA9U4EFRrK|>x2JTyMH|t(7P|2>8)?1zh_14}l+{5?G{wfsb|WhMAsUNM zUW^~3SI4Ow@GSmA}YC>eO^h{uEEW-V;Bj~#$z`C&~^v8a~s!udhM?} zm^uhJ5PiQqk%P0FUfQ3g%3gL#rS~GJ4G%i{;Ne6}YBiFRrZ#q*nWp5F zTQdWiqhIQsHB-6%f*C|4caZA}rUWUL5O3Ct?F425@#)j2El_qHjB2_Lodjk8fSiDj zUi9^%6L}>1!51;K8rp(B;7MN&>)F{6effrRF}XgMQEbO2TefU5-vA!Gzqp{E>PnOQ zaND+Rrk^(Sp&vkV9n^at;VJ5Lx?;War8zX%#@9dg?AeopIKBi(j~_p7HjtMAdaA2v zQ`&}3Pv~?1Qb!;0E&9?_#Se|QQ7-Ez^`6`=_Bl^HQIBuoPi0WgFRZLOBzv661;q-l z#*RK{@$U4Lr?#{RXxrFAT~W~&Ar-Q#s$FO9_$tEo^(A7w%JWrfXs^GE*0loBX5dT> zau1|+=UO@Tk$A9fU`ilL0f=A;;1H+?kmPHX08&OcEEXZyqZi;I;e>7zeL*HS1q6I@ z0x-bmpO=D$I&_j~bFib1&4V`Jj$HsLfW@b)08(V|4kNmp9y;pj5A-DDBHx2Pb*_(` zkuG*2CvBt5F+nymkqte$+k}5yA3(Zq-#!CYXwXM~QY%?Go+NGpkmtIPgMIWBIqb91 z&V~#-5mm+(w*z{tpqpu`%a9%-!9_*tHxvmqrt`FJnD`=60 zTx@59&gHv3{Jy8_Go`EfSEHi8yfV~n?pg4v1@x@B!I;`m;`|0-`x0U* z*i#Mdh4iPDA8}8UZUt@E%_GwK?P6L^d8*2tqjaprsw3lhapjCa{GvBV6i{ z<#qrn_y%5Ru^)Xtt?e?XM=zsu%Giru`bRx<>>~&H$Uq)_rBCQZ7aRTaKB5~s+Mz=> zIeL)GMqQat(Lusb^pVjra_B$D2RpC}utkRZ6kkD0AGtw-2Rdw}9zLHnKt9L+wEBjf zeLxjN#^k5s`dk*SwY%JGbVG}*?F_HtDatrg`$hZO^`*o{XYZ1&L{j}KoukFn3V4yG zQLk$?VM@zMbr;1}8@gaVt4ZlOae}nwU`oIuh!M;f+353yc85=pA{Y`#J|WJ4j%Y z2Y%=PH$a7Yf;jd9A_Q{u00QX6ZU+_W3I1%zaGA6t16%MrK!NS}9UTAyFCJqHJdB0` zKBwWhU~hWUV;~{`dF-*rjE7^2eb@~xaww;|kV~R3l<@<;qEGY#nb?ROt`j=!aB!qA z@FTmOPvQ6DgM6ljk;_dS{P>3rKSBpRW%nPn_zE5Lh2sm*lISNgsK;i`;X{XbC8qV9 zGltdt;rd(yu|1pQ_Qbf+ModS7HoP0HsZL)wNKTiP{ilJb?$21kSyC_ZpZIqzIq@2#Fv{(DOu~A}DjmPaq>`)k)|gFrtr~ zeNU8;OV9*(uz?YY)1ZgI+D$?ppu`3*!JiEfAs{$$M}n3*+RA+D{qP$!>;e3|Kkj4t zLK`;vv7_uji5_U-V{_lbhmD*w@R7qeIobHt2s5;wf4Vy*`>DBH)<(0Z+{$&XO`YOp z`d6B-kwbwnaC%^9bslgHb;I{vGxyN(XD0GvSxrT}Rx^{`R zy~O#t`iL8AJ~G8Y1geYm$u)9!Va`aJ`Hg;bFApK*#~-!~)#d z2-q$g8$7t97dhm}hX;MBO!oy~2Z%X%BLg0{h5j>Id{OJt@vb>MbL658|pVAlF7`5X!jsfQba^S~aY@$!@ zZ_YDxavmZd``l-K9FR+Yp(9a`uaL>P#21hTY2bl##{+3RcJNB;`rA{kBd*a&U4L5@ z2e)qBqU(0jViVUt*WIkHS>ouEhYs0w2Tz^E^$ac7FtK2Z;>;<@;=1RW?q@>Ynt~}E zAz0!p510fsQ>^;34$)4KBKX2vC!vSUM=l;1i@fewoQrI9IXF@7CLzlY7C8qCAO*N6 zQwPw{4`iqHevVsLdf(ykHb55MR6jQa_?9~Cz}_cju$|WnKSlBu1me3^oR2kACCP@RS~{a9wvw9zTdD zT<7THTA$Q*L^s6L_cPT)s~$BHfC!2NAcA!o#0Yu>Kku-sV4pT`_xg4@dR!MH2V@ih z0Dhc_4IBt}a2mAR=_s}#3m_qA6Mz}rFyirmjDE@-JZyu8+-*a@_n$iCc|XtvJ%9*6 zqsRN_JnW-`oP>;2CS}^FcOH%dCl!2v8|~Fi{$-upDYPTS5^jKYR(z_8F-)z4{#k5KQ7c-hAoN-`LuW&8N{AT^)rXW z!-pSvv=*SI;<(ninVu3qGldKus9RlX+;FTZm|`RW>8Yolvfzf{fD3g@m9aVad51ip z5rCm1$kj>kA&Vf4(+Q-WC_DI3W`hpL63{)MI(?@ceu6(!iGT^?r<4=xz#!sEuZdS<;HOV(3Z^I_pfN3mG6I*clq7&s z$B31HN}DJ6*zcy3(0ZVAIi3Rm1Xuzpft8?%PQa9MH+ozqatQ1I3nvEq1Z4V3p8yR4 zJ3*ZU7|@130zZBM$k7d`5U8<-93JY~uoJu4@E1JDfQJN)0~C2G)B6c8Ck}G46W!Q@ zy(IhwA3jGv^vK0;PVYR(q~B~Lbkbj*anlAJHyEx99q7Vd${YuB_PI0kKBEV}xITE; zu#JQtkm=7msiPg;*vv5?x$Wox;L!mea{T5A-Pp-KbS{H^_|ZX*Ewtks_?-v6_=@&9 z^`9l1buY)pEuNXXW`D4a!7bUyzNhoWRJV-j4ycFa?aQ05rbxn*?;ZZS>D&!%IDVv4xj$# zf41}J$^%z~SH9wv;VWPLO8DT1KAg+B;_}PG?YG|^?z;P~@R5)HV_v`7Lh9ctSt(6= z465h{_3b^7`m;S{1fB_9kGpp18EI*%Crv|*aFL#&^6Yb1D}RfL5o4!BtYT3T=PWU< zw-ot)`wV2C~fJ^>PY(A}#O390}P2ZBJ1d}O)~ z0y_N##7XFd2b~0L5&%MimV^wzAGyec2m1)(1aA`j)I&?6oee(nDwzORJ6qfz$OcT% zgT3hSGOK-Q!!@;fG+>|<$J>o zH-umNmA6{|UBpc{-4wp%<=+y1_GjN>(^+0$Hy_T&=s8SM^jEiCCGJlNz{axkGxGN)bv&Cz zA|@3RVB2BX8)J~Jd2Y6e7ri8fg*>hK$ka7?paozEMqES$HabDDUd!Zq930S(J#5`3 z=W&1`V0)0J&gBxA(MQlFc|GN{j(zMQxol*n$%DVwIi2sPxwnUir#bn%%E`?Q6nc|IOdzG^3*<;irG*r^AoE>BsHZcPPS!{C}&X+WKz1R7^=R7m*HSO53 zJzOooBK=?g;WxrRWmBsB*!aKwiJu71yY05{wxQn+n{?Cgqd)Q^;d#%0Uf8Z@y7%6D zZ}^LMzB3=yJ@>gchc|xD_k=62yfS?Kfd|5g*>O3&-lr(X?#<-N;2N^b3Y#*dide6|I$mtZMWST-t*qS z3(vUzdYew4nVAir{M4tK;?{rv>%R(j-0`XK=YRRm@F#!#U-i1fRs&-C<%y~6ANc<7 z3%~cDe&4HBm9Oq9Zm)VRWw+N{-P-$ERxQ(8)~mn6!|^SMcviah-gBmD?K4_QE0|(19WR1guN|q&ht5XetIJhQN1&z;jItOBaAQE7JGGy>s;wBb-~oIM zfU^0%6?*u?@vC#}igc|{Z0ak&`l<+|4-2>)Na6m#2mT>E{`ljWpq20oKmT*#)?04T zJ9dZcIp(wUZ1rc~{N^yLS%gQ$`%6Fn^EO@es8%>`)-&lVp7ylFh?Vg3Klhe!+pV|i zm4Zj~O!ID&^{bj%{*fR1FXCc{6Y6|@ByYJSt`H}G94}UoP8v!)Wa^Lgb_Zp}o z^b?=_gce@;ulJ-;jGJXW>dPe3mp~Y=|acQ-QxUueF>PFWIT28wWbHJ31 zsV~5k+=J}d-nWhD@47bm4UOk@`KsvrSb7efm;LKUs{w|KFWG0R59(XTDex>WFKO!Z z^VY_~ET&f%bo2JVX|&3->+coFz2UpQE4=hYFR}$cJojPY&rkf+PuUIL@BPm2gv&0w zEawMEN$=2T^n?JDU;nxEl1suZH{TrY`})_d4Iq8v_kX`Vf2QKJrqlsbfSal1=V#AH zKcXq%V*1Y4zdl@b;6S)p&w4Ms@It$}Wbu+u3%~2#@3vLaFP6Rk?x%h-{MBFmm5Csr zv!$slv=wPm`u>)yG*Mq>S%0(b94iA=fvM{Buqo;o~4b5CTTrY9eNPjd8N#;gK zXADd?Lg#z6ffE8C^=EHBIt5e*>Fo7@gJjwRuk+m7d?o_W?*6EI*juqtB{KPTbusa3 z6?!fa=%MFHfha&q`qG!aWclS9fgaBmV0_^VUod(`>0ea4gQ@2*{L(J~s6bc0{-W{u zA3)W?^y~NEp93lFjB@X}=bmuYRab@AfBUzGi)15@Xg>4V&pLaaGX=~>dQ!?$W*Pth zKmbWZK~(RR|L*VpZg@Pi)ro%dZEp)#Tyceg_z(WT4`kJ?fv~m4C9Gv$N2}9p3{XwI z;g)%xwykfu)q=EUNwL%7rXD-=l8V#B-=5?tX4}ZN7v=)b089%G>1LN{UJ?Nh zK=n~-zeYvf*fOA9od=3wS=>ju*LiLl1#;YJrvbz2oBqVs<`5j~zVg+t8YRyrU;Itq z6h8W~kJwCWYyRGSCvd_=^l=ct) z@DIZ$?zqENxAP3UEV0Pr-@o;(;SYbm_{_9b@A{1b%XF7npVm1~^ASxc&abljqtR=t zX{|Qkn$t6P7PU+Vz|VA_q>5?T+a++_i#>6GsP zOcO_)Wko-(Zgz2>dg|Di=G~-YAEC4)a=4HzPbvKJ2N?f9O?m!LuX|ninV#hsG^_#yL?$_%jKIO`D)(14ABp^KJ+0PEY``f=AcrE72 z3~Z6XYeUZeW<6)U>#n=P%k+B74gu<~{razm$Fui`p)IBlfAph)QKX;KAJTqVdRTy+ zf_51Vbq{E9)PGPP{+*_y+o#l9DW!WZt?Q*+CBGY4E~8&Ib))-C(t5ueHZ;{V?KEg8 z`2y4}`e;Kx(&58L;$qekVReoQZv(Vj*HsR|v;1byWzUm1XDlV`fz@j&a(PZ<$- z(CJdtfP4daz>jNcQd;L_?nsyQHJ4se8gKs7AN+ycjPd^FGxg3Xi4Q`&Q>%6V_)q@C z_J84*e>uGQr++%!_{?Y8G%2sD0Ky;o(1)!3ZEt^jc%w$0S85aq7~dV} zPR+#cvUB~cCM`(Ti4*YyDrIv+W=b1-7?qA{W5&d<>2B>wXLE-1HKI1^x)ptX(+`03 z>^}Zg7gVv{_s29<#is{v)+mz2KmAB7-r)jg>elt7a=DXcv5|l1BWeRV}pI}>59Xv121k^Z5tfBo!AQWwO0PLMB_?cX0CFZAk_ujvs5 zH=QkORchKYt!stPfpYWNYr0;uMSu3Ee`24>9jy-ldh}~^(;Tq%JV|=(=74W)JiwW% z&7QB72kf=cyY^Dct_1)stxxy;DyVtw*6Kjxb(jXZ>!F5eGPbJJDKNl$tPOva+_SA- z6Y)(Zr#@_ISks+m8`e~n317Xwo+Q*_f{3HAqS**OTRLK&C*^glUeY(TR)8BqZVoNg zvZ0{~K<6a-FH&E5Z$PMON2kI{4#OgYIhPlgpnfBk?)4Dn)JH7MQ;W-~@ z+%G-%wXJ|+W8CN3@tZ#Ihi|oalOFrVH^RZk9=AoGg?px2Vm1p%M+CwwluNJaTFh;T z>VYXBI^x@EgzHK}6lzz5FAHD}v&w*6Ds0=I%s|1TQ z{BZFg_16Q%@LQDLsl7;F+;{%-`N+8sioWS;zfwqLLz8$OF#xDsQz!Mh)!vIP>bSyp zPE)!M==;v6L_aDpOlORmgjehU+0aCM`~426bLv+B$jS zWH==7V^urA#i+6lkV3=rRCxFtCkviVX{Ibot_3YG-W(uBVbt~~sV$G*{UH2gGzXu!w z&IM5QzVzQqiNqEqEfooIz2e;LT=4CsErE17{KPW zF~FJ?bGBX zgQexOtGW3HWSC$vLliCH_Mh4H8PAC1U z2d3P&dEjJRn~rT;qz@|2Sr2gH7?66nNzSBmw$q(D)-NA8xDG(IlgHP(-H)iB7I z=^LCgX#=`@Ji~hc(mE_DO^=rK?-WZ0mIE;DpIZl#&QlL?aivZx(ZB;m57fW(owBzJe60d`ncQxJb(xH!pSs;|C$n4oy49W1x3eRqThB5! zt+fxNV>KX(JN$&N>6$LdYe|hVTP3gMCsqbvTIV4wv)(17($42{0s`iSy5Qn~1XEE)aepKF z*KPD18|(Jn`;+pjZnYqaSU<5{MMYkdtb)6&x~9aMWMvvE>kN=$>-vDH6b|XDNkgOh zC^278(ibCE^d%iz0zD!V07hQ^*OspuPOK;B@Fk0hO~YYqbT}+7t>|n2OJPwjuYYjKV= zThX7rp&`AcJghH~==;vYD#tscM7BD)q^~J0EidTUF6j6aF8GABpGCV#8ygP8I!9KP zSHiN+5BsQ7nbS)Y_v(M=8-QwwQ+ujvcfJ5NX?t4kN!bQt^8R=~%fQuX3Z}0Ukox&v z(B9*27t*>Q?cSJ%@O1%Gg6Qz3$uPEMdl(*{3@eKZ`l!-e7!r^Uk8Ls#T|9L>ES)}~ zGR8Fwq;o?8o~iNCuzSZ;*fKR9X66>cQ%6sQf^^GNKz1l>yvk0=?Sd zAC^yOgWTFmfY_83{>0HeGUG-wUkO34>n^bLWfy&$^kWIr$3CNf zL0?sxnL80q&7V*?eiWiF22=^b51U59__nDqHa%`YI;ZnvQRm09&W}22wP5I4Rx78D zt>-UI1(2L`4)K0^-l2p20Mbq>n<UIcI#DcMxFBewEar&1iaH=|XxWO?0dam@*n2 znV1UWJNAT4TX&f&mlx*k+onS!8fl(B6=r4B@|?b^v&`tUxl8)VZGE5^Fx?z>Zk-Az zPwVqj^NadnVvR6G?n#gk0Res;D+2haKxcYtEbQI2C0wv;tMRGIc$d%>N#QhL$}1?i z9UOOd4RB(azY&m7#lM&w9%3ybqFi#L6CohDWpb0*v~&A(IDT>_Fj}5bpXLRaq!Eq8 zH>=N^C&!Kc^z4FuRCiweUAEEeX7Nu=j2V!gn$ZvL&Ms=i-u?o@UVgHOpC96PFDV10 ze5;a;AMRy;EfU{%p3sjZY@gm8woUD{nTAC>*0I0*Wh6SksGBVLh&J{)M)sAZ{9q*3 ziOxLFO*jVtAu`{N0cC)adfH6^IwA3V^969W;$nVj;-(FY|x zbakjpwfEk4Uk_Tul3W<;0Hy>qjife>htbVj!^F-DtZd&kl_4woZsOeNp|Eh`C@7K)h$i zv_N#hz-ww^M$@drHq{D0Kr|~bBW5y{3~xC_3MY!deHqtZKe>7aI5-K|UDL{!Hz#mi)Ce`wH7F%eGaDM6Ps(rd-LQPiaaqz$#}YuO<2fQ<(>|^6zDx4o zGRIZ)qZ#ie9g9i%b4tEmRQueC8S%~7h;>BA3SGjJ3bd)- z(M>i&9i5uC00nX_X*9ZU>V$$DzeH1hse%vo!D0opMV_~ca{KgzjqJu1)Bp;mKTm7A z^vNRv98J+Na@(Vk>@RCEY zX#@<2PfHfl!%rPK6%HNK2u=ZYLP2)#&dnO_3LHcacw;@F%r;7(m)toGoezoc_~}{6 z84tUpcT)1k$8jBFG@#$MWm59D7+}*MZW0Ky^yQ*G+w=!PKs}cn42V zA~+7~St$#Wn7&(9FlUs&Prcb9CD9iF0TH93VH6mrJx2@#cPUsh%Hvrgewax z&dby*`j?;4Zw1%Mig77CJcNQ6=Vt4LmCwUIxPB` zRPbbEN00@K+217a8ad9QpW%EOmg5BfDGmD=owDGGlW$D4$l0b54WLK6x?*6zw7f~9 z+F1kEX@ayGxCz*-z|8OJ%?Z$XzKai7WJDiQAoV@u!3(f6nr0Nu$b5@(ZY*pxtagC8 zUE*e9i+(4N(I|j=!37r>xKa<8^7BUA05IKq@ZdoksWM{iH<;o}+w=!O1@IQN7;RyZ zpK{ZvTcgb}O$AR+Y?H6?x29Z!rd#niT_gzTVF**K;~F_`-Yu}y)V*9NpyU~+rif|N zc`&S-tSLPs$47uDP9EpxL;IUJhjha#|tlJAKF_jzvLh3#PR4aLHR7&vY>QiuJdUr(iqthBC zoYaV9R==C7;F=q>SOe|#1CEzlo;jOw4ws@*6AUehDFB{hmUJSI-_T;dVa?epbYM_Zvb&O9vb=-Ub z*x?XHjw5PgTK3da`cYxs@Gw%{q#G=I-l_g$)67x*II%!+^YXSZv^}05V*=Z8ogZTY z)CJunEo+(?xnsJ4S=6!Q7;x~Gble%SHmq|i&%fNy!PWS}WmI{O_SML*l6x-KxA-&} zmG;!{7J;dhwv8#_s>;Mgy98ULbk^T4TE=MJBD8kJ+JdQ>Arm#NJ2J+dJ=2=mbY#|V zrgo{fcD;uRm~~2Ynvo695Ko?-*K^EM_R0*;LHUJtfOS-Xlo24)v%Cs3tx@5&=_wP< zD3{SB;K#^iSfe6FC@d0!2f$;bL_1(g8?Ubbq<|%04If=#8W%fGoYI0Owav{>7?|?B zl>}(R!%YCdHK*r#M^EV03&~>Im$s@AAo2lP zuCjahB&AM%|OzzJB{%1(ZJRG{M< zfm8m`@#0x1z&0v>GP1NgM)`(O>8y_bsLlzdZHJh`)v@PB0-#*bbJSTK51yTlPsfE< zO#8C%Xc#*Aen`INI53UO%?ncyfIhNqv6H+L=PRn+6wj9G_Vc+V3k9`Y0PO%!+qFl z)S?~%9wSr?;T4(1SuH}EQTxnk+fSWV<{CN;4FPR|)xZj(6AG3#+Ebg2Zq!D=13*TTB!Kg@o|6)M7g)feQ7Nm20eyfH#AI5T(d-T_co`EY0dcXIy zR&MJV>6AvKJGWdAc4(Uvr~;hS%?NDi(fq=Ue9MP9;&baXfHFM{uq|_Z#dh4J}2dZ)s>#0Z(1|ZN!{&O}+TCwiDJ= zczY{Mb>mNa;(oSvsdH_?6wuSO-l9InFn8n$1v*WU2}HRP?D?qL=1&|8Glw4!vxgqj z>oG_4yi|cpqnOs@ASJ*Ouo#Fb2gm>~Mrr`g!Z5GL=nFd92SAx-wdbb_sDR=rJ%{5x zRlu0ly~wdqjEo{MGHRVrfK?n8@aPB86_{Btgd7sjZIN@5XQE1s7}0Gum>n#b!B*r( zpq&>D)6tA90r$OH_yS7vY6}B(yE(`Mvx%kB>_Ps@Jk!|=0$}TuqzvN(?{@-~y%`OJw6uo|6LHepBQiz9dj) zw90#{<=opv;<%b-L2k!F4{$ ztLVxagQ@(bz{#Sbg=2?x@(VdYDMkWQoAu0TKFkVaXC8kv%n4AJXHQyyDtcMuHa&Vu zQ@Y0$C|Om@^eYR4PBKj=@MOezTq|Z7HL@y~Y2KY%wc1zU!;~(71Bf0KK=O*qNd@8v zxN!=WvW)hrhdgL&T%m|mnt2oD`lMgQNdJFcj50J9Y<$g?4h}gnD zQ@Ttw+k32PWprtM5HK_S3$OzSLY{V#)=cAPU)tCn!c)-APEfycqF-Dq9_2$n;$RB~@v<0cYWJ47ig#QN*7*U*&g=Ni96h1(Xqa5m^J4)n)4ofLO7Xj7kLcL& z>dk`QJJrFia(ik008pb_IHs#j$XMr4yIPa`%HIzkuHK+Eup$?^jz>4w*{w$!jB5|3 zvW+`H8+IwF7sP*|wLLLxwuKnk$2Xoos*ijgY|i4JFrg#ap62lEUvKKBHu^?ss24;S&= zlhF$AhuT!BbfA-a0(c<;%;9Ch5zj@j58D8363<{+Z3`Iku?gNS9UteNT8rOyW59c; z@Gtu34aE<*VkaYB?muRw8(<$}npbTAHq*rP>zLk~oK;-}924F2hk5{#Q72`9l~F2- zj(iobf3$%%8|gMl=-&=^n>-&qe)_Qd#Vbe0bbO*e08{#dV|g9s6rUB;=A%`&LYQpN zI!``+%%*xL^!m$)7A5g9hB+PI8GYPhnHv?I8*?Yr#=^J?e$>oAW8G#Z}MD_4wq?OCeMgJqqQV++{c-9-p;d$wjKy+EpQhByob!sqT<10c359^}|0x1HY{u@XFKyw;hjWBA}$btg^ zxDnt0DHa6@D;W&i6doVQpH-j*;0{X`qfNz41#do{FF=-Ho|&R!ejx(cxB@kYlqo<0 zFDEgeOWoLrz{hCgir~Y?WS{=gjtzWFA=(75rinE&;@Kz3p07$KHv)W0kP#Y*zVY>? z!{R+DKp>%;=|=2EANKLQmBhlSc!OcPrGKF3bAf#22*6}YmsPv~=giDZ{(k9N0x5=> zUp3u(_>^WO7ETIKb+cn0Z9e*O6e}>)haCW4bBgzj{sV40z5wQF-DKJGP90;UTROtHOTeYl*)YAL@?MHcp&irCs7~Lx`8RS z+A}6S3tQ00)K={(kn=6xI?tT|9zGQ~Q@nEI2*8UsHyO=Y%A9jrqeK7*7OE6ajh2AU zB?v$RPTp?QtR^7)lgT-$>DHE%wfU8dOeCEiiVIgzf!quWqXF7KIfKq9tzi$g(Thf6 z8XG`f5TKJ{Yr|G*;$Cb+R;PpxMyNa|1yJL&Vn)buq#ym#Ny_Tm0m|7$eTisMQ?c@Y zNjOxUdMtZcxWs{L_6}95k$+a?pG9T~Opcro$s@nomUNR9+f*f=ip|=VbQ87=XxjZq zb_S@;?q?#AwJ!{7mIDsj7&%LZCFvW}>hl#on$%@Vv9WO!+X!)RNEzFZZ-geil%3{l zYmR(VOBX&oi~LvDY1x_Ep4`u$_EcxSHM_J@Lx^$3Yd>v2^X0}_zOGj4xlyx^knw976JPdxcV zxc`CsT;#gvU3sbtAV4l5w`1zTcU{@s!GKyec92olTnaZ`_^(Xny4JOEviqzJNpGh1 zS>PE0)86_%;2L<~OnIOPZ2hw18P`1{+;Y<`0=dV--~Ro33=p3E%x8yZ-T3Tq&%Iv@ zAOGaXP1TED_@Z#&X;+3XfA#Kg$EWYm&1e>4YL2va@7^7*z4|(>EFU|gU|QR6-oM%m zT@ZEwsQrw$4hmgZ+8gOw`mQ&N+H=)%a_Q|td%5jeYw2t!z}p&v;tCe8ALIiM3_P%| z9ys{eLDRQu*KRE+iwoWOi9FK97hP;Qe=gW_p_RXp(dP{W&a)(VHo{JBeJr^B-WT65 zkagOvl;4T5q#arPl!uD7keQlpVBMqdQaKy7>}y0;ZC_sxY~vPbFfv{Bkr@a$mpo8s z1ZvZ*ed_d~!-wqaRD4Kd*Unw`V*`73UtrQVZ=Md@wrh?3A{Y*G^{8-q!ZCiNxOTI;m^=87+V@JbfmtGz|cjxC?tiAra z8^SeLUaj|^PlpeD=>6gH{g($u$>^KW*Rme{#v|dMKl{&Gur?oF@v>K%{;%D4Pq^}m z1A4V1W8sc}zC*O9wMu;?+<3!{nnu3D-d#SWuV~%(!2RLQFMiIZs%gLJInNFI zF4||dW7nNux-&fZ&;w@IwEA|7`g7q0du?&tV~-z{>`&?ABy$=`kA|C{eY1X`;ZprN z)0EziJ{Io!%H1|W8wIFGY9Wxl2);x#< zvVjLu4>aya)7sw3-#GZFFb@J3ntJ|m1=^oXSo_z92Tm5|dD{c>$ zU-mS6rRju#YyYK}g;%N0e!xJ$wN-!!FkN}zD!qrT*N60_u{1GVe2c(slYY(Vlb`y8 z-nHHwUi5+&3oy4DP|eNFhO4i-MgaLNV*|*M38=Bkd{keiLjOx&^fGI|`l_qLvu=2{ z>PNzZ4?Ykk^g{|vRbRIMX*Ozp;q$&JTz}2=_9_-Y$q&@O?8Pq&yLa!gy4zp=D$!mZ zo;viTKD=@)9Ju1j@XBv~g~`6|+Uvu$*IZ|xy!^62ly{?F_`DZ}-5SZ4rLN*-$tbQX z{xSn<)b(9QNk3|su`x*c?u?Rdp<^BN3`V9Uz8_SbzaH?#K*bSizWAriHTGIgSwp?L zN51iBxZ-IC!i56Osj2v542;aam zj2geLQ6=v+FY=A%Bv9XZ*PY>CGy>+^&aZpT>jc;r>cb(E_QUdzKk-DQmoFwYg1l(& zMPc{O-D-O+!nf|d*noEH z)~y0~-s9D~)EcES#eL8H_n7T7r%zjb*&pI&?VBC?myJ-@A>QWZc4cGERZ>WfkGFc? z*Rwyadp+{5MyKoOgaMeY$I(9rbpS5DorB%8zR|A~R4V~>m8*FHNfIMd0E8*phaY(; z9MTu7E)sZ7Y6_V2s6cn0z?F39&{OgKV3nUz88F?oV`mPgr!{>Wfi(-QSx{k$*3*gO zCk!l^vV9QH+rK}&==m?Q=Hn-hhfmz`34H~O?;^*<6f+4xutiB(pL*m*AYnZ!vIXr)Y)*B>m^V!9Z5<7 z=Pd75srhOD(z1UID>8Mua(%qessdmF_;v~m0jPWLzt?>FkN}nGQl6pO*Q*4YJTE0} z+qONQx!SHW>8PG3dP?o|I?rb{jXNPweco-)3;+1>e>7bm|K!KQXFkVs+uh;mS3cbq zKk;1lUGMoDXA64-*xwMKGSxaUF=_QDPM*+o?@J9xzo_S+_vpFmvjmWgv`Ht`&U?-L zXaV1X=2wXTN2aq`e8n?R(&JA&9zOJu4_bMXz8}qEEZT?``765#wo4RxVC`0O4;?D)KD}4)B0W; zP!>F3U?Na$7q|;7XXS3%KQEK>^(+lx)u+Y3@|y?`m{;YS{}{EMZ_uG0^@19o@o+2gBkf3>}8b5P%C=8IS_e&LI4 z6#B)xz9`_1zld}ydv0sS=+CpB^&DID#E6lbsbiWFKAlXr?vVWe@n!oja~6}$h<4w; zOSF*b6$YSJJnag5Ui+9v#C#K)8wI9}Z+iAkVOG<**XtQ6^56N_?+mv+_cnVb%z~%~ zAAV3jFc6>ro+#LsE9|0B=csidZv9AW0O|S}vjH`&>w)FBtw(Gfz+eH=20RxwvPlGV zHbxu%8N!Wh?`mzMub#Z@x!2dv8?uT#OyQFFKC@5zKKay>HswqGBLXB6qu9UKv&X|n z4uwkuP)yApIeH|#|AX(hQDAOcVz)77fA}QgVXYwk%x6ArHP5@{HcclV3U_NdmC-8T z#o{8Sem|YjIR8EOe9fNO@?0054`w4tRuVI{Rs)Ss810)MXMjb{(VBJXIdpoMKH1#6kN!>&~4!+a6Qy5J6rx z5*KTJ!LEsE1EFVw2RJXzrBroJm(_dSh<>nOddrsZoSSb}fd6ur?pVG57=;o{=JonY zntGMYF$F03A$dltp1%FtzTH;yvfzoI>mwF05-uiI2lFb8`-gV7g&@OB20sDF4MAC= zKgdRZG4V_lUPi*H{32a(-}QTa+hFHJ6|c;AbHDPUZEfYQ0_bb?xt;R10@^mVlUi{{ z)1IZQ)hwvEc`T#vXWjMJT^IiHFaA92-Me@2%(S15J)dNC6VT2kzjW%Cw{roXq3bty zCqG(HOkD-&0yBN%4!d1qB(xr46)QuydkS)XWQwJ%$@Db-(_*MV!D zK((!1ZcAID>c*b>pNi`!fHSTmQ7ZAAEuZmcrs)XB1Jya}?K9A^PX6uGz8aH}-Rm*! zhh5#u=+4%S89>~s)qra8nE#XBMdj!Ej_C*GdrMf=Pm9`F;T}k7deps2AWJPME3c0<7GX|yvIyRvJu?;sou&ILTH$^{c>CMmzW!iJAoF0y`)d4}1wsATv11nW2yC=_BDi^g^Lo#r z@pkIc`n1hukwZg}=bbl3Z2&2OmVi${bzRO=CbL-Qa-GL*^7@oMm7mr_oASA=G8x{M z%A(Hqkqc1ZSH4yQumPli383cfz4$-1+4Xtub~%{hW7_aDJ_gjP02R6YvI{#{tiosO zw&`27er!3n*zgS?i+$+i-AA4kx^KLX*zTM6&D%Ib?bR4>=sEXb)HvrmigK2eu!21$ zP0LR1b-tg9>|YrzsfKjlEpTnceXs9Fp6cy~yp1cPc28IBZjNggLb@Q`xZX`I8`5g? z$HSZ6^rrO#Q-Tm1K*mO}<7+Q$lnG=$lJTiY0x$J!03AUZ8rqRXI{}(@ruAqeP|^lH zZDj$R00}(-72T9c9&~9ZAqRPEBxJfBK7|P_?WxUf6Fk(XZP-dcho5?K5_RyAr-0+r zm1*D52@ST8V<&aUCLz~tLOvUE$muWb?feff^vFgg8}i-v$Z>scGy9&roZ3KJ8K7c2 z8?xE@VHbWu7B>rwPU#Cb7Th>_|Fw$Uz!tlX964hB;rP=x?>}<9e|{Wj_e4L_eXs8= z$A2l{bSb6b{>a!tRFQm@+Pg3|)tr{Q5K)&j6`%WK{d(GsnYnp&nJVbg-lg4dBCl*{ zq8jXU>49~d<|P;bUW}9oa0E319HRpQ5&_<)B?*EAG=PQaUmsP`4)`Jm9@+`ej4a?s zE;Q)!5vPCp%mIp=0Ldr~{cP|#0HBW`>Oe|cI)Z^0{qRwS7JI34o3V+FguS$5ANKkv zh+t0}_1J_x*b1M6qJs={Y~D9)MK8hs|7Y(#;3T=Kd;dE*ZPaRam64Ey63Pihf&gKH zNH)UoKE@;)Kk)x!V;hVE4}$^Q7{fEzet=CdHVA|e!hk>lgAfQKB&{+EtGJ4r)5PwX z|L=RMPj_`scTb1vnVQ|YyFFEP!@1|4sye@W&J6`kWI-8d6Q~dR9>_}>uaoxFlRs^V z^Nai`4-b?j4VuUbo~hRXH_jV%z#FpiK>LuJ-;ZXVi$?iEF7QL$4(P}ZzC8|$6+lJC z#34ucB%jMUe!nQ|_mMKRb5eEC7IafR{nWUkgi7PNU8Yr!)flQnnk28?i`L0rwaU38 zoiaJ&=dqGCqGLH)JB`z|S-Q!$t~JJ@Qf2J5K#1~lwkTdn3w2~gs!5bGeJf8aXMw_p@>{aTScQ|0qoKc1-Gs>eNQGTSO%t)s^ik>)>HH{*U zG|E7eA9SeC1LaYK0HfDUdFTTWW^G9ElSVu|Ko=gM<9t9DTGUHD4p^L1hJ3UaUdV?c z=EpTZ_=YaLpx`M7upob~DT9)CKqihKJdqa~l;s>=Xb)vw9>lqfD91g3UM=}kx69l4 zB`<;U+&}83om{)@s3#uafqOtl2p(y(%L9Iqqu--Oo{Luaf;af$$j?F0>-IJvA8!ly z3x2rvIOIhhe*PTrjKDuKB0l-xTBj4VYjCS7tyZtvvo*>>WjQaO_M@KFB6Ky{)_G`C zJ#p4cD`TzUjAf-dbCD)q5^f-{Me53eKg09%$-4!BAuU0!e-pf_Wn0V)pb9g{T#PM?>dU@!% z+-M7+24C<1Kjeo7ah?xZBMU%=`~WBU0CeJz8Tko-C13!!K*#xT9y!8;U&nccA8$`J z`2#jyS2cd&1$y4rxQtznmGX;UQ_jm0@4U?QT(rs;{80z(=D9(hJTu5IZZVUGd%)43 zFXW7j=p^|)gdPDp+%LDvbHuCcTmfA&cdkk*t(B^z((JYCJ>%CenkBNR@)sHzUsu}v z%}P&;=lz}`ta-0AHS$Jx5PA@ZUdu9#6cn>KAKDvNeGrXqe$AI0m64UpiRG*|qj zqqtFM#B&XB7_&M8_9!}BZ_p*pk0@poA_|nUP@KX&OybaXn zpyIr`GIoF?p8DZ~IIby!LiQhVfEI0`tpxa?$A|t2G zJs`k0of^{O9R}~{ke@biKe;E!gZtt9kp_>nf%gQSd!VgjBf)bur>DIXH8fYU5uUO< zH6*YY#UAImMzcB-Jx9fJBzK0Vd7`R!A1t!%1P{?h*<7G-__6n&g+5)lo*Pbe5APobbufoB~2PXlp}dig3xhLrC#cR4q(7F`MIV%E<9*a z9_7dnW$F2##gA(gD*OVz@CKjIBAt2((4ubAks&;xw4q5J=n&uqdhkIW()mG;G>%>l z%ZPJmQ#WOxPkG8Zuf)?<0(nV?jtBCQpWt~b<&PX_S0%r+1^!+Bl&36wz#r$HMm?M( z2jmOS@Ipt$Ejs7Jc|wNpnj~Me1wLH99HGyBcUimaT$b=b8RSRXIKr1d1EfKNwsB94 z|H3_WIX80TDJb2UMoM^|)=N5tW{$1{TjRB|CZ|>zpQdZ)v7*HuofNAT=U& zMT2TEP$Rl2@@dW-)3MT>5r}d`sh}`WApUE&9j-6|2?sj|1L{K&qpVPHVXDF z>Yz@}NrN8z@{7ygI|bD5{5p>gBys+r?|f2*Kso4A2S3UoS9jfWzu0{D|YZLH5KWAMP_p;wc9WmlyFqc1-!*qOC@Il?@+V$Fx?m`BDut zR>(*V&SSG>7k_?Q-a#2x3bvi9Drqf<@M|zpBYHttoQFz#AAkIDQzR%oeki>-sJK#X zhZhtsGyyD>ky}{MM46(*0235G$_p?>X;Tg*$2vfcyHI zgZksLBh7!_KI%rs&>_wPyivB^bI}T4w1@f#)JMCBE4E07hSm-?SC;_jECO0bZc$tM3Rgvi! zQ?b(pXRnhCNQdF#{g;3Fm(aU@{d!X@t^iRoC~p);QUO{hQePa8;&)}_0OMdv-6(q$ z7Uj4`$)R-l0e)`rpx{w<{7`<>0r=1s6eu(SHQHo~PYr@5*QB{pr5p;?^U!Y2p-k9Q#ZWQ9@!0%4O z1?psMK$2cvt*oA2r6ikxA z6%|Si06@W02k|Ie6qe@!wA=!7MaU85<-kcg<)B9z*C;uZ9&I6w{FO?aI*F%@*Uyn( zWq=>@ZrQTMlq|FfghiOZ5a@QG~53viRqf23Ci(g3hVpNmHMf}YFR zd7`X$1pJwyGeGd?51GcFA@Xtm=&ZmeZ9)dLkF;K;LBHLwcFo$b-`f4GF6J#f{K!Kl zH|o9cg%<`slKt3Y8``a=SiVzqixs5gWcl(H;h+N!G|$4OO&h}_8y>aivlaH%>i(Q| z%4y-dSAM7I;@sbUb><#@^wF}i=zzlyJ3KtK@u@I1ITcPn?ey^U(@z_ZIejzssZCGY zv*tecXCC^@ux-b7`@nmg5N({$GtUR*qY!^4*XbJ0!UJ!qxog)YPUj~?qq zAXBQ0Q0h5&aXLooFC(9)y@$T{8rL-S{&=8 z!Ed$p#OsQ;As$cN0HuSUTfcx6oeGpX3ci{=p;L`6{8Y>Dd27iZdX$Bqc&H{v;>Zh_ zIo)`f_&Mpcr`6{ITDAC!*TXq&gC~OX<+AW=2jO@g?gJp}vhxlMc}BXU4h-k5h7*oI zF`Rz#Y3h{iFok&hvB!s}H$EM{fAy7NW?J&e$!n3X_qM+1C$x_bM{% zzuy7jIcGd4eEYjsGz?75b68Pn3zsK6`-E`nDW~hcM=t&2B-hPbHiv6|c-5@uxDoPr zHrA|O+ep?~c~02w%>BUq4+!U;b6)uNe|;+~UAZJ2{>&r7GX<{W z6Y{{xL&ClN>Cb-}?!EUOqj}a@XWKoy{M(n8=@p)#t=qO1z_jv?#o1}?d5Ke~dan0) zA%fMqKy^p$Ewwu`wdPR@C@oi9wW_U^K3CaV<*L=wY~FhL>!nfmTcmS4jup-%Ab7Agt_>sm0l~pR^83yC+}SOYo*tUuf|tp-np)!fs6rDw?+YA?m@SC z0t(8h^iEZ)Hj1l)DBWG%;Up>jd+xg@Tz|v$mhYeg4++ci=>+UfdrbnNTz3oBQwEOt!FQ>$MUq*Ohz1F=|KK+&%Jkt|GDvh z4D{EoUK3s!UY&OH4&;p{Wd4i7!_V6oFaI5-r}IqRHo?SEferNZj>tUROVY5aL9FJE!BKwxIA zSfIKqBw&~G4yM(Z$l_{C0H8#9V^!f!c54r7jm9d+V~#o|{QMU;7>!rI;?-gO1M9<~haMIN`Uk>;4?PfmdCN^jMeLQ}%6`o&Un9Cd zGP=({|9O@Nx9DnZpZWL`PlR>1-4b&fMkNn@#$i@JSHJ$MUk`WPb9cDy_FHWn{;V_3 z(srTyg-JcH4?X&DxKXa@`148gM@E)}m%rp?vbw*o4$5{Tij|9OZ}^`Zth0E=X=eoL zVGZw-Pd*{w`>Azg$a9)3P1@F{G~{%HK3vYbQ5lYqg|}eDbL$mH+4Vo{>(60We#mEl}Z=$(r%7L~VJF+PvR>`-{Kb;a+u! z?zrm?%ksSQpKl$meOB(HjaXKN|GebO;`h;T_@Rdv!L+xxCmgJ$iR;8uwZQv+t7Lod zK?iF~)77f~G0V0^*7YF=KO-D*#F63t`|qn(w81!kAX^1hm3I;i)|gZ_t2RYIO1q+c zkdkU)MyDeZK!IZkp)|0PIHKU=imD?OXGDt<;2yZlk$dy9ucCDng|*9A-dSg!70!?w z@IZ}y_4W2?Z0`{RQxrZ*+sEqxQh??c0;=nNdYz58EnUKX$=XarRxV(5_%jbTP`GK` zP2tERpA`-h_*8}xUEI_&aw!glqn>qictm5h|1E$W85s_zo_uO$F#*mK;f}i_!1&Si zKMwb=zs~^hB`^9lO~M-vKmN&&!Y#M03r8P$R5(-ZsSG_DBZSAEo|5Xp(&9&W))CJN zM;v}+xL$3$`r50)@@31zX|ga0r|JlgH>+Jg{n_<4#(e&{&$YD5KkQEpIC9V8;qJTd zG`hr{e(D*bcZ>jQUHGxuj0`S(;Y&=`$oqvCyx7LDfB2(o!`=7%Dja*vaYa&eYF;cW zm(I|CU-!ds-~G}d7rX!fKmbWZK~(pK(?pN)PXe@#R9ha_eZ2ab@7p`Oe*OK{*hz}LrbMLRLlk^+`{Sw{#T4k+ZLw~5ddi_<417h~8 zn%eP5<1{ruTIz^o00}|%zLsgnZ+&tVG_Sy`)y>Us;>D>(VnO2kp-ms}&e_~zfp?dG zsnqwc`9WaJ>u`bI@y9+pZaI3%y5F=o{V7bNvmgu7! zqKPigkOh12fd|Xo`Kx#qozIrzBac2JpuE%aF+OgdWY#-=bNPmtFpiVlqH_zd-AW zf`xdetiUq_C_Ngtj`K(XIv9^U@<@3zOCmR#bTV$gT z(HUkE5f&`K``F_fW{uw-rH%-4rbBS3+`^3WLO&jGQ@`pJuL|d%dtSKm2j7b)wQ(MQ zCMtyGR2LY;jKc-y!9`JdT(n?H&zg{C;Z_RU6lsC)vNU@*;tz6JYIGs1BWknv=nF31 znFhy8DKDkdrQ?js%uK7t-e}n>>{O4^JyH82KDj?`!KrO6zW}1w4c%y#_Kdl09t~umcy#bBK#_nTtHF<77M!R0&66FUd9eeaK;W&Zn4oxommE5QvSpJ3Uc88R= zVPpz=I^Tp@Spw4Nm7b^vlbX=o0MI8t7FyYSw>)i<#$|CmwKfC4b^)NbC zIwubA25$Gm1fut(j-Dr>}vV_O^6+lF^*EeN39kuIxqzcm`)zONE*Y^OFXTfJP&HB zgMo|yp;zl*<=Qb|)!&&JxodOU>M*ZypK9Rd<@&XoP@jIi(OTE(oF*Pk><-g8&8`I) z>x3@JtE*R&mj<;3qsq@{c4K}*Ye%OvYr0yUb!gR00<3$qk8qzppVzNhh`I*UrfJ*M zSky+lNB)x5q*Rt*yo+YJ zU(A!xOTCXiT7YZwrdw_fQ)&|m{pSJ?teh3v(JCIIaq}JF5V;4LkV0VWwBZmJef+#g zplW0>werL}w2UX>V#bo&RRH9r0`LjVNUa1?m*3FfaM-GU1ORuE8Lq0HBQ96oZzwGCHhJ()%%$J_+APb`^He+HQGkaNo_(S`5c%-1#+EC?x4-i( zYinhE<@Hl?V`Et}W0}Bg-o!%jQHW0ba*qC3V%1j?vicKx=PGHpckPmq`e~2Q{FX+s zGM+S`9&f*Xyv$5?7*}r)MTFIo&VY5LEeD5|hQ1}ML(hQ5ObSosw0f9RyEcW~?yV7^ zM*XisGYaK~mxsRPYs0|OeIuX}0OrQDBxak|rfq*R0#p%%q-?)v4t~tA2(Cfmq)RT z15n>2;2P8H;B(JDH{5vhFKsfm*lPxQ85c zNH|UFI30}P4;tH8H!NUfa?_ZO8#hO5HL3gDv(8hSZVXEVw8uZYWC3p8Aqy%DJecFRqX$EeRKj0YA7X7$2tTgKH6NnD2J~#{tw7XT-z|??yc^H*d zISB{~Oi8O=r`l1pzExiV==(ax&(nb`xy$ifJpdwo=cC)4%wWMSBtl9S+>*(qq zUL}A$zc}`Zg2m!k|G@n=K?Ry}%o}OO_eSM{W%3I5o0G*F);3X{yFD|SHA2by^DF9xyt&; z$^gCp(O57R=c_MzjjgQ(jGgy8WOeSldUbf!MXxqSwrt%Te)5yZ6?%il3HvnR==lOj zCJ5pBz5e>^W&tU6VmV%`2~1~dVi(I{kQH)e0@BSIQzk$UI?sFF3+%||ELUIs1KT_S z8T{}^*MxJ_ak=PauQUt$fd|*yq$0{r>D{?X^v@+-W0wex@z0wJGjprSjdFzbvQAmH zkvbrI~XMqxU=6?9!*A{pv(WrV>xaU>VDk7W6T6i)`1WNEyimN>1eoJfE zedibt@wCFj7*9^^d6&MQSyyg6gt%NHw`%lq)xFsrYASs6YRqV4$?~vFV;V!l+KX0v z0j68klia;a+v=&uo3_9>W%q!96hK^aXc%5~pa5+oQc`*mCU&ywoPq?(U1#xkI~fiSJ%4@TIFF%iZeJE<>X`LjTCJFtQ)*9|@B?wuJF*PfLN~ z9tZgJ1s=&05Fe8DN*EW9W1AnB1^c+H;Za%4D?}G-8GxTMex_u-ntWyL_u!VcHfo<< z+9;V$iH2Fy#rt8Ti{QdLz|sxegeRYNTDa@3xm}MDh1!2{GxqigBzDM+?XoRocXe~} ziKm!bl(m!az_OULo^y8i<`v)6_+Nr$V%)LnuuDwS@IEvC z%jN{L!;Cyoa@niyT$N=nbfN)4Eb~SJlbVKx2E#7NHK$>fNO{9dG#=QkPHUe!jT7>2 zxneIW^e%lIVj%idWW1g6dKt?m4U2ohLrli4@X}`SDpj^gvCZOD7)mtixMPnEmwx?g z(I?mvrQAVc$aGq}!LXYNz4;#8y(mEiy_fvuPs)OL>QYBUuGIkn)8I;NDYi^t0g&kB zGfNLJkky1@*RH$bzcBtuqZHRGAn6}op*;G!j`EHR2q$-L7Rc;0cWuAGU`PPnFR;Ku z1(X2G@h$o;ssL?h^?~LdrcMVij{QqSQ+)LcXd^Jy{q z4}f@TY=^8&EMvxF)hVHKA$f45Uz^vMF>;`jF(42ot?)3YDk*sZCqJy(qE#;dMt^Y(cWm=;*exq28ahDP#~WJlh$*R?NJrY38TuP(qO4kR&<72K)BYtcY)M-!6+wTdmeJi^t8YmyfN_CIqD2QhdF#Z~)YDjowt-=OuFl1maZy z0Z?@d;IQPtojfjCEG(>_3QGj$7-z*j*)tGz8W<-Xm&G-=!`ws#k7qPsu$B zb3EbPCjl0~3!n}dU5#l9u(}2IScnE*Xbi7Ygs0TD0d+J6mTBxq_ct$#rQ2N0w1p^H zzv=|=n;sHaOA!O6)3{}E_aeW762cKC6cy-doLV40v|_Efx-Em+2rzRG$ltH}`qZg_ z4ho)oL?>Wsbep+}naniTGe)_FLlarOJ#pF_Har@>arvcT&Aw~Yz8;%l8lPnv&%1Ev z%`~!-%Xr!Nd1b!Ecnv%k9#XMiI@3kGsqyo9YG(3PCQosmMA33Jiw8`3-lQ;Xd~`~i z)T8vJ#8Q3Bcw2Du%7Vt4LW$d0l$7<100TEG78xL!x1Mu(Z`;O91&Dwm4P^!^mQ#Kl z3sLTOS$usX%MBcfCmk!WN8^rIfPfN!GOgLP({e#!CE_CMk`nIg)?_FFDb^Y8Qr>LF zGJA)Yg&vLLQVvi?&J!A+o!BPVr;d}ddTrda9)OT0fSb|eDnP0yXeP1R&krDt*9my_ zX$%>;P$%P>+*c8>js~D8!Rg2NufSB}pLqdXPRkAe>jGIM+kQz3fP+c(}QwrqSJ_ z;i26RMHgiicZiR&I_A2lV0y0XdtG`-J`Su4eb-7HC2=znqLvVuBwAFTMv2iy1*X*+ z#n_Ae1U|5BfhUKS!HtvOWr~0BX+vouS zNT>4}gB3W9KJ}o$bc-#=!NseLM!)JGbN6aT9_vJi53E3H0?3*46R%T^2GpX?gaDm3 zsvK?WlLbmg16OSkuh2YPaIf`mTJ4%rdB7F$BpEqOY5W&202VF5W=#uxd$c5@pN@il z{uU`cP$}m z36UvpX{WAA_is^wseyr%F_ScG`?MapOY;H`O#raT5LkSbh}0<=HcVH_TmYvT6S4|* zEChfmFUu__tE#jnvivEUnU)JvU@^@k4Grtzu4P6pU<3H{*yq1+ZweltvDQ?!j+MKf`H7bK8FCLMdjr(dW zc~Vv_E?-=;=Asq|nmbq)rdh{@=K*WDS3ruKtkWd`g$}Z_4v1XoTw9vC-IZRi!%)`r z^7~enG~X9RRthKfu9sf;} znVB~z_axs2Wagk$w-#m(+m0YieV>_$#=a7fP69-5FEX~5XT7C<-GGV!2lwZMX3jD@ zHm^xE1_tqG5)cr$05p>tzhjIOFdfwRA8t`xoRgZolb1y`4QNRwJu|Fd)s8i0nQ6(5 zEI~GEh{kJWaml^QavI#r{BUF1OjcfFxAYi06fgnCGja`+-F#JDvw^$Z0U0Zv`vtg*5zCX7 zKXB8O(lz#DZP0yAb%?Q=IG=IasS`u=w9gl$#zoPdoRg!pn1K`-E;29`I7s2LM24|H z*2ZxwEl!G>9ZMp+r@hA`vC=4FKtRAZq2(WFL9A28K=V@6(ee###?9T>cFP~sdieL*t$_; zm`|G3OkMq2kJ=@#g1NB;)_JVWf+bDcY*H5d$wFpLEFA_WPtgG|kdvHfQ{<8czvZS? zzHyD~_AdqOBS7aHptz_Rmn9zSRE@UI$>jFU>VRmBweSpaoYqWK#!eZBo!q@evXq6+ zJ)ASluV#j$mTQc+r!8P@L@sj*u9P8RawWnw&b+>s1zCe`o6%^rP6Mhpy**Zwp;F`_ zVRA*f^LVnTz!YnRb!Y6H!Z;rwin3#$$|)&crp%`+=+zJ)<~4rDEL=8dkWHs|k#SVU2$`HSrFEwK01rSL zixv<^egG2dK3RVWs7}bT>(R_;2~yy!kDv=gaspACEsMKX-~+g@i2-@42HgYPwv4Hc zZS2>~Tv_0%8-N6y*Z~Q!!_Cb2aNiODu23iUPW9nh#v=4dLe^Ojh)!v}EM;;6cRB_D zJ?)@PKniqo>Wm;KWJH_jRG4+Hda+h1)2DT%On@>A7q_x#4jJHU@|EhL12Q3b`dV9) zt;*JHb5%jiU!>oOwy?l}wLqa^LSA!%ZS4TbhPk8ehTGmS)wVJMsDz|JH5oA{L!~I7 zgvk}@F2Knm15*K~#t-vS(qp4iuKLG=2e1-{C6u-?QxrUV=uU2X!a&8|TwY$qWBH)$ zg#tBheF2OGHsezMY{|!3QUa5001(Ooj*Js3F#=bChkVKg+Ifw20z$Yn&2@^Ls8bfR z?WJoIZX%jY^zq%3a&py93XBKpBSn#4dsq*G7H~M-eWMx5nVr#VY z#az{tg)dy<)CpZCbV+t>;E~6Zn%q;;o}>XF&{g3n@!T+H?<5liy zE$f-7TBoX1eWiBjI^IS)2k}5t2_^z#QaGu)1&sVtOO$4bOz(}=S;#9_G@DbHg)CaQ z#cljro9F|;MHxl4r222vqS9S#k6Gjvqu_FmMYVJaO=!hww?wGJg`7X&bPfg^VIlz%jZ`!Lc}fjiRAB0YDp{YUhIm>!kK)r5dx5)F*cQk| zB&xWwm4NGaR-m3*X_nc+b5=%EYi zWOZi}*t;ZvZiSs9?F`yOT7 z?FkYny=AEbQ`V2N#Dg7;ST~BjVCxmxKtRe4oQ-xrV!bBcw2aT>tu@}ZC)~8G4SS*l z4nO?xjyB7ut97)JjCdx2MJ|E+*ROB81*rm4z>ytn28Xmkfo=W9H!=0uHLXQtZD~%+ zLwd9%f^>F3B0swxO=z9qxYjo2*Z|*VqqZ%Vj6f!Vd6B^S1YY7s7%lam8_AvJ$s~|T zpjio|3``@dvtL`z4Grt-9a`9rOP7!CVyTXe_85qc=!49CT91eoo7XNkeQW?A&>hX| z>npm>LMDOvl|bBAJij$JT3hD!ZB$ZOzDxp5NFX*elqxW#3Ak-@+NEb=LRP8%^=ngi z_T!xvxS|lSNQq;xE3drlf`p|yC98Ep8#>rlbg>3PGYgpn8j*l)a^J$Uqff^!im;df zwSAFRr;zlnLW|EGqkx zNuaY5uxC{>%+3AFB6(lFIWGR(YW1QRUJna&ge-NE5kyL>xAjuk2 zT)W+R%cu0YwJAO!*3b7#Bg>NyLDQ$Uy=VoxwiZ>cVJucAFX08wLMDNEkw8)N7Ed}{ ztv}q!)#vZae1fXCqCH_HL#3RD$VN@9(^5`6yXt@h(g32~tdxOiS74lUcw~vq;AN&R z6OahFc-g!`j$cheiZ*q?6%2qflh;4Z7TKxZyzH1sptBMH*0PNe8kcc$5QyZHqZV&L zqF#VF;p{w738V=~X;|vOv_~Iy?$gE%1A~L+;-$xIn>LKot5Z4%ZOzXs4Ic_SlL`|ivl9$F?AViAQ207KCIK^Ah0t)il{Qn zw1Ed^fwQCyND)V>z?9`MxOK-hF3PLIIH|8Y1uUl~aQWs#&seX3Q{VU0Kh~s@HaR|C zSWdIRcR8zmuam&SSf+~;P?LecWT@17P&3Q4mSoZqpGF`>GN}Snn|Q>=3v%bOJs@MK z0IcoJo6pIsqm3GLWZOX2p7KgXX?*>I@zTz|Wa)U6GAI=;%V8#mPuf~ zBv1mePP=)RELjqE?TWs;I=gkP18Ob0odVQky1L0wnJvfin9MpYmq!||N*Is^Zf5h) zGc+{xf!Wce7<%INC6~g|lu}Bk&m1IcygUZ3HPSlGT1J15lt3ddYblRrlIxuq=a0Fzb;(#@|TArjyODQcmu^yHsL8#h$x$i_qJfia47C9vbvR4uz3XsAO_E=;dUd$# zt~&!A({x#;O*SOUo=IRqB~Z}qjF~nEW#wG|?MFTwPCn(7ux;yBS)jYai6@;HUUK1u z;dg)c9bv2D9)0xD@c0vtmlv9K1v8Cv5z*aHm##a5k^@frS_kU9n0Pq{{>u{|` z(oT!6mgaIH?>q(Cpq~Z+3KbUa0qrR8$>g!<0#zi1Pnx%Lp4eOr)f~A0{>J95x7}($ z%EhIZU8WB>42AU%tPgADM*8P}{-^K{pZG-h>CbKmOZEA|KmMb4hciw;-OBy=$Jd3^ z&p17N{_|f5x2#(iKJ|Y;8Sc3A&hYHxjteVSt_=6wb8q;-2mUHNRpEw?=zdysKWTLT z@spnj;{x85E0%}Dk2peB^0x3v(Yo#@*IPY_{AuO>S9@O)VWxt}0=z~Q9AuY^Tmq>C z(`byg$2PGCNb;I+#H1rax7MNZ^%(MiGSk`=Kc^YKGfGda2W>Sa{0H#q(q{auj}`v~ z)Dh3(J16Tw7DkuW*LL+pdkq(XNNF=_pOv9(gjUw}nf#=crS0<-*eIi`cQADK4~K5? z1E16Sde8LasPWRJ`_t7o6uSEcwO?|NZc#quwHA0r>C?1Rv$OZKP}#+4rhWH6@Ict5 zvC%ia@r_~a+O^@v8-E#Yz3sN}`7eIa$Q*poL1E=S`-G8^5xf4t`~NbWeDcZR^2@&! zwr<-RE|wKb+MujSe&jvxyz|1BzWAlEdd=$a(wDw8yzFH!6}>N&vBh?rak?u6q6Zy( zaJcrGYXz=93m3iO72$1feQUB{TE;H*ImkLw_Uz4TOTlyksvxktFZ2xQqvlIihn|69$5MgN zM3~yWML?r9w8JYxuksjRNkg;jXKMG>kRRKnvdco>$a3pUActw~L!2Ak7N&M?5>V}~ z0#YJi0A8-@&1Dtqh2st^0=FqHw;-ePkp)T~3aK3e%D!c5LjUqLp>KGp)sdgjI^P|e zLT=}l(8aw|de87O+Zu94ZOD!82ou|%Qu;=H`pM?U&DWmJdtP2ZC=ap-$jUKu|1Pyb|T4?Xx$_~3^=Se6eU z{nD4eY;h+Xe?mC(%rj>oy!o7&@$3qW37(V>bk6Jmt#00<}u-|^+@h6|Oxa?0R zflLCmC6FpGMX~n^B!`AZ!pO4a_N`LDahEK)ob7HjEr1;iC>6FDBtPp;qYdw8!q}*+ zJSp#V1%OnR^q{s#T(N%`T6K^>Qy^3Tl$?OFTjO7J1O`^D4FgM7Vt!lO0NAlDgCWd1_%6EM*~6ngX< zTywm+!Z+%H!ppef4Wz z9nL!ItnjwCyd_+!F;fq#_ubbzS{okS;8EcbO-`x|Ojz>p$Rm%00}jBIJa-4?(Ky|Q zA2AYr+7~+unFKNk%q@Y`fhks5kCqBxdHPpjICl4R3sBjgH>Z7g<=&LHkR=6tEL`6Q zw=DR*M^~e4-Ufun%=}T(y8FGJLD$- zw&{=;`1UW`Hw-M@M>wlnMkhk9Qvi5!$HtJCg*qdkixg%A-o3*s1>VqGAyCveVMzrr z1(aBdSj4?UOUzmZR4Jby-5Dl!Jgwt)S^O?_iK6+aY)R_-reR#`l} z^x)Dh;0fD2!e@^q{r2ClOimjfdrTgf@o@h6=Y`8IzuZ0qap8+!T$YE6RxRapdUa+sFN??| zkV*;k)_Z{>7vTKy4Zbio3Mh^XOnYR70hWRXV}dONaraIMM0y1P!%LQiktNZl_l%=P z3VZ(eox3JbQDeLUrU8NB;L5cE#Z?A0$Q=N)@y=ya8e<%i^(hdNWe1cR2+8W}n_3b2 zy9A7~SOGuV6^a~kt@a9}Ek9$hIh8?xLbWadS)agec=aIyXt`VkcElT)3g9MpZk08; zEA-)Z9$BHfHBPItlLFChm7Nq1eIqLc{<1JtFP3d?Tzez$+N^VZd_eAI+Le=~$*1pd zO~XsSxv14%(Vz)=?WH`qYpYz*s*k)_yjdudz@|-`!`bJY9S%J3z;MaeE(yD}H0FW} zUSJmJI!!2w2R`<2$=ALX-t?w7g-gEjp90fdShIFbJWF<-Ng$KJ0!g4Qn4<6jRK`!{ zBTyfk)iXv4XfSq)@;2ZR1B@YJnW9v>262VLRWu0+5GG&R0_|W@5ugXC@rgM#N}rAs z*f9ABo&`*R7@%h9YJ`kP13%gbI8nY^9SDL=S^|hQ@dtpc^x)a}tgK&vvVU0Ptm=?h zg{o>MKceb2Nl30``016~oXJACn&Dwau4T>v{XqdSWwC+@UCt-;WN}XHmQ}6$VSM9C zmKDzZnHF!|nxqu9c}TleZ5N-o-?=~OyGLTm!a_Vh4?grz_{c{-65jQ$cZK)9_r1mk z@4$Dz^If@kFAJ+zt+F__&?J26-@hEz-@iVbDvOqI*PVBT_x;)X?3^JF0v;R>43EUG zJ)SxUPIpWeZ#?+**m!jB<7wG>CV@->vn9}5zD{j+2Edd&^8=`C6^JqrV9NS5-pT+H zMI3#CZ9q#07&m1i&IA^atXV*IAr#_lsTE=Xdx1=MKVT&bo3Tgy(^7gf$W_}W<|lFG zMv4ubG0;&tfO1;x%F7)a&6-u5fiG!NkZkTROENDDQ-nw?vcWwwz*ODDGTXLmhS|@G zSDJyd02F=zcAM}dpk{JzpTBvbN(Y zo?I$$-8K}anS@jrN3}xPpP~ewXkgat53af@{K+5xaro?KKT|AsgO;+a({h?DWD>|E zuy7LSjn-?{X27hRIIz{unn((6Q9v^K0JvO_ns~%`D3gt*^P1eE*+xs2F4xkTA)CoN zH7RRI7E^ky9f25Nqc*Wd)&o{I?o+HebMeYDM9f&O`Y+>EX>z$nZejpf5F`LK>lLeA zJLT9?Ac18WI2AC}3Rq^wW<^u9xtB9txbEGp9L|vXLRLN{aA^Y2e1h8Nmv0|GbnPtr+kV&9T2{dGx zwpmBx#j%8>+_6mi`k0+78rG?JU?R!H_Kk9{Zj6@7$i3Rr zzgz8QOjsXKSDCR*8*HP58QTP*yaQpbU5&*~X-pMs7RwYFcI$_$7Vrbi0YpHYknhrR zBh@>#Yn!=rnY~M3V+JcL-T->sxQc*Tp?G`g73x_2#J|T+j1g8W{Q;gH$&P)rCT9a|HPT9K6@y(B$MJGrD zQEf60OHG(SWNSDDG|_meZjahADJzimqW}<_DX{u#(2BLQc(ta{R|USzMhoF*Dgv6B(&dc~V<%pf%u*m z)nv19+@!B&mrvTBm-w5XE-=+S1RN$bOOYx2Sg8Pj;K6_rprx}sHo8m4$YDuiZ2XaN zQ>CXZ(4J|zc_+8Y3f0zsLLd7NU}9C8H7Lt8C&01B(EtFE-H2>#bX1!qXbB1pw>w8U zwUx5g-uUgb!sbrR?FQUrUE<0u09f=uM)IV7#xZTVjbtfeCVSfjDK}<(0-U&Ou|jb@ z`~JHCDE!(iWAVk9s{y6PZ*#Il0eS;}nqp(8;+0v;%sP%50i%0R(7jn?!IjfOvt7wY z+LUFd!{EuP3aotgCZ z6q?1sc*c^{SgB^^mIt+q|Jr+4gRSLBmf+H;NaC_EP8paKR1#m5(e*yX-C8YVqSN9$ zS!MzD^q7Fw(?!?PMPpikYgV>Wi{)q!oq&m*ilrO@Yr2ANl(6{!BEv-#ToI+2WK3o6p*`Bst%+JxeooxNwzScfx#DRwe_HY_R=(3BzrBGE ze@5!z#eB)3B`}?ju}K#af-sd;2{?tGNg;8b$a8-K}rKpRawVj06 zHF2;~cK-@_qMT8>&WkjyxD_hOr}6ecX#PXx{hNOdvKli9%#Q^8SzpL^vMDfig~<*@ zC~H^PNs7yqzsBey5Gd$0=`aIu0|*qow&PPC6dgPVf@&z~KT#e8S zT+OP{dQna2p>DaNHKRz3$qif2>J;6m&S;i#k~f=4Sj=+f=zxkGYzHZgaZ*?GF#y)M zMs8Qq*(u8JA@Qu~jpWT&cuKN#+K47jmuuSO<*3K$*;bFSH1bh)O73cYb=%Lqk!-m? zme1}Du4O8uPL}sXYjvaDpy)6}Wx9KnrqA5b*|8bJx-T<%6=YKh{R~UhHZ|17ij~R6 zftmR%qE?PfHV$ndt0*cZzw@K#OgWS->TIxa1+1kdAnPn-64;w1;O{_PJ(H+_BE#kJ z#~;@!h9(0{@v>QM@;0D9sN1=gj_2Fwmrh92n*6eRxy>BacD_;H&4aq$RP z3cCqWam^sUy8FY-;67nyNIL+ri_nzj%8qH)>%?|#)rTvzdiki%$a-{{&BbR;Hew0> z&^Q7MN1^49lbFVCkO&FfQq7rP(!|AxZ*{6el>~ z7D(DNr)WKP1%7z`bfM>-Ks}}4L`pH8J}a-k)3s&vGYKp%3HbXlr@m>kSzr)hU_i?= zo2BJS)Rb0H6E#bvc7B#=bOV$qKF_9;9s^IT4$`p}qCDMFoa`{f8p^o@XS{2PNv~$8 z_J{^+RdZTKlGBj zt6Nu1Cdw0NKQsxn+iCg{kaKwu$1i@*NOu`RgLLxq^LQ^uJxm1?ctb=1 zVf0)bfgkbFqqG?V;(}!gzakOIOD7!(Fq+ zV9%8R?`B7X>8vjM#&${bHBh4~Srlqq!8TB|sVtQOFqLHrGZIXcj-o`@N|B<{3cymY zB6#L|o9vo|V#i{FMwjr=6@S(8s(VJ|X7rh{E`29-TEN(*73f$EViC#*iVMxAyX^hon~2gNwBLC4F-Ya-4;k|Q+YbSvwH4s<NvY6lVt(d*(eboobVo%rkZc7k?BGm}6jfwm=3Fg{wcOyde9e%>}ya}oeN zXDb(?3{0z}Rgz?!6=jc7=Lo%M8BPfRD$Ce4)`~VU$g@LHzrI91xXdh5HiO4HwJ}t5 zdzC;vUMI<%Cmm4kuHc<^TvoNx<9WOsM~|nRgRrN28rPL{NsFg@9*))0h_4&!-P5XR zpkZ_)qU6fwrQ({)2z4ViEV&|&QX<(#@to5ZKi1{Ht;E@Uv`P+shxC;tHbTilCV@-> z2}+=#hs;oGmTaZ6Y_|MD=BM4!52cT55Z7lZt|o8FW8?Sb;T9oF->ZiD_~vIN%S#k^1JuIJ3{~8@4qX&{ADi-Z++X_!y}JAIv2?- zJ(EBtffgj-ezF#dF3uvTSl59DqzyGArkJrzD1$`8cU6yGW;q7mGTR7&JW5OzZ4E(jPeNFh6fBvVipT2K8S9rzCULLOc z{*__hefKS9{o9XzBz*c)|5&_!^wACa*6AZ**Y0S)qyUy`G~h3d36@^qI0}*uclM_P zY!<+?Xo*r4vz7#|Hu0!-y?`f{sk_`VFzvwYS;Qm{*nfZH{UmwnU5 zI3HO5fL$MP_%p-rz2hC>gcD8(Teofvd?5VuU-)8p*-Kv<-uAY)TAt5*<}=}{A6yj< zJn+D9(n%*-+&8}dweaDOd^8+*zyaa6fBQ`W>;I)PzZQP`O}`y(yz$0x-g)Qh9qS4| z_`wgtCqDUqt<2J;OT!<(``zL6(@(SOA76J}IOFuwRo@rFcfWUKIOgc1!|%&&{H!C7 zv@zIgfB3`jFaPf|;ywDFtrg1tWD?j5C6GEWr8nHINj>zgyJh{cwlDflslZFXL>i0b zNpq}?+^E_}LK`V`9IT4q>26+(`#7-Aeu_HI)9fm%H_+$-+c6=b`GE&al{efjc<5^KK3vcesSY3!Y^;SDLnW5 z^TU_F{N-@-&9{V=D_4XA4%lDo-Uh?J{@cHqd-vJL9T)c5XP=1P%9UZ+vZdj$XC7vC zoOkZI;T11`dAQ<=Z-)mSd@%fN^^2sNK%fI!lux;zMaPh^j3nL>VMh^mi{%7yg zZ2120@sIyQI9kBUSnS<*-5svF<{G1){mCS-cS)e6`%?v`tTE-I-vh%VVNf3k=ewo= z5uaTf*9WTs)qW`nJ|0e*yP;USIktOO7#q_^xY?N~GE)=0r?Nb2x_L{&Ot8X?R7xd) zXC%4KQCFClm<)gT?mr4|dGnjY3tsSoaN&g)8htFskNnNwh7(UZ(SQ)w?~OP9(td#F z3txCa_{t@hgmt&9GcdjW`s>4n$D#>K7eD=U7?GP83$|AH(1-rofcB=Fei=S1tM<$@ z&j?RG^^~mI6T_8PUKu|9PycL1AAIPc@SzX>RgnyqE3<-k>3#dx&woCA`9J<6?A*1h zn4Xw#3&32|{=f@)ai}%0Zc2U8K ze9fz070x>Atnk*iyg6*y9F24S&fDJ}_<1<=&_n&SrK8X4edyswtUNP{S8Eaz9n?oR zJXWOnXvOT`Pkrjs;q7mKTllLFe87e-)~&lW{N3OGeRx^`o`p;Td$R;m1*Uc*w1pfK zcBTZJd<(Qs8!Ye}9Ev9K0IU<^`ZSpG4M?E@xGW98?K>}_1vl@!@Re24wgeinIE#78 zydMDMY>;e(q0>)2HC*({i^69=`#FKwhH%r(H;3D9zdc;}-S33`_uD_*amO7-<~{Fy zui~TgWwJW+W%aXux%^e`t+;N4mkp0SCJ)efIA3F_Oj^R@^Wqo3sI2J!$YTDH#!(N_ z*z5(eq;XHb;q|W%pZv$t?BTKk*+pv-kZx=3iE6F7#ZoGo8dH0Y%KW#Z#7p@bIRBiK z)o3nVfn|^jX<}krAle_6E?q7l9Vse{3Ava6Q^thAPb}7)EK{sgzJXe5S{dm!FGMwS z^R_9&J+F8Y&)TNVo5MM0pKTL|zIMsiZ21Zxg@w3o-8!4)J2o~J{_qd}F#N}V{%05( z91L%L>s!K|cit7=^QV7m6OxeZ>ovA{#kc<}{OX>2!WK<9(X5s?xwsN9sRPrzUSP&Xd-K{uS8iNp8)~ku0i{{20$XHh zQ}aW#-|d9lyOE2R*|kNHCMPfYh?$!=L3yT=RO{oRtv_>&Nq$5#Zr}B;cZT=A_dO;U zmQj7@yWb6$UUr#Thwp#?{|WC@`X9af4-I7Ryz|cR51;&m<;7CG=bn2thWa9#nD zhP&>%E4)vj%{#|LsZW3U(>8N)JvEWaY{YW4Gy#MiB1Jr-SE)e5v3M}4?$2Zn~j(q${OBg_)L z$Xzm4cZMC?w#Z@~v+>ep%U4=D)$Q7`O}*r;k!#r8u+^EMx6i)3OknEPmtUr}w=VPy zh^1(11~xb!SQV!Cc~+QOd03bpSQ5G?c7?vJ4~Fh7_v*_rn+4*Gon~RNOJJ^N+tWER zX1{dF(s1f&r-eHOI_-x2_FHRn0G`tLV|+&=r_27Iyo=Sg1*CI1!|;fH-Flr z5$%a!D;-u}#kVQEtaxG)u;;MBXOLaXF4Yn=HdslOWlC3=ZT|WNb|V7OK>;d2id%PV zbho*4$Z`*a|Ixd{XFl`)iba2R!wun9 zE!T+$ck#yK+C49dMelsIP!w^4imn=shT;nKSLdQ33R$K_ECIXkHJ-uBX2r9m3QXB& zlyTBwO|TeQx=f%sU}K>yjbX_PFB1TYb=j{p0%N5t7`Kl)cWA`a{9T!w*W{i3nJs~4 zJ8{*3w-&(8uB|!Xu2w!C=OF5FS8KiK9hv}j)KN!S)DurU5$?VJ{tk9jz2>=(MR1F2 zFcmY9Z53cet4Mcl7P1_f1S%v@dU{d?ri@Dhl*6nu9h6lnw=9#2Y&NglSGJ3g#(H`+ zjh{4T_D*V|QeNXh%__7Ci8Owh&E{=fR(nZKyeI93Y5?mr0or=$p0CkkC4g3zdqV3? z`7M@U(vRJPy5O3D=VH07i-XTtCoWZBYD!5UNicBKI40w#j0Z9KsCRN)u3k-05}^7F z-ih%secZc~E?(m%bMx9=$^Oi@1o*TnU{UxE`kdA4Lr68lTp(Jr;37+?1lL8ze#Y)% zl0d8zmntyDRm=L)U79t>#0Dk|0sL5}jE&|LmzOm<&Q3=<+L}{MAc|bPoxB5jJu^3N zCk4F7X`3ySSQ}=2o4g6i)yo>ZG(jcHy5JJXEa=2%@;|@;06+jqL_t(;L0wNp+qIP5 z{d9q;kkj0@vNT6WwGo5*B`xmWX4BnPmij%w<_bVf@|W~TJo+eAcX zG(!K9veSAw4J4JWJ9ZxmgjIhwo438NrNa{R03%fSD(KOZ0pM>(bNn) z6BTE(#F7Kp2%D%sRbc7@DS>2(QBU(@DUZi=@+i?~Zr)A`c(0?~2>?Zi)j+gS;rgWAvZ{1Uu=Gsf|eRDc0?veoLp1-dX=8a81n-hGAg;Djb}&C$5W_K@AlQJ*3vOR zG|Tvqht~XL1#8O1j%k5fTb;DP5W-`DK*<+Im6LcLN@cJ5o*)7eA##q(Gx5ETGjX0* zLz$~CWL5_RDnPDh2h(qn>NSLC0JOAxREPBUY#_7Efk>032Kxs8`Eri)`)GF`UzUi- zwwd0KyECn({oxxSh&+@TlB1$J%kzwGk~SMpNtnoIYx)=PNRD;Oo9<_pAV ze2U*KiOmTvPWh1Rn6sit!tis4dZ9sage4Ewh89l6ORPYe9G*kXa47Cq>M9KYR^?VL zr03|dwwz5m*t77xNt6cG_8TW%oo9?I$?3fOGeZMD84qn^))`p^ z$NFWx4oNyG;=v6qIioUl(WQt#QKlG3+aOB;*vQp|R(IT)Wn=J}fX!e)JLI?lp4Y~= zPPl-WasJgow7P6@0b_}5W`Nr0Pjpv+Lr2VyE+FO`e93Ei%dFj8W|EH=H-A?HsZzHJ z2tmO^ji0qD4eg~iJ`GAzo%XSfZ^5tPW4Uz6&dPdN=ZgFsAbPigK#01&-^2@Y^y=o1ccSb)Gg1(aKVSl7A z(}~ypfpB*lwud2!8<+bHd%Vmw)8P~(NT{w|CsXAP^pO?~AMnN@m;kfO`0~s}i#?G{ zbJ4;tCe@W$ZsU_2h&qvOAGawnTNHo?kRfB19Rac%lHLNzDhaF5ZUA%#%QfZ^liKw* z36p?$AfG4Z*L9$Ak$yUjXO57Y8p+*6FijUui#E)apjec82vxzea2jZjDFdQT?GsOW ze;)MF=xdHae6Up1L}66<-6;>(TGm5?+rXjY)dMmM#y;_a`1H{1NvuT+Uuip$z~utq z^F1e>hX(7stQ`wCHoqL`_U0(C3#eQxZp`1(5oAAS@)r`elTA`4e3eOjjeNCo(~G_b zA8Zz=YHy-U`ZFjo)aJZKRaovgnvL-Bkjx%!U1d6~Yn9q8(~eTj%)t7x%d_0ZdFNoG zxGI{l&`x*pQXO1g+yEU#SyjM@r+!J5EuMlvee-*@+Dyvx;cSov7azS$`GFFMfr-=Q ziX?6ARM6op5)SfT0Qmg-2mv39fK&h++NCO!AO_E_j5biBUil|$s~DQ6>_81{d>0() zZq7Q&U%&=gf65yqejtV_!Yt{DcKYcAq^K{XOmitlAyxnfiGW@>&a^&1-7n!WWr>CU z+eYsUN--0_4n<<4U(>>6Tsn)B1`p%K97Gm~lm?$On2V0!&8Pmd#!O^{WazPm^?!4SxWI#}341CC<% z0CrqqPEG6NaTzj|U@&eTdS|{&^rK&OTg~>9!5+W8)o4mfK;e`%BY}A6#wa>r3?#e+SySIpuc!MITx2{>2x4%}2y^ECYys zU({G#2MzvdJ(buU(rIA8$6U(C62EOs104Dz+GsDVVtB$(up9a{0#=Uw3 z$_DU+CSPYWyP;V4S`wDNA!W;sqGUuB0`_g~?$V-C?E%Lg*6{qR_j1DYQuw+ECN@%C zp$Zg~(!-lI#bvFNw#?H0Y`&8bWkY!Wr>LlB(~A7&Bw-#;D<{E@Mo!;u=C+eUJ0B3D zydp!t#-46qC%CIOck{gg438(GSJkIw@3YLeqlGqppStO!4^sKa|6MV9Sj-B(e(ulG z8{9P#y(f!!2Cw@1-8^(AmaUua9OZLkWrtsEF0$UgP6<|*1AtgsEfcQU-NC#~!pJ@X z>t6#kTyY-Xk;H9ifeMeFhcA~rDUrw+f|x9RZzlgEy)~RZJECcB46Yfer|0m>pch{&1Ix)PeEos`ypq!$9@rh}sTi_oc*T(JVLM#|E zADQZc40_0vzUu4dG`BKa4tHGvem!r#C-pwvzW6lAy+7eB;>9}L8;!8|iCWxh%J7^^ z+QBY_gh(v@_UuNyTq7^?L4seQavgWGKftqEp}Qj2&Fk&tMsyD)QEXnray`+Nt*j6C zodNtpn=ukSNT}^S#)!nh@1t6XtXoX3lhe6N*LU~uh zYCvnP`E0Q|_C!Wn$x4|{I?Atg8oX@jU8G1u{ZLrssBHfmP$UI6BbbtR_)_bq75rBt z^w(1k@!c!fag`n*P0QDC)zLm*yy9U~YWyz+2!#G(%}bzbjTT+IX&WOOMH3LS>t+&5 zqZ~+*9TvP}mi~$G7{?VW6Ss?CEl5mc%2#OqxM;)nY_rXz{!0)|8xZZG4M(zV15fk) zWvlsUytM6KvZJFX;Zx)l(jf6uyUDVys<~*^+T)XRO*_i887(H}shpFSy{_tYi zby@+q_O+vKwHFDf4xi>B={u}{2l;o?&76J5T`~zPny;Xz(OGwO1?2&2fv4-8Ps)6^ z`Gma#MZDf;I)k5Khqjf?AKNxP0_c%YQO0_zZ3Ro(H=$B7W|S(VV9;?CSmWLHesSO{ zS~dT;ouqcYnS3NCKEofpOl6U=Pg|krKKR#JZMlxQ*OyhbP+r88g8+|57Q9?+x123B zjB4Ev?*39=Enh-NA@QwzkEN8XFV6yTh08ikKJ8Z@;vQT!F(muu&D_?%zka=Z_Ud=p zbJ;bx^&avRZ~i!76QaPZaRT}swavK!C}MqhbdbM%m@hXd0esiCjZFaV?=P&HE+wf) z22tlhpEwO}f$X0yf^Y?f#3H#c`NhJxtWZ@a(0}Cs#XQOP(L6s=(bBiaW_jMHOGTG% zO?$adSTHBj@lgUbpkcO2cQM$;+x8|KM4L5r{o7y3ov11Ma+HQ5JiLFnu<seCD5bCTYx(s=dbbV~s?d?fo)5};uMqvZW|wcC6IiUutfrbDbmUTxSH4sx zMoG6kB6}HptnwGIJ^~mRElzS{fV1iDugK>YtfQG#m=_#?Him?8Hy8&^%U$w@|L}w* zTY^QW0H9*e-+PF{W*eKeV9p5LaD7fMr~Mi;!hazy8!bLnv3tx`bCt|zZIPuT_14b^ zwb;ek4^G?fIJzUm9Nay?N*#p& z;M3VDJg#UBhEQp2?Mu0oqX?522MIf!0#7DPUi^F;)+|xK`+WfLwCn<@-&lRv;e~r{ zviZL@e7>7s;lF!=H?B#+{OCP- zG0ZMW^>ZoCY))2!iHrwp+IZhT^X#B53J*!(`cC%VLY9@Z154K z$dF-}Uhi?&4&l-tU;i(957y1EMlH^vnlTx%18x~vsy^-xu^Ru*27Yod@6xw8{{Z}c zBY{?|e1RJ;KMwo_U|wzl&{^xdd|*4%wiEi%hx#EONRrSE3~rGgG|f{Sr?OfL69CGK zy_g94Xgu<`mFf(59Vj3ZMEb08jv9R`^*E8=UT0q615c#C?0qlxg32?SwIT<|0w9I1 zm}CoY1B6m6Nj;oYQ){m%+aDhu$JIF7M>%%-ivy7p`kCi}N;jwKIW zTYml%Pb(-mVpM}<_lD=Jq z-njWU6-GL((MJmPt-mFE^ZPq#WorszH~p5<77`tB9#>;JZ^ex0PyOY(LuN^|c@j%} z7v<5`>)9b;mQM4S)>&VN=Xdjd9kOy=w7wBxWO>rJStBiqgnQZf zOZrNO-$kSaV5zSpdAAghco?M4l48#5+!mu1Y=r-o)h)3iSr>R+5aLz&*>v)dMG4b} zxNlw`_-!ot2xNg>i@*m6eg6E>oM#Z@6a}90FnFRF+!OjMRgZQcMuvAmTkZoU)qGQ8+ z^Gq?z<7YXGlHg_Ck6oCDk88wWbC%Hy&Md$R;L>vq*SqLN(siM%6r}`3e}t&R+@VL*AE z4cD+obbY%)mL?K9w9HF0x*{=yeqK5>!fcR` z1&r0cs{g`C3$+HYoHZ%qYj0#P)=ssq*bOs3n)-5i^;R5JN~4nPE%nq&cRJNxaKUCgh$N{QR$wkM3JlUG}f^zq|ebvdn{B7pez zCOm9xDXqiRP0D>P3`Zf_BOENnXs&v4IY5subT96bjlG1v9wH@!WaVZdgF3q)*LCPj z#WDxLUhAii<}DJTyNEfUV>(E51~3Zusf0);&8mHYuW)((h1xptuxT=YQUk48uXAd* zDVjwyi_Wc?F#DTt?RcJ-iQ*tC2ki07<>#dz)L)+*Z@i*=tmV6fG#&pzQ^hRYA(&;B z=?Z1*K0W8qOWJ0apIaBVAfa33y9E|5iLB99DumVPu$* zvVh?IM9VyLfjEfKWE1h8Q%1?_e{}fv6;U<-iX!|#``z2<`IH$ij9eKRXGlI_fW$O< zVXQ~AdlT6VW>;t(9aYyzZ~8Q^V)fU-`YR%Qs!3h85*%(~Q85u1} z=Y`0B0sVpaI4mP+>lh0{RenfN=y;qJpE-2s?2xU3_fbGalIUKNffB#}&ERGYR1Ykv zF8A|DDehI2_C>_`c_Bd&sZn0Qw0|JW+Hgm)cBS?|wHn<;tT0%tfQ>=j+K&B$`*(iQ zJT#Kh^K#tW<-jeFYxOdu-y3h5VRi3T#@kS=gm7+g+`N=SfjBu;PD|5jp?wte_t7JX zb=Kn4cV7M2;L02hpN-;#QiMg4I73x_E6E|{d@BugkPXAXP5#NXK7SC&EJ#ag_R{kk zxVmwsg14ny#G>+AY=11@N^S3+Jz!%Pu_&p}6h)nwGuL7aU3@kAyB}sj^^r{z);0zf z6m=YXS60vvJnwDhtJiKr1nBc@F`C!w*r3&|2>GXyB`8&|3Li)yD$vZXmn&1eeI<7I zq9`Z-78Me*pi;QE$RON^jb2fLGA-?C=Ta0VT_X#AH`~OpHuCYB@b0r>ct5#Lo?lMZ znJRjho1tO+7?=W9c;u*pA|vo+gI1?3dn2DC+UWjsrk`#qhPj$5_u0${V!&A=QT9#% zw`?Nw9^o$IQl)(HZ?sdD8oM86TEZKvC6ib$XgQo}z<{a-P`Tf=`N)=4Z^}#r+TzqQ z%WJhK^mQoqOH?o`YInXTz{gtU+n!IMZAnRM9Z=5gR!JtZ){?cV-F48*nK|pqjr4u^ zNVbUaf-mShK@~~DHv8C3WW&K>ALsj=|FHWPJQ`erqF4N_%}8D3o1-CjQ|J_)J+VXR zq3WXI&6wQGE}XgI`)>JES#Ba-mH*>i;;45wvb`D9uVrwX5UZqo#U}6nYA)0r{R%E7-Sl3IXDdh6_OgbuE-H4-Y@DR)%)*xh(!|N*Lodz! z_F4VOQP9qg_(gf*PlR-voSe{ZD_L@JAKv(7#n-(JXyV~kx^aIClZ%Zf zQFP@#)fKT*yMKX11ByRqc7Ol?{}|w%cXPxb0t>IT1l9 zff1V`ZmFY3{8pMhNYm5_gHPUG%P*urq7F9vg=*aLBkrH-A%6P?G6m*Pf7G}_P(AH@ z?Z+?Q+?~2r>ushg70Ff+G$aYnv5BgZY;Uoco7_^B1+RW^kj3!02Q9hP-Mo-Cs|R~j z#6Bs}B}>K$0+_J|K%J)gL_gS5zLDj=omRzr>*drF@y8SVZVsdC^=*niS_Szg3ww%h zy@GNXnjG86pFJR)gj-0+&ekQRk6S?CADHbzgW3&0=0h)pb z=DyDGOwR!Ir_c9RZD#^ZdL(~;qdoYAAKSnCaQ)`p;v4I{Mgm&<#_7)nV$-fa|8>&8 z{jUo11QTvAys`IqpJQD&z;$=Zi$u^Vmh<`+A4Dp#WVZH~r;-=);y8FKhw}i*zHexu zbT;o&Zgp<|&4DT}#14D8`M02UVUsE)8tfQm1WHjDLtwc`O=g@v zaL^lQOrd;ha!)H>RbzIiEZ%{-B#=AhGdrI1WLWAFuGpRbF?W&q*js;GLN|x=Gak3O z26Gb{2d(`D$p>Km%(n4SwyeH4sD5Tq9$9$_sgr7@sHSHVq&+KF)sOwJ^1@>yqW%He#nPN(0Fm_v=m8KywDl}41syFIvI7Y4ZQ3a?4 z7G~}y7z{IbG{Q9(4Qjm0dvkaUYh$D?>y_4GW8B1c6RKb`LHCZL_5p`KBTPO>igjp zcKVCWgFo54Qs~P3B5f5BhTEuRA6=&3)(A-K5&pnM-VeFXrHMG9eo0ZuE#?}h$6+!$ zG1E2Fv!4kZeY^bh=Z=4kAWpBbL71*=K3`D*k_t)(nJU-cJ6gE^V4|!Uax9Q0;^F;U z0eaC5^j=km70)x`3GIXIJN?lFFwE{u==3tePc*27$Tq{XCh(4qNZI_t2RZ1wMw2~L zrm9oT6p$T>&7a+a;X75>B!`L}T`n#rg+78}-tX7Y zKJhk~Jz7F%(A$nHSUq^ns#7)584s(~PbwYVD1cCk2n29q+ks`h3_7dtlXJ$k=5A&! zA||jTh9?aykV?~owHN8J&?FwgXZs!A(zs$alxzS_6|p;MMoF@9=czR^aQ1S2JlslJ zGk{(C0Q|L0PI0m~V?5aRik^nJoxdH9j@+)}b`>#*c(+rW$LUx6H!WLznkECrPc$F2 zNG#IKk4q&mP)EW@L4sr0WM7QC+DVL!!k2sH5w<3dr;whFFBaC6%_+c1ZGD-Ie8d|$ z9zv-LFmPYvCqaxC)!dTCxi6UpAD%Ir#O}B0J)=9d18X*Zbk@0D*9TQ!Bz}qtDBhm3eHy73LFGWOb;7hn@%u2u~zowtTF5i1pXBQ2z zhgODM_jLu<^2aZZEUwE#l`z$cq;O(HTx`pIm_S$;`LGwscaNU7f?Y%rSDGD1gvthg zwnxd!eprK`3Y?fgsa2DyQGKkG-w zt^iy1f;O@V)VVpkz$4P*Djuz0cE8RbSX3gsipf^hOvPWxx52YIaCFxbK+G3yYiP9Lj3-qdV6_}F>+T>PbzZ4 zU&2ACP4h&?gtZ}M?9!e_`utjVF_&T;@h z3}k5rGAACK_vkO-v~~JSV2CmctCayr{H9QH@oFabAElI9>46A(y=qxb4WaPDJ*duu zt52od+D0@WMDiRM^`+%pD%B8A-+sq{AuEIn#?+y^-ZN@FVzXMa*zcQwm?<^WbLuHF*EQbA(I&@)E@wIC&Vqr}5L~ zOq97>33Adg*Ktw6R^Q>2J5rwKI2};^y`eV9gPtmz!63`jETKV>tMi^)eC>6_JF%`W z-m{8uQ}KL6GSW=VF|wIcBYnfiVbHx=fX3=-MHl7|+q5611>?IY>4g%&gn6a11aI_j zB0Q%6`86xEza^T}xf><5DX;wVd0(i~w65E79_CO0kki zlK_|Qo}B17KZX@Rg z;R1g&y1br!zRq&*yiMy|^c(c>m1=Z1E5BcQK3qM1I)glJZk8o-G~FJE-rHP12JGLb zttvfTD7hQmigrvr*0VjyT9Mo;HS(bM|yXoStdIlsOd1L4N2H$@JN zfuwO_m5n?g#rTy4=`AW%wbpXZ=-K8Z$7JflI5_WQvZj1!>9oS;v`H<+O?{+PbM)Z8 z`ul&bRdJqHLFb1cU`g*AAs0~o^4ZBviod;DX*C)!zeI~7s!X_Mfst6uBv>~op88FL zLB-$JJ7arY(W9ajPO3ET*SItCY0w$8nd3oKG-qZd4GRS|KH7%ALyAWJyO%|tCb!U% zQPYc;kjv9obvlsq26Gu9|C>gJaC8B}h&rHS&#O1Ts-yY~1RrM+uWl6RH=}KHN3&wT zAx3G`Huvhdr|9X74JZzh(IA zCi~u;u76iR9K@H!u~wEfP_^+;fx(L(;&Ay$itz$bRj0KIeS#a=v(u=$a2+SSqGss= z01mMi3QCArE!6rUiQSf_riC)~PJ3zc70^3YQA|O~Z|-wUSvxH$)@p|#g(;w0iPvvooEd3mXFEO+fh_AKy_HY5~h~JjaBaWr;}X z_$h3K4UD_y;lOIy!hiSL_Vkq9I2NqvzFi{*tXELTYHpqbA5kjV%EmQ{aa}|E@CW8PWTO*ngDjw|{>SRwT;q73_w(a*ni0(| zrNwyu9u&Io_&De2wtDTiI{yf*eAK-*>bx~VcIqFOuBWTVZK5k9TZrLdsw+2i57dswIv5A>B-?GB16qLmYtmj`zc7UOFDi%RwdmmOsuEFZK2 zsVkG9x!;3C-c~f zUWWpMuW8B$b5|Ybs3*)Kb+yoV4uNfvWwt1&MM{v;WJiXtn?N0RP0e^8D&6ROTGp(wu-0(C1tX(`{4{G($ zE&KA1#C1Fi@%pI!dS%pS)<@^?LG@a$^Fj{xU+w?+`400r`yS7Wdq)XfY<8XIp__(F z&V>PWjDpv}RVh-2Trw_xhB!Puv;M%phdFN054goDp={=J=AAq>tNHZy&!AV|`9d zCx7F&p!@v!#KaUKI*5FzBT!Ondu9(iPVs+XgHg#TKx{SFqe_VsdMb3P($T zU*zXIdE8C(rhiBOx>O=9bI%-_$wkv2i{^O)T*G%^v(BifOY2U6bs1V(tBqk{WIvMi z0`KC_R$4AK4m_}Wr2@WR|J3CSzNdDn+ph#AGTRC zKyv2|K^I7svE9~Zmb8$kb)~28*G$hlt95yApQntTMk9Z-qvnV{#fe(WQHVZmimvz} zTObafc9Gj2C!bpH-P4yB8m+sqdXpBAci(UH(0@!}^gJN5yl$k?R%?ed=soUu8@oK) zB9nY9+&NIukls6Ega27kjFHx!ku&|4_~E}30H$flZ+yL`e<^<{3%LB0@$h8G6YvYt zkr%$2J?JbbFmfLNjGhbWcYGX@v2^aAddZ(sXsxiu#nuc^Otj+nr(>gt1u+278#WxM z0=Pedh~y4+nFaGq2<*-wY^&yr^2^Yld9Qg=!(6N8gG__z@@E5EpIcK{v}tk|L$96E zuhUpm6$&BUlbY#YPl`-X2fDnQR!Iww&+3yBnJJZKT6aD06M2~ce+UK)#hWTWWY#cuCoK55Yz^!40lwT2BYaTWNgtv9(C` zp*7&%aS6fpd>8=pTQz!!fIXj*K7F|s^?$zhc)o4FR_sS@0)NBpd^+r0@!OklqE9P3uul1!eXN#d2B>%^waxtipm@A6=G}wnm9(yKieaAd%Mjx`A zk0Mz)z&&|bbT}Dr^a$loUb`FLbU1$UfnA0^8+Gl8qp1l`S--3oF9wwv>hV+AwkaJ{ zRs8G3__GPVGLw4z_FdI!d3HaNa{cB%hXq5nl$^AG^@5<-&{gc~r42uw1YAV7IQk>f zU`e5X2A-r8C(le5#$=uJ^}spARNZOBVk_;fCvX?+Ano0Ll8%erRM=wV2K7jv+hgy{ z>#PiaR%wa5c*t12&495zpLZVkUs(pY*UZV&%Se-+JBL{ts#VvAc)f*v#@M^TMjJ>5 z4`$W@2e-E_#?JQmol64eb!Ny#s`o0N*Pfl*;#z(uTYIWl#w@fZp|$?^+lOa5KOz(M zDqFlbS!?mD6(MsoH_c=X=Z18Rxd=us*0_dqswY>d`JQ^J7L__tMrk{Z8;$R&_;nt? zr+H1GS7k%ImNZL7KV6ZR+c;>3s@SliwxcIkUdA8{P4OC@1m6?y?}yd-p`^(8yJ@xS zro3d6trpXGh*9oN4ZByXS&{d>pDT~9tG?@AS!f$7EF|r~afqI$+Wb`sQMPNG=_h3~_}J?Q_J}^% z*1^T@0UAr-G%&k;qXRkNvp){+(s$F()D~z)#@z1TTKc$7h{OgBnH;%-N-I}?{$;|@ zXrCO_E$WSBX&5;n^1+Bts-K834xj;B@Lhm)inX*l*3abwI@8f)gUeBsFtRiE9=-;a zXqi|z8~2)a7$C!|<(04*m3j$^bJh|c!DU~kk~B!a01Qk%<=z3=e^g%;q>7%#3cf-+ z-l04Mx>LZ>p~#ZF&TrCVxgj9McUIH zsaoL1NH99%6hz`>$}q7KdecZh1ws%+_l9pV{aB z#hC*3^c6F$L)S5lcW?OJCz{>amfjhattbxux6jrn(?v3gpzftn`K5-GxV=&j?fpYf z1BM&ov>>f+uDOA+IWL_y2c}CQ>WVMEYUFHefxI}yj~y8u;6u!TvL`lr+E>-=jg1I~ zYVajb@Mxu9!Uqr;C--!7PvcT+j>x<5{w1S;fvtB{`&=i<-|x@fE%9b&TDI)3#uX`X zX8U9)gsoh+1TgGhTEGfoML1Q=4!1FgDz#$=6m?1xe0?dHW zJmnxAL=w}N&NfsfR@sk2Z1Fx;0&dn}@siI7IDBV=Vi**IIx}s(GEci3twrKGWviq_ zEgB^<-tn;hqEcU9*;NKVRIIXwY@gUGAULOsX-7ywB%y0oy6dBMNXDWn=f5PoFsw4*v$eJ(= z?Bn+C5lz`N1xnfIIPl?4z)cNHp6+!2Z`ah6siNpzmEO38wqLjPckf>3Q=32Ful6n~ zU6Z8TZRZ;XQ-`VGE_x#rCMG5r6dHNlizjL&_Kz*Om|HC)8Q0p&ydJ)0zq*osC|K;?V9yM& zEr^g~^78w5Sfay;liuK=njrzDSxVCSu3A;yj6BeVulG)G<(1?HaS{7zJbP(N5D)Xl zmV*A^hOpyJ)kKD@|NRwbg45UGv$M0uqK`$DjgDXE$rcDP3kzKWx4yiw@Z;cF%E9p( zCYqj81={Fc;8^`s$QBeN?9^`1a*E&AnN%$Df{c7WP|I#Pba9z)TpIf=@l8`RvG(&@ zitR#V)>?ulqC#N&7iB6Gk7mnz0b`20D)SS&n|`*n0oCi6G6Q5#0$Ul!lIDlM=yCwt zHJ}^Ok`v|x9<3a^clhvAf3mNHX_wXyf5Oc_VWn+R@lYW1a~S$)z(FBFvh3RCTi{JzhMuNt12E!ZAd&(W@DSZOFAP5X@wm(7 zGiH7Eu|(T`b5gN$-O9)IJ$mzshhoagB|6S~kbiF|o_=K`gJDC-_)*twTbNefuj+(S zx8FDim}@qRUi!B6W=#F@r^M1?s2h9fbRqDvH0qstR{D2367{1e$4ibL9>>cL)8b)# zLmC2|QpbDse8MpCtvEd|(hQR451u#k?2h2)ZjVA$FNstoPB+p{~@rL_J0V+Df~7X}JD)MHFDi|Nq&1dO%IwW>ua`!L#M45T(n7$C>{I9=o65!XR zPV!GfuSHikRmFMgr%`n&6y<2su;Y_UoEpbJk`I39cRqDVU+M^#W&iXLmqu^S&$djU z=T5`gw3z0w3JU|{$AzQ8;d&$o4m{)a9upjK8)BiN550L;cVmf&g6k_K1)RvKL`!Z( zlRQY?85`n(IW-TtOd59N0@qqag+z)(PqmC8{%>jYv2&6OlFLudb0;Kw$N~AkrEkiE z0sVLXJ#b%)LpTpT1{IQ{ zNBrG)jXz%J-ui|U`NAcPTr@k%&jMiF)p^<_pjqZ{NsFOf2kgh0b}X!!?ACMtS@G*n z?YV;OO1WrL7mECRdP$6T3l0h;wo-o^JUkE0_B|jEy)gQge><({y8g)#C}QxT6Z>|B^_mB{h3k^LltDN-p9x-f(ZCETr_ z>6Q*W{Y{hfaQ1n5NGi8;!faS4J{P-wdc21PVd4#76n9y&X0hw1LM$RE7owvx+JhW8otG8vN3c=Ovi3<_YpgBx zS{DsyG9rtOT6+)eJTr!8amTDGDKpr60zUhWhZ>2s*!Rwh--YpZAJG*|gE+R8=|@_# zIH+GGs5P-!vlRS6)Rx56ooUho{}9TwdF~vfDNgf~W)#g|98cy+l?!W7wv@nZ&7u*3 zp_iAJb8~g*`^%%&L7U`gntIh{hI;Y5nHLhCyZ?;(2F)?@&)>N1-fEe1!xJXdV2 zy3>pj)jzoMKy{B^J#n9Rp_~IbaMXK+LW93K6q;`Ku_?DUVcx0*PH8K14Z+Yu@LP_$vPHgU>7UTmIM;_A z7v|mag5cxw`T_^KTbf^cn_zf_9&hfd!i~I#kJnoh#C{!TvA_teeI=ycvZzSkYa@G^ z-Lco-FZ^ap>e0ao6Km*a=b%~38DNi>beG~3b%cJ&3)644wY?oZa$b)K8)h?7ofO!W zmo9thD){&%YnHY%^St=T+tAx#i{as&R*RZ%CZp8UrOX+TH~e`~XmUqT*YaRtZCi4L zKdEIA3OmWPKpj&Y38FtEMa^yer#OX4Js);zmWnEZ@o-90HT_)75t1EPe3i#Gd4>JK zHe1^dEO^p34HXJlLvND2kC<|7IW!JJX*?g4tPedez5n)<@BX3c|zPYmHLR_q)T$?WWO`HPgqO*1S7@6#@C>(#m5;wSb}f~-Xe zbgWEP*xcR`)GtE+xOrpA9eMQ}a-YUdw~^86r$x@bp*3i~w}+jBA!zbRt?h7n6MoIh z@i^`I{U!|X$!*)kXZ2zgvh63NmdXTCD20B;uau#mtgL(^Yv)jlSCINS9{;PP! zHvzJD$eHRq6Bp)ak>vnl7FyjrbxL4W4K7pX1}58{EIsZ-i2#)!bwgEw&)jbUzcw5B zto!BN)K~d$Nh76hx8LKo^m2%4c|mNY_ohBmVUuNRZO17hbuSgn>E_A7O!OJ$>~%K3 zSj$&;w{_ErID-9x1LA~$slmSv|FmN_wg*fAzAXof zlbe7nc}>6v@_41wPh;h?#~Ep*upZyCSl8uqJv?PF&6q1wwYDRMT~aUPQd_RenttEX z#jxQ4@Mh54w7CY&ZFyrO$ef#$Ix35hl?C5%l@V8|JcAyR$P<>J=5^1paha_%#EVLM zvNFWLwQkAjJ1z=4qIQJ4>Q(3Sx2+5Tw`}?6oCIqYWvLu7{`fu-S5{ceN@9#uab{!J z>EBF}0MFd5jzg{y2iTn>zXvlNvgX9{Cvof_C1=Bg^t7#B@{;!VXeNz>lk1eoIXRXy zyE|6)7fY{R%``)lcv=QaqCZ}rHr23Gut#Qo2f6qOy455IybY?tvpQ4v27SC0mc!d) z7gp2s-c`ig&#O6~9tXr*Cth`RRk}z z0hwtL=W5veFVTuOcmhMxI^;u%Z8dQexy#1JvcMwYt1E@>zI?vsIE1xc$YFTedBoPs)8FC z7@OqwjSTp{ATJ+1q@aul*Vx;Fta&0krr_(8>Q`58W>*{6e}8AyUkB&e-NewhJb=)) z2TE@)nH@VPU3YErty^TM4>~CO2k4dOi$NuA`@b)&YR9&tcZy&ATDK-rp6sVjVeA|T zC((%u@M0460yqE+&*M6_LNJZTe$rt_UI|epcqBv|^}Q|Cyo$Hw7CZZbkK+RX zP)*8w6<41-Q0;n`@r{{u?UGCE{FCN~$j*#$Hx`pZ%)sqDaNBO`56$uNgf^xm2K|gq z#d~W3i+#Qf9`^RyR4osJ3LLOxgTQ^g>zs!sX`D#V5(un#GqHGBrc>+D191ju6E5HZ z?2nGne{ueJ`P>z&QJrvDE$s8K zu;<7v+pZcd?-*Co{qD!ha)@NnFrO_sZuU$KZX6i#D?Xmd8b4^08GbYjY0)cit13_)<jR8k_WJ@*_ua-NhppdAI|3e)5q< z=YfXb>gpmVj;rZNB5BxWUl!6)z8z5e93V*toW8T$4M;VsF!6!XJF(b+=}cxr_M@w- z>9p@Z-N=;aT=y=kGaWNXI7p{%+OSO_rX!YSwSh@GwB#r6;$bcwvTy*U!`_a}J9p5%75+be=SFuk_t^mE zLE5wY+8S|Wa+kFm0M|67Go3vXnX~HlGdRc=volp@aA0urx!Y@vC(Ldqox7QW6>`57 z-3-tIJ+d`O(4PUow*rWF*H+q;HQ8;%;wN{%m6H{|5LxhJS#IsHv=3U@)XB}nQCC}` zrB0G8_0bPDTAn%jlpd@tZg=nHpcw`RaR<5$1M-|vY7Q=oN}27*CfknuUTYw3^( zq-{jn+i8_`Z9#+gv)_bt7^M+sJT4ifgSyn&ysbPL0Ic6jbF@(=BgO!#(SL8@4sqNH zq)@<{|M}%18C<#Z=YMqoEC`&1M0TLzSr{XIP1*1f0Qb3KMMt^=&dJRVdpg0xR zoq`t+_~9rc1(!~G;Tn^M16c6oj$iR=ls7zLL>GVfmJZ;#{2s1j@$i9`yo9?v<5%4C zWPl%K(kPXw@@oaiAM!z0oM9Q1FH_K|o3(VjI&^?^J%Du+Fvj}Y z-rLBP!AdL`o&7e;F(9*{{Grd)oq0A~Mj){j%ath&XNyKRz6 zy3N!*Kk_$qt=%Z|V)7@O<@uQg=C`vZJfA_ia`tPhX@|)RAm0ioEXFe3Ntv%-I|{I7 z9eFu}pbQ3Bzy=`0&afTO0leD*RG>|^rnK|FS?hOit^}l&v7belPd(CuehPW7dpiRW zaoy%6S_TO-*G1L?D$yz*4Ipl3R*-QX{zMa8@PgWWfsIQr|9-s|A zPU*p1uLqku3*E!i!I5rmz$3jq*1zzqeY^0ljcFfrxZ-Qmesk(U9Bqg0YxCL%P|`M~ z6}Q@^c7hh5qufSZ3;+(kt_e`wPg$Rb#N?~?lshZ*?1OgnZ zOXt*sT!E`*HO%AL-0d_zOsBQKyPk?RMSZ$qN`K9C?+gow^^kJ3%~ToXUQixNO@Uop zDS*mV=@gG5x>GPVPZ%eh(otk1GtYhlfDbt z3VhMPL&c+@p6l|A2X|mDe~K+%{K-cn5I*eGvvd?1_#1HoG37Tc{NaZmw)E&-Bm=ym zD@{51DxdOeIIfb1sbTq5LzK?5_;|;whKi4p9a&U;NGCt#;y>!dXMY z0WrYy!Q=Hd^=Zl$pe+SB0GLkKRI6!6o$S(}e$WF%kFz0%6|)$tsdl_R$&}>APWCAz z4{7hmvb^_nE7Pmdkp_~p0gU}N<>(o}t}MnP4?mnWJyXJS*{LqP)*i)cSNPJA%hz+$ z-S{PHKs;+}v7kRpd02zWfDhBrq(N5FE${VKs;Swv#hsLGrcEc`i{<^%lMJ4cR(w;! zTjARVB=lKS|k}6I4IMZ&r!uIEavLVqpt#T zE_u#nu-{J|XJ#dJWcpVfDE~srr%bGCe)##|(R!=9&49W5g#$j01${r3qv>G-C|0V$ zl{zW@@NckV1DQMwhI&0PUEY^pUZ?aRG6c2;D{bm~5H8P(>o-^Z4S#JE@JG!QwCk{-*ZySZ~rK8%;S*HDl zq<(=~0S^{fV_Bz(!0q11?!D8yyOIyjW{7(QHhz1mUAk~U`@W_uB$PK{vAb&b` zXr#b^)TX4lBZu+sl;j-uB}tiiQ5O0|XVSL#|G_ltIO+bh4_7r&N^HGi9Y1 z(xCxVDFy`sTH;6#Ab2t9HIONq6dOH_0R3|Oioa4&cX7~=UOs-y zb2y)(;a>bJAAU4IaXo7&8YPT9?hqrZ!%<9l??lp_lV#IQ=9WIFYJ*7S7rKtTt}v8(_Bu+fR?+^G+sU}5RF zR})_+iay6u+Gi1EVA@+v);V?Bv1((kuo%69#*tM6PW*&6i&35jely9$8N_3y;)|77 z04u$VV-W(-2LbO7pY$E(XEGgqC+oAygm>WTvL5MK*`+&(3CqegK-g06@e}TjC(a;* zU%yP$ugkJ#pKfJcHZ)D^y6<1_(@49SX-_;a>tN*_nY#yS&$Pl8KXuBoo>}&)N;>1r z8if4LRk1#IBPUj|SpXKZOkn4P?N!_#l;2dTUR(<8oSP#x7q@#f6 z{K{M1%3FV`n_tQt{mw?W5r932|3MmH3U{h#%1V(6L?|Fdv;I(uR*Fq-z@R#NgeXT{ z6vHJQ#Vn<%aPcV{`e+D8&sBivu3_PclNL=1%MZVGNsl*;Py>{%2Oy#w1U_H5R-Sul&xFfxM(Cz8d3$Km3GO9Ny$Z2K+BZ*$}(&;ay>^8mVa9MXOAU(D;fJcYj);bfG;Z=P5{P4rN<5mDyXaQI*5OJfa_Ref}{kac3qAOODy zPaq9oT?P=>A`j&Nf;JghOO=m7%znVvnl4?!w{o(qmy|RAt)I6#$z7e)R4*A}v2?`7 zFy&Nk{&;SBfG2t|&>b}*?=};tyn`#=y)2Tr+JZ3cR~rM01(xpR=y$dK>Q|~=>Z@8` z-gGFuru^t>Tl$nH`{YN~F0|y04mZqO#o_Nad2z%2UGc{cPh6D&FUo;7Kk3oYC+R<` zkJFxq`N9*w>VLIuzb6b#(*V5sva*_j1ZYx7Q@I@A7H{Mo(`+4IzLg!l{chET<*W&;-0il*`&_~o^A4)_P-nN+yWP#~b_?hp z>L*4Dq{1l}pfRE|RRNf4aa_d;Q{gJUK#O8g7+@uR0iZaP#l5Zq09jKyn*lf@ON#Fq zAfkb$X;yi1o{LW1t1)^m1*WVT6F)rTixTt0Yw<5!na^|lOR?S2t~FY+DH@|P&=JT_ z_^3luo@7uBuw))eLE zvw6oR9n-oxMxb=Gn6#oE zYi(z!jZJOJ5-=u+C5kTK(aBq@WqAV5!*neA=)(XvXKH#S;05&L%OyB*!Vj6{YCOri zh4v9F?=*`ELc}$i`O{m^^dSJ9PS|TQz~KHyKqZ6Scd|}tlac9db#NzBp<%5rV{(uN zPy=3I0?3tPF`JaoCWD^0fvg%=$DB1@`~f~_cn!raSynoy6y!=qs?s2YV;y>KvQL@K z5~xr0WpkE62}{z>TDyqx1b8`vi+58lu1!%#&dO<%Qrk%#_O^GI+O)1TWM%NO7#Xx( z-=zo1zv)4fW3LCTyrE0q+F&QLp?CN!U92`%gI-{x4>0qj-B#Nzo!71`wOD@9Zdk7F zWsT}5q|si`uC}SIc-Drhf2#I{7r*h<>ue96O`b#k`V>>HcKPZT0X4ql%}LAEU|xSy ze%0Q~+QyHva^h58#gpe6B+Fysz;q@J!hsA6=@k1Nx*3e+(1xv4kXK@sV!3Jr8lVQ} z9n-mkjCyyr@|-c);pq`%lf*0kNd*AW%TDu}`k6_2vhm22Za*+cb2PnccW>58jgal8 z^C9$uwyqjQYyZw?UWEb+>jVIVvJ^;Z3>2EOQx?iWF{Gi$cG6OgQc!Uz1H~&q5RWCL z5%8z*F3Q8tRX`y>jSFB1=c>WDb9kk!lpWZ~4?h|eE7XN28UX&JB?G)^SfetKXEo>%vd6M28Q>e8YUs+PJatz-e9I3{F7hI~l9TXfl?4yd zm6MJf3@#t3w{*0?3$R%uHHAn=?yqMpH0x=mc6+es2Pk&VK1!O_p(pNUrV&jQnpT{j z$(q{0TB_+*Yjf7wtjXbHA#Dv1>dbZa{N%~{(rL4{SXDZ7>wv(5RRu_d0~uh!`eOB& zx@?<;5^ncx%P%wlGa!@36fCRG1}Ol=-)1XQ*Va3&83L5GrGC1a98A}?9j%dXTUQRi z;T4SGZ!6CmbZ;{+`e(xsFa?;v>tTRP+~OU`?H>e~XS0kEes;p=e!@>`rLUb7_1FS5 zWv*V(u;cboXqvh%tC0Mf^*-1Xgo0hp>Ps3xYlkx3P*!)cdYZNDwWJl^)_50eP&(|b zaRa7Pb&~gkBOfxAuX^)3jW*WwAUvJXgPG_-;?swN%otcYqvP67wF|N-(9mA=6?C#d zN_)`uxYBKH#(E-u?Yr7Cebz?B({|89r`o1`v?u)&r(eOpJn>Y6V>G3gH^1sf+^fwL zK%=P)^3jI%6-BSwrm{+lmh|#+aq_^EJki24I%w$cq*YFICkjjfifLVkGf>_dApsbE zhhh9MubC*gj)`j@V@{gA#3{kGfGhnIjemt{UK@}O_A(gQc-U>kPS}ps$sO!uO)hJr zUYj|{v4T!Y+RpRAUPgOQ@5L(3dw@}d^PIA(Xbbv&Qv}Mvsp#5~tC9g!H4yQHaeh-Y zcd|y0atQaEjM3H@D2_(LpF@v=Q9kv-A06?fqk#OR1M+yni$)+Gg_Jix`BBok#Mj`2 zQB-N=ah@w0M#9Q~Hs0h7*yQgTmVtb%cZhotUFDFkjZA*yiEPL~xzVq3^2e*^l7X~3 z3B1U|vwX$F^LdWHxaf_*_;GafrM#AQ=Jqu4iiZC{x-ma&B+cjfw|&6o*r}6vrLF3MWrxq$m`J zD?p<#Xh|;)C6QLRwC(zT=b+J9&G|VY0i^D0sTcZ32jfNbk?0Zdj9OHU0{n8O~tN2%i$4%|P=uH64}?@>EkKpxFH3qSJE zhPT464LN2%hx6+<*2FJql&_UB^3c=Buaz@t#hZ=4wOpq(gSJ5^{3=JAAxN0?TS+J^Sys&MHG znE0bFzPzMGOWWZm-tcqvCBo4br~J^b{Hjgzt2U=i4Nn2fS6RQ6ck~Sk(2>qn>F~;* zlP6w=%h&?)=h{Sp>42-gQY?h`jgXcSSab30tE~UqdkXh!5U2M%^OxW*2d1g$`8(a= z?JsuwZ~d9yK=u6;@^GdFty_etmfTa_@Oo z4x#@NBxGME8XaQm}c>We}i1sHBiMrkOZbRYgE8JN>84{G|%H$ZFWzGfoa7ZCr%?+jNJx=~sloOx+S4;bhbYihAPch{&?zu8( znVuy+%FF0;PyOs)lvsb~@gjB~bbfgAO-2`%1yzxTg+J`q!i1l8EzJc581+7;pC2`I zQq+d^jvw8uStLLNluX@PLMd>nw8QrTvths2`MdIW2dD%yNC38oiv`^EyIwVB2Ro-;eyD6^L@CG2M5#+v4tx4GXKlhcepArctv#3rp+`tZFB z3ZDSZOiOy*1pu0AG|hMnq?5+@l%Hox;6?OqIF13I^-R;fKo(G)=UxVo&P)HQe#80u zjZb$@c?wuxHJ>S%zK8_VdtA!~p>E@&ZYFE9sf_?@7N2vdLbFa&AyYS%z^6$9RNk6v zmS8jCXw9~)s85p=KD}iHLM+)j6^yla5tvS*l!n3ds`#5oFzpL!YBj}dI5jlYmJvl8 zQy5u{hFz6Z&d|v;>S1>MZGD(6lXl|DJ9FrVrg?4VX^fmnHT9TE;FBzYLGznx|0lVU zFDM%@HATv*1FHJ$0;mg;eB!+)4or=1YR9ejaoym4{tGyO4@z*GWL34CfK08FQ@mq2!zH#=`4W>Vtm*=22dS02y7iQ zXiGYN8?eSg+}ezFdd`m4@riFrc`AXa1f~+0N?#*`uges?ECo=%+5}K!V61uF1!Fx!0sr%-NB3Rc%044#?I0}Gj!YVU=dBWsR07*qoM6N<$g5^jzrvLx| diff --git a/docs/images/user-guides/desktop/coder-desktop-win-enable-coder-connect.png b/docs/images/user-guides/desktop/coder-desktop-win-enable-coder-connect.png new file mode 100644 index 0000000000000000000000000000000000000000..ed9ec69559094a50569e5a5c5f547cdc7fa09224 GIT binary patch literal 237890 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