diff --git a/docs/data-sources/secret.md b/docs/data-sources/secret.md deleted file mode 100644 index 36ee85c1..00000000 --- a/docs/data-sources/secret.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "coder_secret Data Source - terraform-provider-coder" -subcategory: "" -description: |- - Use this data source to declare that a workspace requires a user secret. Each coder_secret block declares a single secret requirement, matched by either an environment variable name (env) or a file path (file). The resolved value is available at build time via data.coder_secret..value. ---- - -# coder_secret (Data Source) - -Use this data source to declare that a workspace requires a user secret. Each `coder_secret` block declares a single secret requirement, matched by either an environment variable name (`env`) or a file path (`file`). The resolved value is available at build time via `data.coder_secret..value`. - -## Example Usage - -```terraform -data "coder_secret" "my_token" { - env = "MY_TOKEN" - help_message = "Personal access token injected as the environment variable MY_TOKEN" -} - -data "coder_secret" "my_cert" { - file = "~/my-cert.pem" - help_message = "Certificate chain injected as the file ~/my-cert.pem" -} - -# Use the secret value in an agent startup script. -resource "coder_script" "setup" { - agent_id = coder_agent.main.id - script = "echo ${data.coder_secret.my_token.value}" -} -``` - - -## Schema - -### Required - -- `help_message` (String) Guidance shown to users when this secret requirement is not satisfied. Displayed on the create workspace page and in build failure logs. - -### Optional - -- `env` (String) The environment variable name that this secret must inject (e.g. "MY_TOKEN"). Must be POSIX-compliant: start with a letter or underscore, followed by letters, digits, or underscores. Exactly one of `env` or `file` must be set. -- `file` (String) The file path that this secret must inject (e.g. "~/my-token"). Must start with `~/` or `/`. Exactly one of `env` or `file` must be set. - -### Read-Only - -- `id` (String) The ID of this resource. -- `value` (String, Sensitive) The resolved secret value, populated from the user's stored secrets during workspace builds. Treated as missing if empty. diff --git a/examples/data-sources/coder_secret/data-source.tf b/examples/data-sources/coder_secret/data-source.tf deleted file mode 100644 index d8304fe2..00000000 --- a/examples/data-sources/coder_secret/data-source.tf +++ /dev/null @@ -1,15 +0,0 @@ -data "coder_secret" "my_token" { - env = "MY_TOKEN" - help_message = "Personal access token injected as the environment variable MY_TOKEN" -} - -data "coder_secret" "my_cert" { - file = "~/my-cert.pem" - help_message = "Certificate chain injected as the file ~/my-cert.pem" -} - -# Use the secret value in an agent startup script. -resource "coder_script" "setup" { - agent_id = coder_agent.main.id - script = "echo ${data.coder_secret.my_token.value}" -} diff --git a/provider/provider.go b/provider/provider.go index 9346984c..7e4451b8 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -65,7 +65,6 @@ func New() *schema.Provider { "coder_workspace_owner": workspaceOwnerDataSource(), "coder_workspace_preset": workspacePresetDataSource(), "coder_task": taskDatasource(), - "coder_secret": secretDataSource(), }, ResourcesMap: map[string]*schema.Resource{ "coder_agent": agentResource(), diff --git a/provider/secret.go b/provider/secret.go deleted file mode 100644 index b40b76b1..00000000 --- a/provider/secret.go +++ /dev/null @@ -1,194 +0,0 @@ -package provider - -import ( - "context" - "encoding/hex" - "fmt" - "os" - "regexp" - "strings" - - "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" -) - -// posixEnvNameRegex matches a POSIX-compliant environment variable name: -// starts with a letter or underscore, followed by letters, digits, or -// underscores. This mirrors the rule enforced by coderd when secrets are -// created, so enforcing it in the provider catches typos at terraform -// validate/plan time rather than at build time. -var posixEnvNameRegex = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) - -// validateSecretEnv rejects env names that can never match a stored secret. -// Empty values pass through: the env/file mutex check in ReadContext handles -// that case and produces a clearer error. -func validateSecretEnv(val any, _ cty.Path) diag.Diagnostics { - s, ok := val.(string) - if !ok { - return diag.Errorf("expected string, got %T", val) - } - if s == "" { - return nil - } - if !posixEnvNameRegex.MatchString(s) { - return diag.Errorf( - "`env` must be a POSIX-compliant identifier matching %q; got %q", - posixEnvNameRegex.String(), s) - } - return nil -} - -// validateSecretFile rejects file paths that are not absolute or home-relative. -// This mirrors the rule enforced by coderd when secrets are created/updated -// (paths must start with `~/` or `/`), so enforcing it in the provider catches -// mistakes at terraform validate/plan time rather than at build time. -func validateSecretFile(val any, _ cty.Path) diag.Diagnostics { - s, ok := val.(string) - if !ok { - return diag.Errorf("expected string, got %T", val) - } - if s == "" { - return nil - } - if !strings.HasPrefix(s, "/") && !strings.HasPrefix(s, "~/") { - return diag.Errorf( - "`file` must start with `/` or `~/`; got %q", s) - } - return nil -} - -// secretDataSource returns a schema for a user secret data source. -func secretDataSource() *schema.Resource { - const valueKey = "value" - - return &schema.Resource{ - SchemaVersion: 1, - - Description: "Use this data source to declare that a workspace requires a user secret. " + - "Each `coder_secret` block declares a single secret requirement, matched by either " + - "an environment variable name (`env`) or a file path (`file`). The resolved value " + - "is available at build time via `data.coder_secret..value`.", - ReadContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { - env := rd.Get("env").(string) - file := rd.Get("file").(string) - - if env == "" && file == "" { - return diag.Errorf("exactly one of `env` or `file` must be set") - } - if env != "" && file != "" { - return diag.Errorf("exactly one of `env` or `file` must be set") - } - - // Build a stable ID from whichever field is set. - if env != "" { - rd.SetId(fmt.Sprintf("env:%s", env)) - } else { - rd.SetId(fmt.Sprintf("file:%s", file)) - } - - // Look up the secret value from the environment variable - // set by the provisioner at build time. - var value string - if env != "" { - value = helpers.OptionalEnv(SecretEnvEnvironmentVariable(env)) - } else { - value = helpers.OptionalEnv(SecretFileEnvironmentVariable(file)) - } - - if value != "" { - // Happy path where secret is resolved. - _ = rd.Set(valueKey, value) - return nil - } - - // Note that an value is treated as missing. The provider cannot - // distinguish "user has not stored the secret" from "user stored - // an empty value", because both surface as an unset or empty - // CODER_SECRET_* env var. This means a user must have a non-empty - // secret value to satisfy a requirement. - - // Only enforce missing secrets when we are certain this is a - // workspace start build. We check both conditions: - // 1. CODER_WORKSPACE_BUILD_ID is set (real build, not local - // terraform plan) - // 2. CODER_WORKSPACE_TRANSITION is "start" - // In all other cases (stop, delete, local dev, ambiguous state) - // we return an empty value so the operation can proceed. This - // prevents a missing or deleted secret from making a workspace - // unstoppable or undeletable. - buildID := os.Getenv("CODER_WORKSPACE_BUILD_ID") - transition := os.Getenv("CODER_WORKSPACE_TRANSITION") - workspaceStartBuild := buildID != "" && transition == "start" - if !workspaceStartBuild { - _ = rd.Set(valueKey, value) - return nil - } - - var requirement string - if env != "" { - requirement = fmt.Sprintf("environment variable %q", env) - } else { - requirement = fmt.Sprintf("file %q", file) - } - - var detail strings.Builder - _, _ = fmt.Fprintf(&detail, "Required: %s\n\n", requirement) - if helpMessage := rd.Get("help_message").(string); helpMessage != "" { - _, _ = fmt.Fprintf(&detail, "Help message: %s\n\n", helpMessage) - } - _, _ = fmt.Fprintf(&detail, "To resolve: ensure a secret exposes the %s.\n", requirement) - - return diag.Diagnostics{{ - Severity: diag.Error, - Summary: fmt.Sprintf("Missing required secret: %s", requirement), - Detail: detail.String(), - }} - }, - Schema: map[string]*schema.Schema{ - "env": { - Type: schema.TypeString, - Description: "The environment variable name that this secret must inject (e.g. \"MY_TOKEN\"). Must be POSIX-compliant: start with a letter or underscore, followed by letters, digits, or underscores. Exactly one of `env` or `file` must be set.", - Optional: true, - ForceNew: true, - ValidateDiagFunc: validateSecretEnv, - }, - "file": { - Type: schema.TypeString, - Description: "The file path that this secret must inject (e.g. \"~/my-token\"). Must start with `~/` or `/`. Exactly one of `env` or `file` must be set.", - Optional: true, - ForceNew: true, - ValidateDiagFunc: validateSecretFile, - }, - "help_message": { - Type: schema.TypeString, - Description: "Guidance shown to users when this secret requirement is not satisfied. Displayed on the create workspace page and in build failure logs.", - Required: true, - }, - "value": { - Type: schema.TypeString, - Description: "The resolved secret value, populated from the user's stored secrets during workspace builds. Treated as missing if empty.", - Computed: true, - Sensitive: true, - }, - }, - } -} - -// SecretEnvEnvironmentVariable returns the environment variable used -// to pass a user secret matched by env_name to Terraform during -// workspace builds. The env name is used directly and assumed to be -// POSIX-compliant. -func SecretEnvEnvironmentVariable(envName string) string { - return fmt.Sprintf("CODER_SECRET_ENV_%s", envName) -} - -// SecretFileEnvironmentVariable returns the environment variable used -// to pass a user secret matched by file_path to Terraform during -// workspace builds. The file path is hex-encoded because it contains -// characters invalid in environment variable names. -func SecretFileEnvironmentVariable(filePath string) string { - return fmt.Sprintf("CODER_SECRET_FILE_%s", hex.EncodeToString([]byte(filePath))) -} diff --git a/provider/secret_test.go b/provider/secret_test.go deleted file mode 100644 index 68169dd4..00000000 --- a/provider/secret_test.go +++ /dev/null @@ -1,503 +0,0 @@ -package provider_test - -import ( - "encoding/hex" - "fmt" - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "github.com/stretchr/testify/require" - - "github.com/coder/terraform-provider-coder/v2/provider" -) - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretByEnv(t *testing.T) { - t.Setenv("CODER_WORKSPACE_TRANSITION", "stop") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "github_token" { - env = "GITHUB_TOKEN" - help_message = "Add a GitHub PAT as a secret with env=GITHUB_TOKEN" - } - `, - Check: func(state *terraform.State) error { - require.Len(t, state.Modules, 1) - require.Len(t, state.Modules[0].Resources, 1) - res := state.Modules[0].Resources["data.coder_secret.github_token"] - require.NotNil(t, res) - - attribs := res.Primary.Attributes - require.Equal(t, "env:GITHUB_TOKEN", attribs["id"]) - require.Equal(t, "GITHUB_TOKEN", attribs["env"]) - require.Equal(t, "", attribs["value"]) - - return nil - }, - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretByFile(t *testing.T) { - t.Setenv("CODER_WORKSPACE_TRANSITION", "stop") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "aws_creds" { - file = "~/.aws/credentials" - help_message = "Add your AWS credentials file as a secret" - } - `, - Check: func(state *terraform.State) error { - require.Len(t, state.Modules, 1) - require.Len(t, state.Modules[0].Resources, 1) - res := state.Modules[0].Resources["data.coder_secret.aws_creds"] - require.NotNil(t, res) - - attribs := res.Primary.Attributes - require.Equal(t, "file:~/.aws/credentials", attribs["id"]) - require.Equal(t, "~/.aws/credentials", attribs["file"]) - require.Equal(t, "", attribs["value"]) - - return nil - }, - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretWithEnvValue(t *testing.T) { - t.Setenv(provider.SecretEnvEnvironmentVariable("MY_TOKEN"), "secret-token-value") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "my_token" { - env = "MY_TOKEN" - help_message = "Set the MY_TOKEN secret" - } - `, - Check: func(state *terraform.State) error { - require.Len(t, state.Modules, 1) - require.Len(t, state.Modules[0].Resources, 1) - res := state.Modules[0].Resources["data.coder_secret.my_token"] - require.NotNil(t, res) - - attribs := res.Primary.Attributes - require.Equal(t, "secret-token-value", attribs["value"]) - - return nil - }, - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretWithFileValue(t *testing.T) { - t.Setenv(provider.SecretFileEnvironmentVariable("~/.ssh/id_rsa"), "private-key-contents") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "ssh_key" { - file = "~/.ssh/id_rsa" - help_message = "Add your SSH private key" - } - `, - Check: func(state *terraform.State) error { - require.Len(t, state.Modules, 1) - require.Len(t, state.Modules[0].Resources, 1) - res := state.Modules[0].Resources["data.coder_secret.ssh_key"] - require.NotNil(t, res) - - attribs := res.Primary.Attributes - require.Equal(t, "private-key-contents", attribs["value"]) - - return nil - }, - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretMissingOnStart(t *testing.T) { - // Default transition is "start", and no env var is set for the - // secret, so the data source should fail. - t.Setenv("CODER_WORKSPACE_TRANSITION", "start") - t.Setenv("CODER_WORKSPACE_BUILD_ID", "test-build-id") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "missing" { - env = "DOES_NOT_EXIST" - help_message = "Please add the DOES_NOT_EXIST secret" - } - `, - // Assert the full labeled-section format so refactors that - // drop the summary, the "Required:" paragraph, the echoed - // help_message, or the "To resolve:" action are caught. - // The last line uses \s+ instead of a literal space because - // Terraform soft-wraps long diagnostic lines at ~76 cols. - ExpectError: regexp.MustCompile( - `Missing required secret: environment variable "DOES_NOT_EXIST"[\s\S]*` + - `Required: environment variable "DOES_NOT_EXIST"[\s\S]*` + - `Help message: Please add the DOES_NOT_EXIST secret[\s\S]*` + - `To resolve: ensure a secret exposes the environment\s+variable\s+"DOES_NOT_EXIST"`, - ), - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretMissingOnStartFile(t *testing.T) { - // Missing file-path secret on start should fail with a file-flavored - // diagnostic. Mirrors TestSecretMissingOnStart but covers the `file` - // branch of the requirement builder. - t.Setenv("CODER_WORKSPACE_TRANSITION", "start") - t.Setenv("CODER_WORKSPACE_BUILD_ID", "test-build-id") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "missing" { - file = "~/.missing/secret" - help_message = "Please add the ~/.missing/secret secret" - } - `, - ExpectError: regexp.MustCompile( - `Missing required secret: file "~/.missing/secret"[\s\S]*` + - `Required: file "~/.missing/secret"[\s\S]*` + - `Help message: Please add the ~/.missing/secret secret[\s\S]*` + - `To resolve: ensure a secret exposes the file "~/.missing/secret"`, - ), - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretMissingOnStartEmptyHelp(t *testing.T) { - // When help_message is empty the diagnostic should omit the - // "Help message:" paragraph entirely rather than render a blank one. - // help_message is schema-required but HCL validates presence, not - // non-emptiness, so `help_message = ""` is legal and must be handled. - t.Setenv("CODER_WORKSPACE_TRANSITION", "start") - t.Setenv("CODER_WORKSPACE_BUILD_ID", "test-build-id") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "missing" { - env = "DOES_NOT_EXIST" - help_message = "" - } - `, - // Require that "To resolve:" immediately follows "Required:" - // with only blank lines between — no "Help message:" line. - // Go's regexp lacks lookaheads, so this adjacency check is - // how we assert absence. - ExpectError: regexp.MustCompile( - `Required: environment variable "DOES_NOT_EXIST"\s*\n\s*\n\s*To resolve:`, - ), - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretMissingOnLocalPlan(t *testing.T) { - // A local `terraform plan` without a workspace build id must not - // hard-fail on a missing secret. Only real workspace start builds - // (transition == "start" AND CODER_WORKSPACE_BUILD_ID set) should. - t.Setenv("CODER_WORKSPACE_TRANSITION", "start") - // Explicitly clear BUILD_ID in case the surrounding environment - // has it set. - t.Setenv("CODER_WORKSPACE_BUILD_ID", "") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "missing" { - env = "DOES_NOT_EXIST" - help_message = "irrelevant" - } - `, - Check: func(state *terraform.State) error { - res := state.Modules[0].Resources["data.coder_secret.missing"] - require.NotNil(t, res) - require.Equal(t, "", res.Primary.Attributes["value"]) - return nil - }, - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretMissingOnStop(t *testing.T) { - // On stop transitions, missing secrets should not error. - t.Setenv("CODER_WORKSPACE_TRANSITION", "stop") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "missing" { - env = "DOES_NOT_EXIST" - help_message = "Please add the DOES_NOT_EXIST secret" - } - `, - Check: func(state *terraform.State) error { - res := state.Modules[0].Resources["data.coder_secret.missing"] - require.NotNil(t, res) - require.Equal(t, "", res.Primary.Attributes["value"]) - return nil - }, - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretBothEnvAndFile(t *testing.T) { - t.Setenv("CODER_WORKSPACE_TRANSITION", "stop") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "both" { - env = "MY_SECRET" - file = "~/.my-secret" - help_message = "Pick one" - } - `, - ExpectError: regexp.MustCompile("exactly one of `env` or `file` must be set"), - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretNeitherEnvNorFile(t *testing.T) { - // Both `env` and `file` are optional in schema but the ReadContext - // enforces that exactly one must be set. Covers the `env == "" && - // file == ""` branch. - t.Setenv("CODER_WORKSPACE_TRANSITION", "stop") - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: ` - provider "coder" { - } - data "coder_secret" "neither" { - help_message = "Pick one" - } - `, - ExpectError: regexp.MustCompile("exactly one of `env` or `file` must be set"), - }}, - }) -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretEnvInvalid(t *testing.T) { - // Schema-level validation rejects non-POSIX env names at plan time, - // before ReadContext runs. Each subtest pins one specific rejected - // shape (leading digit, hyphen, space, dot) to guard against the - // regex drifting in a way that silently accepts one of these. - t.Setenv("CODER_WORKSPACE_TRANSITION", "stop") - cases := []struct { - name string - value string - }{ - {name: "LeadingDigit", value: "1TOKEN"}, - {name: "Hyphen", value: "MY-TOKEN"}, - {name: "Space", value: "MY TOKEN"}, - {name: "Dot", value: "MY.TOKEN"}, - {name: "LeadingHyphen", value: "-TOKEN"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: fmt.Sprintf(` - provider "coder" { - } - data "coder_secret" "invalid" { - env = %q - help_message = "ignored" - } - `, tc.value), - // "POSIX-compliant" is the only substring guaranteed to - // appear on the same rendered line as the error - // headline. Terraform soft-wraps diagnostics at ~76 - // cols, which can split the regex source and the value. - ExpectError: regexp.MustCompile("POSIX-compliant"), - }}, - }) - }) - } -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretEnvValid(t *testing.T) { - // POSIX-valid env names must pass the validator. The happy path is - // already covered by TestSecretByEnv; these cases exercise the - // less-common shapes the regex permits (leading underscore, digits - // after the first character, all-lowercase) to guard against the - // validator being tightened by accident. - t.Setenv("CODER_WORKSPACE_TRANSITION", "stop") - cases := []struct { - name string - value string - }{ - {name: "LeadingUnderscore", value: "_TOKEN"}, - {name: "TrailingDigit", value: "TOKEN1"}, - {name: "AllLowercase", value: "my_token"}, - {name: "MixedCase", value: "MyToken"}, - {name: "SingleLetter", value: "X"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: fmt.Sprintf(` - provider "coder" { - } - data "coder_secret" "valid" { - env = %q - help_message = "ignored" - } - `, tc.value), - }}, - }) - }) - } -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretFileInvalid(t *testing.T) { - // Schema-level validation rejects non-absolute file paths at plan - // time. The provisioner writes secrets using the path verbatim - // (after `~/` expansion), so a relative path would land somewhere - // surprising in the agent filesystem. - t.Setenv("CODER_WORKSPACE_TRANSITION", "stop") - cases := []struct { - name string - value string - }{ - {name: "Relative", value: "creds.txt"}, - {name: "RelativeDir", value: "config/creds"}, - {name: "DotRelative", value: "./creds"}, - {name: "ParentRelative", value: "../creds"}, - {name: "BareTilde", value: "~creds"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: fmt.Sprintf(` - provider "coder" { - } - data "coder_secret" "invalid" { - file = %q - help_message = "ignored" - } - `, tc.value), - // Soft-wrapping by Terraform can split across lines, so - // match the stable prefix only. - ExpectError: regexp.MustCompile("`file` must start with"), - }}, - }) - }) - } -} - -// nolint:paralleltest // t.Setenv is incompatible with t.Parallel. -func TestSecretFileValid(t *testing.T) { - // Absolute and home-relative paths must pass the validator. Covers - // shapes beyond the `~/.aws/credentials` case in TestSecretByFile. - t.Setenv("CODER_WORKSPACE_TRANSITION", "stop") - cases := []struct { - name string - value string - }{ - {name: "HomeDotfile", value: "~/.netrc"}, - {name: "HomeNested", value: "~/config/app/secret"}, - {name: "AbsoluteRoot", value: "/etc/creds"}, - {name: "AbsoluteNested", value: "/var/lib/secrets/token"}, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProviderFactories: coderFactory(), - IsUnitTest: true, - Steps: []resource.TestStep{{ - Config: fmt.Sprintf(` - provider "coder" { - } - data "coder_secret" "valid" { - file = %q - help_message = "ignored" - } - `, tc.value), - }}, - }) - }) - } -} - -func TestSecretEnvironmentVariables(t *testing.T) { - t.Parallel() - - t.Run("EnvSecret", func(t *testing.T) { - t.Parallel() - result := provider.SecretEnvEnvironmentVariable("GITHUB_TOKEN") - require.Equal(t, "CODER_SECRET_ENV_GITHUB_TOKEN", result) - }) - - t.Run("FileSecret", func(t *testing.T) { - t.Parallel() - filePath := "~/.aws/credentials" - result := provider.SecretFileEnvironmentVariable(filePath) - expected := fmt.Sprintf("CODER_SECRET_FILE_%s", hex.EncodeToString([]byte(filePath))) - require.Equal(t, expected, result) - }) -}