feat: include template variables in dynamic parameter rendering (#18819)

Closes https://github.com/coder/coder/issues/18671

Template variables now loaded into dynamic parameters.
This commit is contained in:
Steven Masley
2025-07-21 13:02:31 -05:00
committed by GitHub
parent 40a6367d4b
commit aedc019b4e
11 changed files with 327 additions and 79 deletions
+27 -4
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/zclconf/go-cty/cty"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/apiversion"
@@ -41,9 +42,10 @@ type loader struct {
templateVersionID uuid.UUID
// cache of objects
templateVersion *database.TemplateVersion
job *database.ProvisionerJob
terraformValues *database.TemplateVersionTerraformValue
templateVersion *database.TemplateVersion
job *database.ProvisionerJob
terraformValues *database.TemplateVersionTerraformValue
templateVariableValues *[]database.TemplateVersionVariable
}
// Prepare is the entrypoint for this package. It loads the necessary objects &
@@ -61,6 +63,12 @@ func Prepare(ctx context.Context, db database.Store, cache files.FileAcquirer, v
return l.Renderer(ctx, db, cache)
}
func WithTemplateVariableValues(vals []database.TemplateVersionVariable) func(r *loader) {
return func(r *loader) {
r.templateVariableValues = &vals
}
}
func WithTemplateVersion(tv database.TemplateVersion) func(r *loader) {
return func(r *loader) {
if tv.ID == r.templateVersionID {
@@ -127,6 +135,14 @@ func (r *loader) loadData(ctx context.Context, db database.Store) error {
r.terraformValues = &values
}
if r.templateVariableValues == nil {
vals, err := db.GetTemplateVersionVariables(ctx, r.templateVersion.ID)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return xerrors.Errorf("template version variables: %w", err)
}
r.templateVariableValues = &vals
}
return nil
}
@@ -160,13 +176,17 @@ func (r *loader) dynamicRenderer(ctx context.Context, db database.Store, cache *
}
}()
tfVarValues, err := VariableValues(*r.templateVariableValues)
if err != nil {
return nil, xerrors.Errorf("parse variable values: %w", err)
}
// If they can read the template version, then they can read the file for
// parameter loading purposes.
//nolint:gocritic
fileCtx := dbauthz.AsFileReader(ctx)
var templateFS fs.FS
var err error
templateFS, err = cache.Acquire(fileCtx, db, r.job.FileID)
if err != nil {
@@ -189,6 +209,7 @@ func (r *loader) dynamicRenderer(ctx context.Context, db database.Store, cache *
db: db,
ownerErrors: make(map[uuid.UUID]error),
close: cache.Close,
tfvarValues: tfVarValues,
}, nil
}
@@ -199,6 +220,7 @@ type dynamicRenderer struct {
ownerErrors map[uuid.UUID]error
currentOwner *previewtypes.WorkspaceOwner
tfvarValues map[string]cty.Value
once sync.Once
close func()
@@ -229,6 +251,7 @@ func (r *dynamicRenderer) Render(ctx context.Context, ownerID uuid.UUID, values
PlanJSON: r.data.terraformValues.CachedPlan,
ParameterValues: values,
Owner: *r.currentOwner,
TFVars: r.tfvarValues,
// Do not emit parser logs to coderd output logs.
// TODO: Returning this logs in the output would benefit the caller.
// Unsure how large the logs can be, so for now we just discard them.
@@ -0,0 +1,65 @@
package dynamicparameters
import (
"strconv"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/json"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
)
// VariableValues is a helper function that converts a slice of TemplateVersionVariable
// into a map of cty.Value for use in coder/preview.
func VariableValues(vals []database.TemplateVersionVariable) (map[string]cty.Value, error) {
ctyVals := make(map[string]cty.Value, len(vals))
for _, v := range vals {
value := v.Value
if value == "" && v.DefaultValue != "" {
value = v.DefaultValue
}
if value == "" {
// Empty strings are unsupported I guess?
continue // omit non-set vals
}
var err error
switch v.Type {
// Defaulting the empty type to "string"
// TODO: This does not match the terraform behavior, however it is too late
// at this point in the code to determine this, as the database type stores all values
// as strings. The code needs to be fixed in the `Parse` step of the provisioner.
// That step should determine the type of the variable correctly and store it in the database.
case "string", "":
ctyVals[v.Name] = cty.StringVal(value)
case "number":
ctyVals[v.Name], err = cty.ParseNumberVal(value)
if err != nil {
return nil, xerrors.Errorf("parse variable %q: %w", v.Name, err)
}
case "bool":
parsed, err := strconv.ParseBool(value)
if err != nil {
return nil, xerrors.Errorf("parse variable %q: %w", v.Name, err)
}
ctyVals[v.Name] = cty.BoolVal(parsed)
default:
// If it is a complex type, let the cty json code give it a try.
// TODO: Ideally we parse `list` & `map` and build the type ourselves.
ty, err := json.ImpliedType([]byte(value))
if err != nil {
return nil, xerrors.Errorf("implied type for variable %q: %w", v.Name, err)
}
jv, err := json.Unmarshal([]byte(value), ty)
if err != nil {
return nil, xerrors.Errorf("unmarshal variable %q: %w", v.Name, err)
}
ctyVals[v.Name] = jv
}
}
return ctyVals, nil
}