From 38bc617a63119b6ff45dd5bb6ce7b6ce28b8583f Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Mon, 21 Jul 2025 15:17:37 +0000 Subject: [PATCH 1/6] fix: correct URL validation and centralize logic --- provider/app.go | 14 +++------ provider/app_test.go | 57 ++++++++++++++++++++++++++++++++++ provider/helpers/validation.go | 19 ++++++++++++ provider/metadata.go | 15 +++------ provider/parameter.go | 26 +++++----------- provider/parameter_test.go | 36 ++++++++++++++++----- provider/provider.go | 11 ++----- 7 files changed, 123 insertions(+), 55 deletions(-) create mode 100644 provider/helpers/validation.go diff --git a/provider/app.go b/provider/app.go index adbbf0e7..38135c0b 100644 --- a/provider/app.go +++ b/provider/app.go @@ -2,7 +2,7 @@ package provider import ( "context" - "net/url" + "github.com/coder/terraform-provider-coder/v2/provider/helpers" "regexp" "github.com/google/uuid" @@ -93,15 +93,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..e532c701 --- /dev/null +++ b/provider/helpers/validation.go @@ -0,0 +1,19 @@ +package helpers + +import ( + "fmt" + "net/url" +) + +// ValidateURL checks that the given value is a valid URL string. +// Example: for `icon = "/icon/region.svg"`, value is `/icon/region.svg` and label is `icon`. +func ValidateURL(value interface{}, 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/metadata.go b/provider/metadata.go index 535c700c..b9e0162c 100644 --- a/provider/metadata.go +++ b/provider/metadata.go @@ -2,8 +2,7 @@ package provider import ( "context" - "net/url" - + "github.com/coder/terraform-provider-coder/v2/provider/helpers" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -56,15 +55,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..c70f4270 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -6,7 +6,7 @@ import ( "encoding/hex" "encoding/json" "fmt" - "net/url" + "github.com/coder/terraform-provider-coder/v2/provider/helpers" "os" "regexp" "strconv" @@ -223,15 +223,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 +257,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..c96b70e5 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -227,23 +227,19 @@ func TestParameter(t *testing.T) { data "coder_parameter" "region" { name = "Region" type = "string" - default = "2" + default = "1" option { name = "1" value = "1" icon = "/icon/code.svg" description = "Something!" } - option { - name = "2" - value = "2" - } } `, Check: func(state *terraform.ResourceState) { for key, expected := range map[string]string{ "name": "Region", - "option.#": "2", + "option.#": "1", "option.0.name": "1", "option.0.value": "1", "option.0.icon": "/icon/code.svg", @@ -665,7 +661,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..6657de74 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -2,6 +2,7 @@ package provider import ( "context" + "github.com/coder/terraform-provider-coder/v2/provider/helpers" "net/url" "reflect" "strings" @@ -26,14 +27,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 de3cf097faa71bf1512cfb325e9d4dd39d0999a2 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Tue, 22 Jul 2025 11:14:17 +0000 Subject: [PATCH 2/6] fix: go imports order --- provider/app.go | 3 ++- provider/metadata.go | 4 +++- provider/parameter.go | 3 ++- provider/provider.go | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/provider/app.go b/provider/app.go index 38135c0b..52996a72 100644 --- a/provider/app.go +++ b/provider/app.go @@ -2,13 +2,14 @@ package provider import ( "context" - "github.com/coder/terraform-provider-coder/v2/provider/helpers" "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 ( diff --git a/provider/metadata.go b/provider/metadata.go index b9e0162c..5ed6d478 100644 --- a/provider/metadata.go +++ b/provider/metadata.go @@ -2,11 +2,13 @@ package provider import ( "context" - "github.com/coder/terraform-provider-coder/v2/provider/helpers" + "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 { diff --git a/provider/parameter.go b/provider/parameter.go index c70f4270..2d3ea413 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/coder/terraform-provider-coder/v2/provider/helpers" "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 ( diff --git a/provider/provider.go b/provider/provider.go index 6657de74..a0ef63f9 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -2,7 +2,6 @@ package provider import ( "context" - "github.com/coder/terraform-provider-coder/v2/provider/helpers" "net/url" "reflect" "strings" @@ -11,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 { From e46a0a7427fda655858285dbaeaa7f649a829190 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Tue, 22 Jul 2025 11:39:30 +0000 Subject: [PATCH 3/6] tests: add validation URL tests --- provider/helpers/validation.go | 7 +- provider/helpers/validation_test.go | 163 ++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 provider/helpers/validation_test.go diff --git a/provider/helpers/validation.go b/provider/helpers/validation.go index e532c701..9cc21b89 100644 --- a/provider/helpers/validation.go +++ b/provider/helpers/validation.go @@ -5,15 +5,18 @@ import ( "net/url" ) -// ValidateURL checks that the given value is a valid URL string. +// 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 interface{}, label string) ([]string, []error) { +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..6f9c310d --- /dev/null +++ b/provider/helpers/validation_test.go @@ -0,0 +1,163 @@ +package helpers + +import ( + "strings" + "testing" +) + +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 { + if len(errors) == 0 { + t.Errorf("expected an error but got none") + return + } + + if tt.errorContains != "" { + errorStr := errors[0].Error() + if !strings.Contains(errorStr, tt.errorContains) { + t.Errorf("expected error to contain %q, got %q", tt.errorContains, errorStr) + } + } + } else { + if len(errors) > 0 { + t.Errorf("expected no errors but got: %v", errors) + } + + // Should always return nil for warnings + if warnings != nil { + t.Errorf("expected warnings to be nil, got %v", warnings) + } + } + }) + } +} From 1907d5a3650c2e4c13fe8c282f4ed747dd1c5e53 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Tue, 22 Jul 2025 11:45:11 +0000 Subject: [PATCH 4/6] revert: parameter_test ValidDefaultWithOptions test update --- provider/parameter_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/provider/parameter_test.go b/provider/parameter_test.go index c96b70e5..35f045b9 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -227,19 +227,23 @@ func TestParameter(t *testing.T) { data "coder_parameter" "region" { name = "Region" type = "string" - default = "1" + default = "2" option { name = "1" value = "1" icon = "/icon/code.svg" description = "Something!" } + option { + name = "2" + value = "2" + } } `, Check: func(state *terraform.ResourceState) { for key, expected := range map[string]string{ "name": "Region", - "option.#": "1", + "option.#": "2", "option.0.name": "1", "option.0.value": "1", "option.0.icon": "/icon/code.svg", From 30ad47ccab43196cbeb6328a8574ee3df33f4358 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Tue, 22 Jul 2025 12:08:10 +0000 Subject: [PATCH 5/6] test: update validation_test to use require --- provider/helpers/validation_test.go | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/provider/helpers/validation_test.go b/provider/helpers/validation_test.go index 6f9c310d..9e926a68 100644 --- a/provider/helpers/validation_test.go +++ b/provider/helpers/validation_test.go @@ -1,8 +1,9 @@ package helpers import ( - "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestValidateURL(t *testing.T) { @@ -137,27 +138,17 @@ func TestValidateURL(t *testing.T) { warnings, errors := ValidateURL(tt.value, tt.label) if tt.expectError { - if len(errors) == 0 { - t.Errorf("expected an error but got none") - return - } + require.NotEmpty(t, errors, "expected an error but got none") if tt.errorContains != "" { - errorStr := errors[0].Error() - if !strings.Contains(errorStr, tt.errorContains) { - t.Errorf("expected error to contain %q, got %q", tt.errorContains, errorStr) - } + require.Contains(t, errors[0].Error(), tt.errorContains) } } else { - if len(errors) > 0 { - t.Errorf("expected no errors but got: %v", errors) - } - - // Should always return nil for warnings - if warnings != nil { - t.Errorf("expected warnings to be nil, got %v", warnings) - } + 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, got %v", warnings) }) } } From 484b5b1c89437eb4acf8068c6037229067244bf9 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Tue, 22 Jul 2025 12:11:30 +0000 Subject: [PATCH 6/6] test: update validation_test tests --- provider/helpers/validation_test.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/provider/helpers/validation_test.go b/provider/helpers/validation_test.go index 9e926a68..557bae41 100644 --- a/provider/helpers/validation_test.go +++ b/provider/helpers/validation_test.go @@ -138,17 +138,14 @@ func TestValidateURL(t *testing.T) { warnings, errors := ValidateURL(tt.value, tt.label) if tt.expectError { - require.NotEmpty(t, errors, "expected an error but got none") - - if tt.errorContains != "" { - require.Contains(t, errors[0].Error(), tt.errorContains) - } + 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, got %v", warnings) + require.Nil(t, warnings, "expected warnings to be nil but got: %v", warnings) }) } } 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