fix: render base variables into templates instead of tfvars (DEVEX-287) (#26436)

Part of the Template Builder wizard PR stack.

## Problem

The kubernetes base template used Terraform `variable` blocks and
`var.*` references for `use_kubeconfig` and `namespace`, but the
composed tar bundle never included a `.tfvars` file. This caused
`terraform plan` to fail with "required template variables need values:
namespace".

## Fix

Base templates now use Go template variables (`{{ .Variables.* }}`) just
like module templates do. Values are validated, HCL-quoted, and rendered
directly into the output HCL.

Also adds explicit "variable is required" validation to both
`mergeBaseVariables` and `mergeModuleVariables`, replacing the previous
reliance on `missingkey=error` at render time for clearer error
messages.

---
> [!NOTE]
> Generated by Coder Agents on behalf of @jeremyruppel
This commit is contained in:
Jeremy Ruppel
2026-06-23 11:26:38 -04:00
committed by GitHub
parent 2f6f8b9520
commit 7ea5d48296
7 changed files with 126 additions and 61 deletions
+75 -1
View File
@@ -104,7 +104,13 @@ func renderBase(baseTemplateID string, baseVars map[string]string) ([]byte, erro
if renderCtx.Variables == nil {
renderCtx.Variables = make(map[string]string)
}
maps.Copy(renderCtx.Variables, baseVars)
vars, err := mergeBaseVariables(baseTemplateID, baseVars)
if err != nil {
return nil, xerrors.Errorf("base %q: %w", baseTemplateID, err)
}
maps.Copy(renderCtx.Variables, vars)
mainTF, err := RenderBaseTemplate(baseTemplateID, "main.tf.tmpl", renderCtx)
if err != nil {
return nil, xerrors.Errorf("render base template: %w", err)
@@ -112,6 +118,63 @@ func renderBase(baseTemplateID string, baseVars map[string]string) ([]byte, erro
return mainTF, nil
}
// mergeBaseVariables builds the final Variables map for a base template.
// It starts with manifest defaults, overlays caller-supplied values,
// validates types, and converts to HCL literals.
func mergeBaseVariables(baseTemplateID string, callerVars map[string]string) (map[string]string, error) {
allVars := BaseVariables(baseTemplateID)
if len(allVars) == 0 && len(callerVars) == 0 {
return make(map[string]string), nil
}
allowedVars := make(map[string]ModuleVariable, len(allVars))
for _, v := range allVars {
if v.Computed || v.Sensitive {
continue
}
allowedVars[v.Name] = v
}
// Validate caller-supplied keys and values.
for k, val := range callerVars {
v, ok := allowedVars[k]
if !ok {
return nil, xerrors.Errorf("unknown variable %q", k)
}
if err := validateVariableValue(v, val); err != nil {
return nil, xerrors.Errorf("variable %q: %w", k, err)
}
}
// Build merged map from manifest defaults.
merged := make(map[string]string, len(allVars))
for _, v := range allVars {
if v.Computed || v.Sensitive {
continue
}
if len(v.Default) > 0 && isSimpleJSONValue(v.Default) {
merged[v.Name] = string(v.Default)
}
}
// Overlay validated caller values, converting to HCL literals.
for k, val := range callerVars {
merged[k] = toHCLLiteral(allowedVars[k], val)
}
// Ensure all required variables without defaults have a value.
for _, v := range allVars {
if v.Computed || v.Sensitive {
continue
}
if v.Required && merged[v.Name] == "" {
return nil, xerrors.Errorf("variable %q is required", v.Name)
}
}
return merged, nil
}
// loadCatalogMap loads the module catalog and returns it as a map keyed
// by module ID.
func loadCatalogMap() (map[string]ModuleManifest, error) {
@@ -248,6 +311,17 @@ func mergeModuleVariables(manifest ModuleManifest, callerVars map[string]string)
for k, val := range callerVars {
merged[k] = toHCLLiteral(allowedVars[k], val)
}
// Ensure all required variables without defaults have a value.
for _, v := range manifest.Variables {
if v.Computed || v.Sensitive {
continue
}
if v.Required && merged[v.Name] == "" {
return nil, xerrors.Errorf("variable %q is required", v.Name)
}
}
return merged, nil
}