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 }}" {