From e890833351e229dc226db1b7ebcbe7767c228de6 Mon Sep 17 00:00:00 2001 From: Danny Kopping Date: Mon, 23 Jun 2025 21:33:13 +0200 Subject: [PATCH 1/9] feat: implement default preset (#414) --- docs/data-sources/workspace_preset.md | 11 ++++ .../coder_workspace_preset/data-source.tf | 10 ++++ integration/integration_test.go | 1 + integration/test-data-source/main.tf | 4 +- provider/workspace_preset.go | 7 +++ provider/workspace_preset_test.go | 56 +++++++++++++++++++ 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/docs/data-sources/workspace_preset.md b/docs/data-sources/workspace_preset.md index 26e597e2..69057403 100644 --- a/docs/data-sources/workspace_preset.md +++ b/docs/data-sources/workspace_preset.md @@ -27,6 +27,16 @@ data "coder_workspace_preset" "example" { (data.coder_parameter.ami.name) = "ami-xxxxxxxx" } } + +# Example of a default preset that will be pre-selected for users +data "coder_workspace_preset" "standard" { + name = "Standard" + default = true + parameters = { + (data.coder_parameter.instance_type.name) = "t3.medium" + (data.coder_parameter.region.name) = "us-west-2" + } +} ``` @@ -38,6 +48,7 @@ data "coder_workspace_preset" "example" { ### Optional +- `default` (Boolean) Whether this preset should be selected by default when creating a workspace. Only one preset per template can be marked as default. - `parameters` (Map of String) Workspace parameters that will be set by the workspace preset. For simple templates that only need prebuilds, you may define a preset with zero parameters. Because workspace parameters may change between Coder template versions, preset parameters are allowed to define values for parameters that do not exist in the current template version. - `prebuilds` (Block Set, Max: 1) Configuration for prebuilt workspaces associated with this preset. Coder will maintain a pool of standby workspaces based on this configuration. When a user creates a workspace using this preset, they are assigned a prebuilt workspace instead of waiting for a new one to build. See prebuilt workspace documentation [here](https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces.md) (see [below for nested schema](#nestedblock--prebuilds)) diff --git a/examples/data-sources/coder_workspace_preset/data-source.tf b/examples/data-sources/coder_workspace_preset/data-source.tf index 4f29a199..3c245f7a 100644 --- a/examples/data-sources/coder_workspace_preset/data-source.tf +++ b/examples/data-sources/coder_workspace_preset/data-source.tf @@ -12,3 +12,13 @@ data "coder_workspace_preset" "example" { (data.coder_parameter.ami.name) = "ami-xxxxxxxx" } } + +# Example of a default preset that will be pre-selected for users +data "coder_workspace_preset" "standard" { + name = "Standard" + default = true + parameters = { + (data.coder_parameter.instance_type.name) = "t3.medium" + (data.coder_parameter.region.name) = "us-west-2" + } +} diff --git a/integration/integration_test.go b/integration/integration_test.go index b075aebd..ec0b5e4f 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -93,6 +93,7 @@ func TestIntegration(t *testing.T) { "workspace_parameter.value": `param value`, "workspace_parameter.icon": `param icon`, "workspace_preset.name": `preset`, + "workspace_preset.default": `true`, "workspace_preset.parameters.param": `preset param value`, "workspace_preset.prebuilds.instances": `1`, "workspace_preset.prebuilds.expiration_policy.ttl": `86400`, diff --git a/integration/test-data-source/main.tf b/integration/test-data-source/main.tf index 12344546..5d312b1a 100644 --- a/integration/test-data-source/main.tf +++ b/integration/test-data-source/main.tf @@ -20,7 +20,8 @@ data "coder_parameter" "param" { icon = "param icon" } data "coder_workspace_preset" "preset" { - name = "preset" + name = "preset" + default = true parameters = { (data.coder_parameter.param.name) = "preset param value" } @@ -64,6 +65,7 @@ locals { "workspace_parameter.value" : data.coder_parameter.param.value, "workspace_parameter.icon" : data.coder_parameter.param.icon, "workspace_preset.name" : data.coder_workspace_preset.preset.name, + "workspace_preset.default" : tostring(data.coder_workspace_preset.preset.default), "workspace_preset.parameters.param" : data.coder_workspace_preset.preset.parameters.param, "workspace_preset.prebuilds.instances" : tostring(one(data.coder_workspace_preset.preset.prebuilds).instances), "workspace_preset.prebuilds.expiration_policy.ttl" : tostring(one(one(data.coder_workspace_preset.preset.prebuilds).expiration_policy).ttl), diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index 0a44b1eb..1d7576e9 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -19,6 +19,7 @@ var PrebuildsCRONParser = rbcron.NewParser(rbcron.Minute | rbcron.Hour | rbcron. type WorkspacePreset struct { Name string `mapstructure:"name"` + Default bool `mapstructure:"default"` Parameters map[string]string `mapstructure:"parameters"` // There should always be only one prebuild block, but Terraform's type system // still parses them as a slice, so we need to handle it as such. We could use @@ -92,6 +93,12 @@ func workspacePresetDataSource() *schema.Resource { Required: true, ValidateFunc: validation.StringIsNotEmpty, }, + "default": { + Type: schema.TypeBool, + Description: "Whether this preset should be selected by default when creating a workspace. Only one preset per template can be marked as default.", + Optional: true, + Default: false, + }, "parameters": { Type: schema.TypeMap, Description: "Workspace parameters that will be set by the workspace preset. For simple templates that only need prebuilds, you may define a preset with zero parameters. Because workspace parameters may change between Coder template versions, preset parameters are allowed to define values for parameters that do not exist in the current template version.", diff --git a/provider/workspace_preset_test.go b/provider/workspace_preset_test.go index 84dfec17..073193c6 100644 --- a/provider/workspace_preset_test.go +++ b/provider/workspace_preset_test.go @@ -530,6 +530,62 @@ func TestWorkspacePreset(t *testing.T) { }`, ExpectError: regexp.MustCompile(`schedules overlap with each other: schedules overlap: \* 8-18 \* \* 1-5 and \* 18-19 \* \* 5-6`), }, + { + Name: "Default field set to true", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + default = true + parameters = { + "region" = "us-east1-a" + } + }`, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 1) + resource := state.Modules[0].Resources["data.coder_workspace_preset.preset_1"] + require.NotNil(t, resource) + require.Equal(t, resource.Primary.Attributes["default"], "true") + return nil + }, + }, + { + Name: "Default field set to false", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + default = false + parameters = { + "region" = "us-east1-a" + } + }`, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 1) + resource := state.Modules[0].Resources["data.coder_workspace_preset.preset_1"] + require.NotNil(t, resource) + require.Equal(t, resource.Primary.Attributes["default"], "false") + return nil + }, + }, + { + Name: "Default field not provided (defaults to false)", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + parameters = { + "region" = "us-east1-a" + } + }`, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 1) + resource := state.Modules[0].Resources["data.coder_workspace_preset.preset_1"] + require.NotNil(t, resource) + require.Equal(t, resource.Primary.Attributes["default"], "false") + return nil + }, + }, } for _, testcase := range testcases { From e6bbd8c7c72323518eb432be8b267ee8a3b6f674 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 19:47:44 +0500 Subject: [PATCH 2/9] build(deps): Bump golang.org/x/mod from 0.24.0 to 0.25.0 (#411) 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 fcb25b13..370fd5f3 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.10.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 - golang.org/x/mod v0.24.0 + golang.org/x/mod v0.25.0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 ) diff --git a/go.sum b/go.sum index 31e83346..ff0ed9c7 100644 --- a/go.sum +++ b/go.sum @@ -231,8 +231,8 @@ golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXy 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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -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/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= From 0ce611a8d9fdba7955ebc7a3fe51d9d394758dc8 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Fri, 27 Jun 2025 20:43:57 +0500 Subject: [PATCH 3/9] docs: clarify cron attribute format for coder_script resource (#409) Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> --- docs/resources/script.md | 19 ++++-- examples/resources/coder_script/resource.tf | 17 ++++- provider/script.go | 35 ++++++++-- provider/script_test.go | 72 +++++++++++++++++++++ 4 files changed, 130 insertions(+), 13 deletions(-) diff --git a/docs/resources/script.md b/docs/resources/script.md index 22ac1b50..9058fce4 100644 --- a/docs/resources/script.md +++ b/docs/resources/script.md @@ -43,15 +43,26 @@ resource "coder_script" "code-server" { }) } -resource "coder_script" "nightly_sleep_reminder" { +resource "coder_script" "nightly_update" { agent_id = coder_agent.dev.agent_id display_name = "Nightly update" icon = "/icon/database.svg" - cron = "0 22 * * *" + cron = "0 0 22 * * *" # Run at 22:00 (10 PM) every day script = <"`. - `log_path` (String) The path of a file to write the logs to. If relative, it will be appended to tmp. - `run_on_start` (Boolean) This option defines whether or not the script should run when the agent starts. The script should exit when it is done to signal that the agent is ready. diff --git a/examples/resources/coder_script/resource.tf b/examples/resources/coder_script/resource.tf index b7fced38..8b3fa661 100644 --- a/examples/resources/coder_script/resource.tf +++ b/examples/resources/coder_script/resource.tf @@ -28,15 +28,26 @@ resource "coder_script" "code-server" { }) } -resource "coder_script" "nightly_sleep_reminder" { +resource "coder_script" "nightly_update" { agent_id = coder_agent.dev.agent_id display_name = "Nightly update" icon = "/icon/database.svg" - cron = "0 22 * * *" + cron = "0 0 22 * * *" # Run at 22:00 (10 PM) every day script = < Date: Fri, 27 Jun 2025 21:02:05 +0500 Subject: [PATCH 4/9] Fix coder_script agent_id reference typos (#418) Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: matifali <10648092+matifali@users.noreply.github.com> --- docs/resources/script.md | 8 ++++---- examples/resources/coder_script/resource.tf | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/resources/script.md b/docs/resources/script.md index 9058fce4..21bfaec9 100644 --- a/docs/resources/script.md +++ b/docs/resources/script.md @@ -22,7 +22,7 @@ resource "coder_agent" "dev" { } resource "coder_script" "dotfiles" { - agent_id = coder_agent.dev.agent_id + agent_id = coder_agent.dev.id display_name = "Dotfiles" icon = "/icon/dotfiles.svg" run_on_start = true @@ -33,7 +33,7 @@ resource "coder_script" "dotfiles" { } resource "coder_script" "code-server" { - agent_id = coder_agent.dev.agent_id + agent_id = coder_agent.dev.id display_name = "code-server" icon = "/icon/code.svg" run_on_start = true @@ -44,7 +44,7 @@ resource "coder_script" "code-server" { } resource "coder_script" "nightly_update" { - agent_id = coder_agent.dev.agent_id + agent_id = coder_agent.dev.id display_name = "Nightly update" icon = "/icon/database.svg" cron = "0 0 22 * * *" # Run at 22:00 (10 PM) every day @@ -56,7 +56,7 @@ resource "coder_script" "nightly_update" { } resource "coder_script" "every_5_minutes" { - agent_id = coder_agent.dev.agent_id + agent_id = coder_agent.dev.id display_name = "Health check" icon = "/icon/heart.svg" cron = "0 */5 * * * *" # Run every 5 minutes diff --git a/examples/resources/coder_script/resource.tf b/examples/resources/coder_script/resource.tf index 8b3fa661..53c9dfb8 100644 --- a/examples/resources/coder_script/resource.tf +++ b/examples/resources/coder_script/resource.tf @@ -7,7 +7,7 @@ resource "coder_agent" "dev" { } resource "coder_script" "dotfiles" { - agent_id = coder_agent.dev.agent_id + agent_id = coder_agent.dev.id display_name = "Dotfiles" icon = "/icon/dotfiles.svg" run_on_start = true @@ -18,7 +18,7 @@ resource "coder_script" "dotfiles" { } resource "coder_script" "code-server" { - agent_id = coder_agent.dev.agent_id + agent_id = coder_agent.dev.id display_name = "code-server" icon = "/icon/code.svg" run_on_start = true @@ -29,7 +29,7 @@ resource "coder_script" "code-server" { } resource "coder_script" "nightly_update" { - agent_id = coder_agent.dev.agent_id + agent_id = coder_agent.dev.id display_name = "Nightly update" icon = "/icon/database.svg" cron = "0 0 22 * * *" # Run at 22:00 (10 PM) every day @@ -41,7 +41,7 @@ resource "coder_script" "nightly_update" { } resource "coder_script" "every_5_minutes" { - agent_id = coder_agent.dev.agent_id + agent_id = coder_agent.dev.id display_name = "Health check" icon = "/icon/heart.svg" cron = "0 */5 * * * *" # Run every 5 minutes From f239e51ecbafcb7184e9965a1303fd62f8deb4b6 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:41:17 +0500 Subject: [PATCH 5/9] Mark tokens as sensitive in data sources (#416) * Mark tokens as sensitive in data sources Mark the following attributes as sensitive to prevent them from being logged or displayed in Terraform output: - data.coder_workspace_owner.me.oidc_access_token - data.coder_workspace_owner.me.session_token - data.coder_external_auth.example.access_token This follows the same pattern as ssh_private_key and agent token which are already marked as sensitive. Fixes #266 Co-authored-by: matifali <10648092+matifali@users.noreply.github.com> * Update documentation for sensitive token attributes Regenerate documentation to reflect that oidc_access_token, session_token, and access_token are now marked as sensitive in the schema. Co-authored-by: matifali <10648092+matifali@users.noreply.github.com> --------- Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: matifali <10648092+matifali@users.noreply.github.com> --- docs/data-sources/external_auth.md | 2 +- docs/data-sources/workspace_owner.md | 4 ++-- provider/externalauth.go | 1 + provider/workspace_owner.go | 2 ++ 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/data-sources/external_auth.md b/docs/data-sources/external_auth.md index e4089f24..d1e6d649 100644 --- a/docs/data-sources/external_auth.md +++ b/docs/data-sources/external_auth.md @@ -39,4 +39,4 @@ data "coder_external_auth" "azure-identity" { ### Read-Only -- `access_token` (String) The access token returned by the external auth provider. This can be used to pre-authenticate command-line tools. +- `access_token` (String, Sensitive) The access token returned by the external auth provider. This can be used to pre-authenticate command-line tools. diff --git a/docs/data-sources/workspace_owner.md b/docs/data-sources/workspace_owner.md index 2a912e1f..f16480ef 100644 --- a/docs/data-sources/workspace_owner.md +++ b/docs/data-sources/workspace_owner.md @@ -52,9 +52,9 @@ resource "coder_env" "git_author_email" { - `id` (String) The UUID of the workspace owner. - `login_type` (String) The type of login the user has. - `name` (String) The username of the user. -- `oidc_access_token` (String) 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. +- `oidc_access_token` (String, Sensitive) 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. - `rbac_roles` (List of Object) The RBAC roles of which the user is assigned. (see [below for nested schema](#nestedatt--rbac_roles)) -- `session_token` (String) Session token for authenticating with a Coder deployment. It is regenerated every time a workspace is started. +- `session_token` (String, Sensitive) Session token for authenticating with a Coder deployment. It is regenerated every time a workspace is started. - `ssh_private_key` (String, Sensitive) The user's generated SSH private key. - `ssh_public_key` (String) The user's generated SSH public key. diff --git a/provider/externalauth.go b/provider/externalauth.go index 915a21a9..b278ecc1 100644 --- a/provider/externalauth.go +++ b/provider/externalauth.go @@ -37,6 +37,7 @@ func externalAuthDataSource() *schema.Resource { Type: schema.TypeString, Description: "The access token returned by the external auth provider. This can be used to pre-authenticate command-line tools.", Computed: true, + Sensitive: true, }, "optional": { Type: schema.TypeBool, diff --git a/provider/workspace_owner.go b/provider/workspace_owner.go index 078047ff..109b0b93 100644 --- a/provider/workspace_owner.go +++ b/provider/workspace_owner.go @@ -113,6 +113,7 @@ func workspaceOwnerDataSource() *schema.Resource { Type: schema.TypeString, Computed: true, Description: "Session token for authenticating with a Coder deployment. It is regenerated every time a workspace is started.", + Sensitive: true, }, "oidc_access_token": { Type: schema.TypeString, @@ -120,6 +121,7 @@ func workspaceOwnerDataSource() *schema.Resource { Description: "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.", + Sensitive: true, }, "login_type": { Type: schema.TypeString, From d5496af252cbe36dc43b94c7d3bcba1257fb4e13 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Tue, 22 Jul 2025 13:19:33 +0100 Subject: [PATCH 6/9] fix: correct URL validation and centralize logic (#421) * fix: correct URL validation and centralize logic * fix: go imports order * tests: add validation URL tests * revert: parameter_test ValidDefaultWithOptions test update * test: update validation_test to use require * test: update validation_test tests --- provider/app.go | 15 +-- provider/app_test.go | 57 +++++++++++ provider/helpers/validation.go | 22 ++++ provider/helpers/validation_test.go | 151 ++++++++++++++++++++++++++++ provider/metadata.go | 15 +-- provider/parameter.go | 27 ++--- provider/parameter_test.go | 28 +++++- provider/provider.go | 12 +-- 8 files changed, 279 insertions(+), 48 deletions(-) create mode 100644 provider/helpers/validation.go create mode 100644 provider/helpers/validation_test.go diff --git a/provider/app.go b/provider/app.go index adbbf0e7..52996a72 100644 --- a/provider/app.go +++ b/provider/app.go @@ -2,13 +2,14 @@ package provider import ( "context" - "net/url" "regexp" "github.com/google/uuid" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/coder/terraform-provider-coder/v2/provider/helpers" ) var ( @@ -93,15 +94,9 @@ func appResource() *schema.Resource { Description: "A URL to an icon that will display in the dashboard. View built-in " + "icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " + "built-in icon with `\"${data.coder_workspace.me.access_url}/icon/\"`.", - ForceNew: true, - Optional: true, - ValidateFunc: func(i any, s string) ([]string, []error) { - _, err := url.Parse(s) - if err != nil { - return nil, []error{err} - } - return nil, nil - }, + ForceNew: true, + Optional: true, + ValidateFunc: helpers.ValidateURL, }, "slug": { Type: schema.TypeString, diff --git a/provider/app_test.go b/provider/app_test.go index aeb42d08..2b9a5580 100644 --- a/provider/app_test.go +++ b/provider/app_test.go @@ -478,4 +478,61 @@ func TestApp(t *testing.T) { }) } }) + + t.Run("Icon", func(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + icon string + expectError *regexp.Regexp + }{ + { + name: "Empty", + icon: "", + }, + { + name: "ValidURL", + icon: "/icon/region.svg", + }, + { + name: "InvalidURL", + icon: "/icon%.svg", + expectError: regexp.MustCompile("invalid URL escape"), + }, + } + + for _, c := range cases { + c := c + + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + config := fmt.Sprintf(` + provider "coder" {} + resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + } + resource "coder_app" "code-server" { + agent_id = coder_agent.dev.id + slug = "code-server" + display_name = "Testing" + url = "http://localhost:13337" + open_in = "slim-window" + icon = "%s" + } + `, c.icon) + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: config, + ExpectError: c.expectError, + }}, + }) + }) + } + }) } diff --git a/provider/helpers/validation.go b/provider/helpers/validation.go new file mode 100644 index 00000000..9cc21b89 --- /dev/null +++ b/provider/helpers/validation.go @@ -0,0 +1,22 @@ +package helpers + +import ( + "fmt" + "net/url" +) + +// ValidateURL validates that value is a valid URL string. +// Accepts empty strings, local file paths, file:// URLs, and http/https URLs. +// Example: for `icon = "/icon/region.svg"`, value is `/icon/region.svg` and label is `icon`. +func ValidateURL(value any, label string) ([]string, []error) { + val, ok := value.(string) + if !ok { + return nil, []error{fmt.Errorf("expected %q to be a string", label)} + } + + if _, err := url.Parse(val); err != nil { + return nil, []error{err} + } + + return nil, nil +} diff --git a/provider/helpers/validation_test.go b/provider/helpers/validation_test.go new file mode 100644 index 00000000..557bae41 --- /dev/null +++ b/provider/helpers/validation_test.go @@ -0,0 +1,151 @@ +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateURL(t *testing.T) { + tests := []struct { + name string + value any + label string + expectError bool + errorContains string + }{ + // Valid cases + { + name: "empty string", + value: "", + label: "url", + expectError: false, + }, + { + name: "valid http URL", + value: "http://example.com", + label: "url", + expectError: false, + }, + { + name: "valid https URL", + value: "https://example.com/path", + label: "url", + expectError: false, + }, + { + name: "absolute file path", + value: "/path/to/file", + label: "url", + expectError: false, + }, + { + name: "relative file path", + value: "./file.txt", + label: "url", + expectError: false, + }, + { + name: "relative path up directory", + value: "../config.json", + label: "url", + expectError: false, + }, + { + name: "simple filename", + value: "file.txt", + label: "url", + expectError: false, + }, + { + name: "URL with query params", + value: "https://example.com/search?q=test", + label: "url", + expectError: false, + }, + { + name: "URL with fragment", + value: "https://example.com/page#section", + label: "url", + expectError: false, + }, + + // Various URL schemes that url.Parse accepts + { + name: "file URL scheme", + value: "file:///path/to/file", + label: "url", + expectError: false, + }, + { + name: "ftp scheme", + value: "ftp://files.example.com/file.txt", + label: "url", + expectError: false, + }, + { + name: "mailto scheme", + value: "mailto:user@example.com", + label: "url", + expectError: false, + }, + { + name: "tel scheme", + value: "tel:+1234567890", + label: "url", + expectError: false, + }, + { + name: "data scheme", + value: "data:text/plain;base64,SGVsbG8=", + label: "url", + expectError: false, + }, + + // Invalid cases + { + name: "non-string type - int", + value: 123, + label: "url", + expectError: true, + errorContains: "expected \"url\" to be a string", + }, + { + name: "non-string type - nil", + value: nil, + label: "config_url", + expectError: true, + errorContains: "expected \"config_url\" to be a string", + }, + { + name: "invalid URL with spaces", + value: "http://example .com", + label: "url", + expectError: true, + errorContains: "invalid character", + }, + { + name: "malformed URL", + value: "http://[::1:80", + label: "endpoint", + expectError: true, + errorContains: "missing ']'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warnings, errors := ValidateURL(tt.value, tt.label) + + if tt.expectError { + require.Len(t, errors, 1, "expected an error but got none") + require.Contains(t, errors[0].Error(), tt.errorContains) + } else { + require.Empty(t, errors, "expected no errors but got: %v", errors) + } + + // Should always return nil for warnings + require.Nil(t, warnings, "expected warnings to be nil but got: %v", warnings) + }) + } +} diff --git a/provider/metadata.go b/provider/metadata.go index 535c700c..5ed6d478 100644 --- a/provider/metadata.go +++ b/provider/metadata.go @@ -2,12 +2,13 @@ package provider import ( "context" - "net/url" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "golang.org/x/xerrors" + + "github.com/coder/terraform-provider-coder/v2/provider/helpers" ) func metadataResource() *schema.Resource { @@ -56,15 +57,9 @@ func metadataResource() *schema.Resource { Description: "A URL to an icon that will display in the dashboard. View built-in " + "icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " + "built-in icon with `\"${data.coder_workspace.me.access_url}/icon/\"`.", - ForceNew: true, - Optional: true, - ValidateFunc: func(i interface{}, s string) ([]string, []error) { - _, err := url.Parse(s) - if err != nil { - return nil, []error{err} - } - return nil, nil - }, + ForceNew: true, + Optional: true, + ValidateFunc: helpers.ValidateURL, }, "daily_cost": { Type: schema.TypeInt, diff --git a/provider/parameter.go b/provider/parameter.go index c8284da1..2d3ea413 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "net/url" "os" "regexp" "strconv" @@ -19,6 +18,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/mitchellh/mapstructure" "golang.org/x/xerrors" + + "github.com/coder/terraform-provider-coder/v2/provider/helpers" ) var ( @@ -223,15 +224,9 @@ func parameterDataSource() *schema.Resource { Description: "A URL to an icon that will display in the dashboard. View built-in " + "icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " + "built-in icon with `\"${data.coder_workspace.me.access_url}/icon/\"`.", - ForceNew: true, - Optional: true, - ValidateFunc: func(i interface{}, s string) ([]string, []error) { - _, err := url.Parse(s) - if err != nil { - return nil, []error{err} - } - return nil, nil - }, + ForceNew: true, + Optional: true, + ValidateFunc: helpers.ValidateURL, }, "option": { Type: schema.TypeList, @@ -263,15 +258,9 @@ func parameterDataSource() *schema.Resource { Description: "A URL to an icon that will display in the dashboard. View built-in " + "icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " + "built-in icon with `\"${data.coder_workspace.me.access_url}/icon/\"`.", - ForceNew: true, - Optional: true, - ValidateFunc: func(i interface{}, s string) ([]string, []error) { - _, err := url.Parse(s) - if err != nil { - return nil, []error{err} - } - return nil, nil - }, + ForceNew: true, + Optional: true, + ValidateFunc: helpers.ValidateURL, }, }, }, diff --git a/provider/parameter_test.go b/provider/parameter_test.go index 9b5e76f1..35f045b9 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -665,7 +665,33 @@ data "coder_parameter" "region" { } `, ExpectError: regexp.MustCompile("ephemeral parameter requires the default property"), - }} { + }, { + Name: "InvalidIconURL", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "string" + icon = "/icon%.svg" + } + `, + ExpectError: regexp.MustCompile("invalid URL escape"), + }, { + Name: "OptionInvalidIconURL", + Config: ` + data "coder_parameter" "region" { + name = "Region" + type = "string" + option { + name = "1" + value = "1" + icon = "/icon%.svg" + description = "Something!" + } + } + `, + ExpectError: regexp.MustCompile("invalid URL escape"), + }, + } { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() diff --git a/provider/provider.go b/provider/provider.go index 43e3a6ac..a0ef63f9 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -10,6 +10,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "golang.org/x/xerrors" + + "github.com/coder/terraform-provider-coder/v2/provider/helpers" ) type config struct { @@ -26,14 +28,8 @@ func New() *schema.Provider { Optional: true, // The "CODER_AGENT_URL" environment variable is used by default // as the Access URL when generating scripts. - DefaultFunc: schema.EnvDefaultFunc("CODER_AGENT_URL", "https://mydeployment.coder.com"), - ValidateFunc: func(i interface{}, s string) ([]string, []error) { - _, err := url.Parse(s) - if err != nil { - return nil, []error{err} - } - return nil, nil - }, + DefaultFunc: schema.EnvDefaultFunc("CODER_AGENT_URL", "https://mydeployment.coder.com"), + ValidateFunc: helpers.ValidateURL, }, }, ConfigureContextFunc: func(c context.Context, resourceData *schema.ResourceData) (interface{}, diag.Diagnostics) { From c85b5f7fb831f892b12ec5022c0a79ce2fd8a334 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:05:15 -0500 Subject: [PATCH 7/9] docs: add link to styling options documentation (#423) Add link to coder.com/docs styling options documentation in the styling parameter description to help users understand available styling attributes. The link is added in the Go source code so it gets properly generated into the documentation by tfplugindocs. Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: bpmct <22407953+bpmct@users.noreply.github.com> --- docs/data-sources/parameter.md | 2 +- provider/parameter.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/data-sources/parameter.md b/docs/data-sources/parameter.md index ecba3929..c1001835 100644 --- a/docs/data-sources/parameter.md +++ b/docs/data-sources/parameter.md @@ -150,7 +150,7 @@ data "coder_parameter" "home_volume_size" { - `mutable` (Boolean) Whether this value can be changed after workspace creation. This can be destructive for values like region, so use with caution! - `option` (Block List) Each `option` block defines a value for a user to select from. (see [below for nested schema](#nestedblock--option)) - `order` (Number) The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order). -- `styling` (String) JSON encoded string containing the metadata for controlling the appearance of this parameter in the UI. This option is purely cosmetic and does not affect the function of the parameter in terraform. +- `styling` (String) JSON encoded string containing the metadata for controlling the appearance of this parameter in the UI. This option is purely cosmetic and does not affect the function of the parameter in terraform. See [styling options documentation](https://coder.com/docs/admin/templates/extending-templates/dynamic-parameters#available-styling-options) for available styling attributes. - `type` (String) The type of this parameter. Must be one of: `"string"`, `"number"`, `"bool"`, `"list(string)"`. - `validation` (Block List, Max: 1) Validate the input of a parameter. (see [below for nested schema](#nestedblock--validation)) diff --git a/provider/parameter.go b/provider/parameter.go index 2d3ea413..ca1239f4 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -205,7 +205,8 @@ func parameterDataSource() *schema.Resource { Type: schema.TypeString, Default: `{}`, Description: "JSON encoded string containing the metadata for controlling the appearance of this parameter in the UI. " + - "This option is purely cosmetic and does not affect the function of the parameter in terraform.", + "This option is purely cosmetic and does not affect the function of the parameter in terraform. " + + "See [styling options documentation](https://coder.com/docs/admin/templates/extending-templates/dynamic-parameters#available-styling-options) for available styling attributes.", Optional: true, }, "mutable": { From e6d58d0ed91f1bcd614ecbac2db228b64c1de3d4 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Tue, 22 Jul 2025 18:11:12 +0100 Subject: [PATCH 8/9] feat: add icon and description fields to workspace preset (#422) * feat: add icon and description fields to workspace preset * chore: update preset.icon to use ValidateURL * docs: add description and icon to preset examples * chore: run make fmt * chore: run make gen * chore: add size limit to preset icon and description --- docs/data-sources/workspace_preset.md | 12 ++- .../coder_workspace_preset/data-source.tf | 10 ++- integration/integration_test.go | 2 + integration/test-data-source/main.tf | 8 +- provider/workspace_preset.go | 26 ++++++- provider/workspace_preset_test.go | 77 +++++++++++++++++++ 6 files changed, 124 insertions(+), 11 deletions(-) diff --git a/docs/data-sources/workspace_preset.md b/docs/data-sources/workspace_preset.md index 69057403..e7de98e4 100644 --- a/docs/data-sources/workspace_preset.md +++ b/docs/data-sources/workspace_preset.md @@ -21,7 +21,9 @@ provider "coder" {} # See the coder_parameter data source's documentation for examples of how to define # parameters like the ones used below. data "coder_workspace_preset" "example" { - name = "example" + name = "example" + description = "Example description of what this preset does." + icon = "/icon/example.svg" parameters = { (data.coder_parameter.example.name) = "us-central1-a" (data.coder_parameter.ami.name) = "ami-xxxxxxxx" @@ -30,8 +32,10 @@ data "coder_workspace_preset" "example" { # Example of a default preset that will be pre-selected for users data "coder_workspace_preset" "standard" { - name = "Standard" - default = true + name = "Standard" + description = "A workspace preset with medium compute in the US West region." + icon = "/icon/standard.svg" + default = true parameters = { (data.coder_parameter.instance_type.name) = "t3.medium" (data.coder_parameter.region.name) = "us-west-2" @@ -49,6 +53,8 @@ data "coder_workspace_preset" "standard" { ### Optional - `default` (Boolean) Whether this preset should be selected by default when creating a workspace. Only one preset per template can be marked as default. +- `description` (String) Describe what this preset does. +- `icon` (String) A URL to an icon that will display in the dashboard. View built-in icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a built-in icon with `"${data.coder_workspace.me.access_url}/icon/"`. - `parameters` (Map of String) Workspace parameters that will be set by the workspace preset. For simple templates that only need prebuilds, you may define a preset with zero parameters. Because workspace parameters may change between Coder template versions, preset parameters are allowed to define values for parameters that do not exist in the current template version. - `prebuilds` (Block Set, Max: 1) Configuration for prebuilt workspaces associated with this preset. Coder will maintain a pool of standby workspaces based on this configuration. When a user creates a workspace using this preset, they are assigned a prebuilt workspace instead of waiting for a new one to build. See prebuilt workspace documentation [here](https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces.md) (see [below for nested schema](#nestedblock--prebuilds)) diff --git a/examples/data-sources/coder_workspace_preset/data-source.tf b/examples/data-sources/coder_workspace_preset/data-source.tf index 3c245f7a..89150761 100644 --- a/examples/data-sources/coder_workspace_preset/data-source.tf +++ b/examples/data-sources/coder_workspace_preset/data-source.tf @@ -6,7 +6,9 @@ provider "coder" {} # See the coder_parameter data source's documentation for examples of how to define # parameters like the ones used below. data "coder_workspace_preset" "example" { - name = "example" + name = "example" + description = "Example description of what this preset does." + icon = "/icon/example.svg" parameters = { (data.coder_parameter.example.name) = "us-central1-a" (data.coder_parameter.ami.name) = "ami-xxxxxxxx" @@ -15,8 +17,10 @@ data "coder_workspace_preset" "example" { # Example of a default preset that will be pre-selected for users data "coder_workspace_preset" "standard" { - name = "Standard" - default = true + name = "Standard" + description = "A workspace preset with medium compute in the US West region." + icon = "/icon/standard.svg" + default = true parameters = { (data.coder_parameter.instance_type.name) = "t3.medium" (data.coder_parameter.region.name) = "us-west-2" diff --git a/integration/integration_test.go b/integration/integration_test.go index ec0b5e4f..423447c9 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -93,6 +93,8 @@ func TestIntegration(t *testing.T) { "workspace_parameter.value": `param value`, "workspace_parameter.icon": `param icon`, "workspace_preset.name": `preset`, + "workspace_preset.description": `preset description`, + "workspace_preset.icon": `preset icon`, "workspace_preset.default": `true`, "workspace_preset.parameters.param": `preset param value`, "workspace_preset.prebuilds.instances": `1`, diff --git a/integration/test-data-source/main.tf b/integration/test-data-source/main.tf index 5d312b1a..cc4802d3 100644 --- a/integration/test-data-source/main.tf +++ b/integration/test-data-source/main.tf @@ -20,8 +20,10 @@ data "coder_parameter" "param" { icon = "param icon" } data "coder_workspace_preset" "preset" { - name = "preset" - default = true + name = "preset" + description = "preset description" + icon = "preset icon" + default = true parameters = { (data.coder_parameter.param.name) = "preset param value" } @@ -65,6 +67,8 @@ locals { "workspace_parameter.value" : data.coder_parameter.param.value, "workspace_parameter.icon" : data.coder_parameter.param.icon, "workspace_preset.name" : data.coder_workspace_preset.preset.name, + "workspace_preset.description" : data.coder_workspace_preset.preset.description, + "workspace_preset.icon" : data.coder_workspace_preset.preset.icon, "workspace_preset.default" : tostring(data.coder_workspace_preset.preset.default), "workspace_preset.parameters.param" : data.coder_workspace_preset.preset.parameters.param, "workspace_preset.prebuilds.instances" : tostring(one(data.coder_workspace_preset.preset.prebuilds).instances), diff --git a/provider/workspace_preset.go b/provider/workspace_preset.go index 1d7576e9..393c7b7f 100644 --- a/provider/workspace_preset.go +++ b/provider/workspace_preset.go @@ -18,9 +18,11 @@ import ( var PrebuildsCRONParser = rbcron.NewParser(rbcron.Minute | rbcron.Hour | rbcron.Dom | rbcron.Month | rbcron.Dow) type WorkspacePreset struct { - Name string `mapstructure:"name"` - Default bool `mapstructure:"default"` - Parameters map[string]string `mapstructure:"parameters"` + Name string `mapstructure:"name"` + Description string `mapstructure:"description"` + Icon string `mapstructure:"icon"` + Default bool `mapstructure:"default"` + Parameters map[string]string `mapstructure:"parameters"` // There should always be only one prebuild block, but Terraform's type system // still parses them as a slice, so we need to handle it as such. We could use // an anonymous type and rd.Get to avoid a slice here, but that would not be possible @@ -93,6 +95,24 @@ func workspacePresetDataSource() *schema.Resource { Required: true, ValidateFunc: validation.StringIsNotEmpty, }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Describe what this preset does.", + ValidateFunc: validation.StringLenBetween(0, 128), + }, + "icon": { + Type: schema.TypeString, + Description: "A URL to an icon that will display in the dashboard. View built-in " + + "icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " + + "built-in icon with `\"${data.coder_workspace.me.access_url}/icon/\"`.", + ForceNew: true, + Optional: true, + ValidateFunc: validation.All( + helpers.ValidateURL, + validation.StringLenBetween(0, 256), + ), + }, "default": { Type: schema.TypeBool, Description: "Whether this preset should be selected by default when creating a workspace. Only one preset per template can be marked as default.", diff --git a/provider/workspace_preset_test.go b/provider/workspace_preset_test.go index 073193c6..d10b8126 100644 --- a/provider/workspace_preset_test.go +++ b/provider/workspace_preset_test.go @@ -23,6 +23,11 @@ func TestWorkspacePreset(t *testing.T) { Config: ` data "coder_workspace_preset" "preset_1" { name = "preset_1" + description = <<-EOT + # Select the machine image + See the [registry](https://container.registry.blah/namespace) for options. + EOT + icon = "/icon/region.svg" parameters = { "region" = "us-east1-a" } @@ -34,6 +39,8 @@ func TestWorkspacePreset(t *testing.T) { require.NotNil(t, resource) attrs := resource.Primary.Attributes require.Equal(t, attrs["name"], "preset_1") + require.Equal(t, attrs["description"], "# Select the machine image\nSee the [registry](https://container.registry.blah/namespace) for options.\n") + require.Equal(t, attrs["icon"], "/icon/region.svg") require.Equal(t, attrs["parameters.region"], "us-east1-a") return nil }, @@ -76,6 +83,76 @@ func TestWorkspacePreset(t *testing.T) { // So we test it here to make sure we don't regress. ExpectError: regexp.MustCompile("Incorrect attribute value type"), }, + { + Name: "Description field is empty", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + description = "" + parameters = { + "region" = "us-east1-a" + } + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: nil, + }, + { + Name: "Description field exceeds maximum supported length (128 characters)", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + description = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur vehicula leo sit amet mi laoreet, sed ornare velit tincidunt. Proin gravida lacinia blandit." + parameters = { + "region" = "us-east1-a" + } + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: regexp.MustCompile(`expected length of description to be in the range \(0 - 128\)`), + }, + { + Name: "Icon field is empty", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + icon = "" + parameters = { + "region" = "us-east1-a" + } + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: nil, + }, + { + Name: "Icon field is an invalid URL", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + icon = "/icon%.svg" + parameters = { + "region" = "us-east1-a" + } + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: regexp.MustCompile("invalid URL escape"), + }, + { + Name: "Icon field exceeds maximum supported length (256 characters)", + Config: ` + data "coder_workspace_preset" "preset_1" { + name = "preset_1" + icon = "https://example.com/path/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.svg" + parameters = { + "region" = "us-east1-a" + } + }`, + // This validation is done by Terraform, but it could still break if we misconfigure the schema. + // So we test it here to make sure we don't regress. + ExpectError: regexp.MustCompile(`expected length of icon to be in the range \(0 - 256\)`), + }, { Name: "Parameters field is not provided", Config: ` From e04ea9c09cae938ca568a14aadc5e65e1ac97026 Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Fri, 8 Aug 2025 10:00:32 +0200 Subject: [PATCH 9/9] feat(coder-attach): add coder_external_agent resource (#424) * add coder_external_agent resource * Change token to agent_id --- docs/resources/external_agent.md | 24 ++++++++++++++++ provider/external_agent.go | 31 ++++++++++++++++++++ provider/external_agent_test.go | 49 ++++++++++++++++++++++++++++++++ provider/provider.go | 1 + 4 files changed, 105 insertions(+) create mode 100644 docs/resources/external_agent.md create mode 100644 provider/external_agent.go create mode 100644 provider/external_agent_test.go diff --git a/docs/resources/external_agent.md b/docs/resources/external_agent.md new file mode 100644 index 00000000..b6d0b8ae --- /dev/null +++ b/docs/resources/external_agent.md @@ -0,0 +1,24 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coder_external_agent Resource - terraform-provider-coder" +subcategory: "" +description: |- + Define an external agent to be used in a workspace. +--- + +# coder_external_agent (Resource) + +Define an external agent to be used in a workspace. + + + + +## Schema + +### Required + +- `agent_id` (String) The `id` property of a `coder_agent` resource to associate with. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/provider/external_agent.go b/provider/external_agent.go new file mode 100644 index 00000000..2856c2bf --- /dev/null +++ b/provider/external_agent.go @@ -0,0 +1,31 @@ +package provider + +import ( + "context" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func externalAgentResource() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 1, + + Description: "Define an external agent to be used in a workspace.", + CreateContext: func(ctx context.Context, rd *schema.ResourceData, _ interface{}) diag.Diagnostics { + rd.SetId(uuid.NewString()) + return nil + }, + ReadContext: schema.NoopContext, + DeleteContext: schema.NoopContext, + Schema: map[string]*schema.Schema{ + "agent_id": { + Type: schema.TypeString, + Description: "The `id` property of a `coder_agent` resource to associate with.", + ForceNew: true, + Required: true, + }, + }, + } +} diff --git a/provider/external_agent_test.go b/provider/external_agent_test.go new file mode 100644 index 00000000..f0638b45 --- /dev/null +++ b/provider/external_agent_test.go @@ -0,0 +1,49 @@ +package provider_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" +) + +func TestExternalAgent(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + + resource "coder_agent" "dev" { + os = "linux" + arch = "amd64" + } + + resource "coder_external_agent" "dev" { + agent_id = coder_agent.dev.id + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 2) + + agentResource := state.Modules[0].Resources["coder_agent.dev"] + require.NotNil(t, agentResource) + externalAgentResource := state.Modules[0].Resources["coder_external_agent.dev"] + require.NotNil(t, externalAgentResource) + + require.Equal(t, agentResource.Primary.Attributes["id"], externalAgentResource.Primary.Attributes["agent_id"]) + return nil + }, + }}, + }) + }) +} diff --git a/provider/provider.go b/provider/provider.go index a0ef63f9..5a2f1972 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -74,6 +74,7 @@ func New() *schema.Provider { "coder_script": scriptResource(), "coder_env": envResource(), "coder_devcontainer": devcontainerResource(), + "coder_external_agent": externalAgentResource(), }, } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy