diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 4e5d4a5599..cd3fc8f61e 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -911,7 +911,7 @@ TEMPLATE BUILDER OPTIONS: Disable the template builder feature for guided template creation. When disabled, all /api/v2/templatebuilder/* endpoints return 404. - --template-builder-registry-url string, $CODER_TEMPLATE_BUILDER_REGISTRY_URL (default: https://registry.coder.com) + --template-builder-registry-url string, $CODER_TEMPLATE_BUILDER_REGISTRY_URL (default: registry.coder.com) The base URL of the module registry used by the template builder for module source paths. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 0d844b6afd..e713e7d2b2 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -1145,5 +1145,5 @@ templateBuilder: disabled: false # The base URL of the module registry used by the template builder for module # source paths. - # (default: https://registry.coder.com, type: string) - registryURL: https://registry.coder.com + # (default: registry.coder.com, type: string) + registryURL: registry.coder.com diff --git a/coderd/templatebuilder/compose.go b/coderd/templatebuilder/compose.go index b3df18ab2d..2b1699fe39 100644 --- a/coderd/templatebuilder/compose.go +++ b/coderd/templatebuilder/compose.go @@ -244,15 +244,17 @@ func mergeModuleVariables(manifest ModuleManifest, callerVars map[string]string) // missingkey=error surfaces the omission at render time. } - // Overlay validated caller values. + // Overlay validated caller values, converting to HCL literals. for k, val := range callerVars { - merged[k] = val + merged[k] = toHCLLiteral(allowedVars[k], val) } return merged, nil } -// validateVariableValue checks that value is a valid HCL literal for the -// variable's declared type. The literal "null" is accepted for any type. +// validateVariableValue checks that the caller-supplied value is valid for +// the variable's declared type. String values are plain text (not +// pre-quoted); quoting for HCL happens later in toHCLLiteral. +// The literal "null" is accepted for any type. func validateVariableValue(v ModuleVariable, value string) error { if value == "null" { return nil @@ -269,45 +271,55 @@ func validateVariableValue(v ModuleVariable, value string) error { } } -// validateStringValue checks that value is a valid quoted HCL string literal. -// It must start and end with '"', contain no unescaped newlines or quotes, -// and must not contain HCL interpolation/directive markers. +// toHCLLiteral converts a validated caller value into an HCL literal. +// The literal "null" is passed through for any type. Strings are wrapped +// in quotes with interior characters escaped; bools and numbers are +// already valid HCL literals. +func toHCLLiteral(v ModuleVariable, value string) string { + if value == "null" { + return value + } + if v.Type == "string" { + return hclQuote(value) + } + return value +} + +// validateStringValue checks that a raw (unquoted) string value is safe +// to embed in an HCL quoted string. It rejects HCL interpolation/directive +// markers and values that exceed the maximum length. func validateStringValue(value string) error { if len(value) > maxStringValueLen { return xerrors.Errorf("value exceeds maximum length of %d bytes", maxStringValueLen) } - if len(value) < 2 || value[0] != '"' || value[len(value)-1] != '"' { - return xerrors.New("must be a quoted string (e.g. \"value\")") - } - - inner := value[1 : len(value)-1] - - if strings.Contains(inner, "${") || strings.Contains(inner, "%{") { + if strings.Contains(value, "${") || strings.Contains(value, "%{") { return xerrors.New("must not contain HCL interpolation or directive sequences") } + return nil +} - // Walk the inner content to reject unescaped newlines and quotes. - for i := 0; i < len(inner); i++ { - ch := inner[i] - if ch == '\\' { - i++ - if i >= len(inner) { - // Trailing backslash with no character to escape. - // In HCL this would escape the closing quote delimiter, - // producing an unterminated string. - return xerrors.New("must not end with a trailing backslash") - } - continue - } - if ch == '"' { - return xerrors.New("must not contain unescaped quotes") - } - if ch == '\n' || ch == '\r' { - return xerrors.New("must not contain unescaped newlines") +// hclQuote wraps a raw string in HCL double-quotes, escaping backslashes, +// double-quotes, and newlines so the result is a valid HCL string literal. +func hclQuote(s string) string { + var b strings.Builder + b.Grow(len(s) + 2) + _, _ = b.WriteRune('"') + for i := 0; i < len(s); i++ { + switch s[i] { + case '\\': + _, _ = b.WriteString("\\\\") + case '"': + _, _ = b.WriteString("\\\"") + case '\n': + _, _ = b.WriteString("\\n") + case '\r': + _, _ = b.WriteString("\\r") + default: + _ = b.WriteByte(s[i]) } } - - return nil + _, _ = b.WriteRune('"') + return b.String() } // validateNumberValue checks that value is a valid HCL number literal. diff --git a/coderd/templatebuilder/compose_internal_test.go b/coderd/templatebuilder/compose_internal_test.go index 9d56286572..4a919aefdf 100644 --- a/coderd/templatebuilder/compose_internal_test.go +++ b/coderd/templatebuilder/compose_internal_test.go @@ -97,7 +97,7 @@ func TestMergeModuleVariables(t *testing.T) { t.Run("CallerProvidesRequired", func(t *testing.T) { t.Parallel() merged, err := mergeModuleVariables(manifest, map[string]string{ - "required_no_default": `"value"`, + "required_no_default": "value", }) require.NoError(t, err) require.Equal(t, `"value"`, merged["required_no_default"]) @@ -153,11 +153,11 @@ func TestMergeModuleVariables(t *testing.T) { t.Run("InvalidStringValueRejected", func(t *testing.T) { t.Parallel() _, err := mergeModuleVariables(manifest, map[string]string{ - "optional_no_default": "unquoted", + "optional_no_default": "${var.foo}", }) require.Error(t, err) require.Contains(t, err.Error(), `variable "optional_no_default"`) - require.Contains(t, err.Error(), "quoted string") + require.Contains(t, err.Error(), "interpolation") }) t.Run("NullAcceptedForAnyType", func(t *testing.T) { @@ -189,28 +189,17 @@ func TestValidateStringValue(t *testing.T) { value string wantErr string }{ - {"ValidEmpty", `""`, ""}, - {"ValidSimple", `"hello"`, ""}, - {"ValidPath", `"/home/coder"`, ""}, - {"ValidURL", `"https://github.com/coder/coder"`, ""}, - {"ValidLiteralBackslashN", `"line\\nbreak"`, ""}, - {"ValidEscapedQuote", `"say \"hi\""`, ""}, - {"ValidEscapedBackslash", `"path\\to\\file"`, ""}, + {"ValidEmpty", "", ""}, + {"ValidSimple", "hello", ""}, + {"ValidPath", "/home/coder", ""}, + {"ValidURL", "https://github.com/coder/coder", ""}, + {"ValidWithQuotes", `say "hi"`, ""}, + {"ValidWithNewlines", "line\nbreak", ""}, + {"ValidWithBackslash", `path\to\file`, ""}, - {"RejectedUnquoted", "hello", "quoted string"}, - {"RejectedMissingOpenQuote", `hello"`, "quoted string"}, - {"RejectedMissingCloseQuote", `"hello`, "quoted string"}, - {"RejectedEmpty", "", "quoted string"}, - {"RejectedSingleChar", `"`, "quoted string"}, - {"RejectedUnescapedNewline", "\"line\nbreak\"", "unescaped newlines"}, - {"RejectedCarriageReturn", "\"line\rbreak\"", "unescaped newlines"}, - {"RejectedUnescapedQuote", `"say "hi""`, "unescaped quotes"}, - {"RejectedHCLInterpolation", `"${var.foo}"`, "interpolation"}, - {"RejectedHCLDirective", `"%{if true}yes%{endif}"`, "interpolation"}, - {"RejectedOverlong", `"` + strings.Repeat("a", maxStringValueLen) + `"`, "maximum length"}, - {"RejectedTrailingBackslash", `"test\"`, "trailing backslash"}, - {"RejectedTrailingBackslashOnly", `"\"`, "trailing backslash"}, - {"ValidEvenTrailingBackslashes", `"test\\\\"`, ""}, + {"RejectedHCLInterpolation", "${var.foo}", "interpolation"}, + {"RejectedHCLDirective", "%{if true}yes%{endif}", "interpolation"}, + {"RejectedOverlong", strings.Repeat("a", maxStringValueLen+1), "maximum length"}, } for _, tc := range tests { @@ -313,7 +302,7 @@ func TestValidateVariableValue(t *testing.T) { t.Run("UnsupportedTypeRejected", func(t *testing.T) { t.Parallel() v := ModuleVariable{Name: "test", Type: "list"} - err := validateVariableValue(v, `"val"`) + err := validateVariableValue(v, "val") require.Error(t, err) require.Contains(t, err.Error(), "unsupported variable type") }) diff --git a/coderd/templatebuilder/compose_test.go b/coderd/templatebuilder/compose_test.go index cbacf73657..c718f94b6d 100644 --- a/coderd/templatebuilder/compose_test.go +++ b/coderd/templatebuilder/compose_test.go @@ -93,7 +93,7 @@ func TestCompose(t *testing.T) { { ID: "git-clone", Variables: map[string]string{ - "url": `"https://github.com/coder/coder"`, + "url": "https://github.com/coder/coder", }, }, }, @@ -178,7 +178,7 @@ func TestCompose(t *testing.T) { { ID: "code-server", Variables: map[string]string{ - "nonexistent_var": `"value"`, + "nonexistent_var": "value", }, }, }, @@ -216,7 +216,7 @@ func TestCompose(t *testing.T) { { ID: "code-server", Variables: map[string]string{ - "folder": `"${var.evil}"`, + "folder": "${var.evil}", }, }, }, diff --git a/coderd/templatebuilder/modules/aider/aider.tf.tmpl b/coderd/templatebuilder/modules/aider/aider.tf.tmpl index 47a9b4cf8d..98ad15383b 100644 --- a/coderd/templatebuilder/modules/aider/aider.tf.tmpl +++ b/coderd/templatebuilder/modules/aider/aider.tf.tmpl @@ -2,6 +2,7 @@ variable "api_key" { description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider." type = string + default = "" sensitive = true } module "aider" { diff --git a/coderd/templatebuilder/modules/amazon-q/amazon-q.tf.tmpl b/coderd/templatebuilder/modules/amazon-q/amazon-q.tf.tmpl index 70c8ed0538..55c55bc564 100644 --- a/coderd/templatebuilder/modules/amazon-q/amazon-q.tf.tmpl +++ b/coderd/templatebuilder/modules/amazon-q/amazon-q.tf.tmpl @@ -2,6 +2,7 @@ variable "auth_tarball" { description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/amazon-q directory." type = string + default = "" sensitive = true } module "amazon-q" { diff --git a/coderd/templatebuilder/modules/claude-code/claude-code.tf.tmpl b/coderd/templatebuilder/modules/claude-code/claude-code.tf.tmpl index c5fb99c988..374885193a 100644 --- a/coderd/templatebuilder/modules/claude-code/claude-code.tf.tmpl +++ b/coderd/templatebuilder/modules/claude-code/claude-code.tf.tmpl @@ -2,6 +2,7 @@ variable "claude_code_oauth_token" { description = "OAuth token passed to Claude Code via the CLAUDE_CODE_OAUTH_TOKEN env var. Generate one with `claude setup-token`." type = string + default = "" sensitive = true } module "claude-code" { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 392270235b..8500dbac0c 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -4776,7 +4776,7 @@ Write out the current server config as YAML to stdout.`, Flag: "template-builder-registry-url", Env: "CODER_TEMPLATE_BUILDER_REGISTRY_URL", Value: &c.TemplateBuilder.RegistryURL, - Default: "https://registry.coder.com", + Default: "registry.coder.com", Group: &deploymentGroupTemplateBuilder, YAML: "registryURL", }, diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index 9cd918b0fa..33b3cec7d2 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -2129,6 +2129,6 @@ Disable the template builder feature for guided template creation. When disabled | Type | string | | Environment | $CODER_TEMPLATE_BUILDER_REGISTRY_URL | | YAML | templateBuilder.registryURL | -| Default | https://registry.coder.com | +| Default | registry.coder.com | The base URL of the module registry used by the template builder for module source paths. diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index e88121f794..9d2d5b6cc5 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -912,7 +912,7 @@ TEMPLATE BUILDER OPTIONS: Disable the template builder feature for guided template creation. When disabled, all /api/v2/templatebuilder/* endpoints return 404. - --template-builder-registry-url string, $CODER_TEMPLATE_BUILDER_REGISTRY_URL (default: https://registry.coder.com) + --template-builder-registry-url string, $CODER_TEMPLATE_BUILDER_REGISTRY_URL (default: registry.coder.com) The base URL of the module registry used by the template builder for module source paths. diff --git a/scripts/templatebuildermodulegen/write.go b/scripts/templatebuildermodulegen/write.go index ca0704629c..2638e674db 100644 --- a/scripts/templatebuildermodulegen/write.go +++ b/scripts/templatebuildermodulegen/write.go @@ -27,6 +27,7 @@ variable "{{ .Name }}" { description = "{{ .Description }}" type = {{ .Type }} sensitive = true + default = "" } {{ end -}} module "{{ .ID }}" {