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/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/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/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/docs/resources/script.md b/docs/resources/script.md index 22ac1b50..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 @@ -43,15 +43,26 @@ resource "coder_script" "code-server" { }) } -resource "coder_script" "nightly_sleep_reminder" { - agent_id = coder_agent.dev.agent_id +resource "coder_script" "nightly_update" { + agent_id = coder_agent.dev.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/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/examples/resources/coder_script/resource.tf b/examples/resources/coder_script/resource.tf index b7fced38..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 @@ -28,15 +28,26 @@ resource "coder_script" "code-server" { }) } -resource "coder_script" "nightly_sleep_reminder" { - agent_id = coder_agent.dev.agent_id +resource "coder_script" "nightly_update" { + agent_id = coder_agent.dev.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 = <\"`.", - 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/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/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..ca1239f4 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 ( @@ -204,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": { @@ -223,15 +225,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 +259,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) { diff --git a/provider/script.go b/provider/script.go index df436ead..23ddec6e 100644 --- a/provider/script.go +++ b/provider/script.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "strings" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -13,6 +14,32 @@ import ( var ScriptCRONParser = cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor) +// ValidateCronExpression validates a cron expression and provides helpful warnings for common mistakes +func ValidateCronExpression(cronExpr string) (warnings []string, errors []error) { + // Check if it looks like a 5-field Unix cron expression + fields := strings.Fields(cronExpr) + if len(fields) == 5 { + // Try to parse as standard Unix cron (without seconds) + unixParser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor) + if _, err := unixParser.Parse(cronExpr); err == nil { + // It's a valid 5-field expression, provide a helpful warning + warnings = append(warnings, fmt.Sprintf( + "The cron expression '%s' appears to be in Unix 5-field format. "+ + "Coder uses 6-field format (seconds minutes hours day month day-of-week). "+ + "Consider prefixing with '0 ' to run at the start of each minute: '0 %s'", + cronExpr, cronExpr)) + } + } + + // Validate with the actual 6-field parser + _, err := ScriptCRONParser.Parse(cronExpr) + if err != nil { + errors = append(errors, fmt.Errorf("%s is not a valid cron expression: %w", cronExpr, err)) + } + + return warnings, errors +} + func scriptResource() *schema.Resource { return &schema.Resource{ SchemaVersion: 1, @@ -72,17 +99,13 @@ func scriptResource() *schema.Resource { ForceNew: true, Type: schema.TypeString, Optional: true, - Description: "The cron schedule to run the script on. This is a cron expression.", + Description: "The cron schedule to run the script on. This uses a 6-field cron expression format: `seconds minutes hours day-of-month month day-of-week`. Note that this differs from the standard Unix 5-field format by including seconds as the first field. Examples: `\"0 0 22 * * *\"` (daily at 10 PM), `\"0 */5 * * * *\"` (every 5 minutes), `\"30 0 9 * * 1-5\"` (weekdays at 9:30 AM).", ValidateFunc: func(i interface{}, _ string) ([]string, []error) { v, ok := i.(string) if !ok { return []string{}, []error{fmt.Errorf("got type %T instead of string", i)} } - _, err := ScriptCRONParser.Parse(v) - if err != nil { - return []string{}, []error{fmt.Errorf("%s is not a valid cron expression: %w", v, err)} - } - return nil, nil + return ValidateCronExpression(v) }, }, "start_blocks_login": { diff --git a/provider/script_test.go b/provider/script_test.go index 37f1a819..64808372 100644 --- a/provider/script_test.go +++ b/provider/script_test.go @@ -8,6 +8,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/coder/terraform-provider-coder/v2/provider" ) func TestScript(t *testing.T) { @@ -124,3 +126,73 @@ func TestScriptStartBlocksLoginRequiresRunOnStart(t *testing.T) { }}, }) } + +func TestValidateCronExpression(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cronExpr string + expectWarnings bool + expectErrors bool + warningContains string + }{ + { + name: "valid 6-field expression", + cronExpr: "0 0 22 * * *", + expectWarnings: false, + expectErrors: false, + }, + { + name: "valid 6-field expression with seconds", + cronExpr: "30 0 9 * * 1-5", + expectWarnings: false, + expectErrors: false, + }, + { + name: "5-field Unix format - should warn", + cronExpr: "0 22 * * *", + expectWarnings: true, + expectErrors: false, + warningContains: "appears to be in Unix 5-field format", + }, + { + name: "5-field every 5 minutes - should warn", + cronExpr: "*/5 * * * *", + expectWarnings: true, + expectErrors: false, + warningContains: "Consider prefixing with '0 '", + }, + { + name: "invalid expression", + cronExpr: "invalid", + expectErrors: true, + }, + { + name: "too many fields", + cronExpr: "0 0 0 0 0 0 0", + expectErrors: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + warnings, errors := provider.ValidateCronExpression(tt.cronExpr) + + if tt.expectWarnings { + require.NotEmpty(t, warnings, "Expected warnings but got none") + if tt.warningContains != "" { + require.Contains(t, warnings[0], tt.warningContains) + } + } else { + require.Empty(t, warnings, "Expected no warnings but got: %v", warnings) + } + + if tt.expectErrors { + require.NotEmpty(t, errors, "Expected errors but got none") + } else { + require.Empty(t, errors, "Expected no errors but got: %v", errors) + } + }) + } +} 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, 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: ` 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