Skip to content

Commit 2279385

Browse files
committed
coderd/wsbuilder: pass template variable values to workspace tag evaluation context
1 parent f8a0356 commit 2279385

File tree

3 files changed

+53
-53
lines changed

3 files changed

+53
-53
lines changed

coderd/wsbuilder/wsbuilder.go

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import (
1212

1313
"github.com/hashicorp/hcl/v2"
1414
"github.com/hashicorp/hcl/v2/hclsyntax"
15-
"github.com/zclconf/go-cty/cty"
1615

1716
"github.com/coder/coder/v2/coderd/rbac/policy"
17+
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
1818
"github.com/coder/coder/v2/provisionersdk"
1919

2020
"github.com/google/uuid"
@@ -64,6 +64,7 @@ type Builder struct {
6464
templateVersion *database.TemplateVersion
6565
templateVersionJob *database.ProvisionerJob
6666
templateVersionParameters *[]database.TemplateVersionParameter
67+
templateVersionVariables *[]database.TemplateVersionVariable
6768
templateVersionWorkspaceTags *[]database.TemplateVersionWorkspaceTag
6869
lastBuild *database.WorkspaceBuild
6970
lastBuildErr *error
@@ -617,6 +618,22 @@ func (b *Builder) getTemplateVersionParameters() ([]database.TemplateVersionPara
617618
return tvp, nil
618619
}
619620

621+
func (b *Builder) getTemplateVersionVariables() ([]database.TemplateVersionVariable, error) {
622+
if b.templateVersionVariables != nil {
623+
return *b.templateVersionVariables, nil
624+
}
625+
tvID, err := b.getTemplateVersionID()
626+
if err != nil {
627+
return nil, xerrors.Errorf("get template version ID to get variables: %w", err)
628+
}
629+
tvs, err := b.store.GetTemplateVersionVariables(b.ctx, tvID)
630+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
631+
return nil, xerrors.Errorf("get template version %s variables: %w", tvID, err)
632+
}
633+
b.templateVersionVariables = &tvs
634+
return tvs, nil
635+
}
636+
620637
// verifyNoLegacyParameters verifies that initiator can't start the workspace build
621638
// if it uses legacy parameters (database.ParameterSchemas).
622639
func (b *Builder) verifyNoLegacyParameters() error {
@@ -678,17 +695,35 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
678695
tags[name] = value
679696
}
680697

681-
// Step 2: Mutate workspace tags
698+
// Step 2: Mutate workspace tags:
699+
// - Get workspace tags from the template version job
700+
// - Get template version variables from the template version as they can be
701+
// referenced in workspace tags
702+
// - Get parameters from the workspace build as they can also be referenced
703+
// in workspace tags
704+
// - Evaluate workspace tags given the above inputs
682705
workspaceTags, err := b.getTemplateVersionWorkspaceTags()
683706
if err != nil {
684707
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version workspace tags", err}
685708
}
709+
tvs, err := b.getTemplateVersionVariables()
710+
if err != nil {
711+
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version variables", err}
712+
}
713+
varsM := make(map[string]string)
714+
for _, tv := range tvs {
715+
varsM[tv.Name] = tv.Value
716+
}
686717
parameterNames, parameterValues, err := b.getParameters()
687718
if err != nil {
688719
return nil, err // already wrapped BuildError
689720
}
721+
paramsM := make(map[string]string)
722+
for i, name := range parameterNames {
723+
paramsM[name] = parameterValues[i]
724+
}
690725

691-
evalCtx := buildParametersEvalContext(parameterNames, parameterValues)
726+
evalCtx := tfparse.BuildEvalContext(varsM, paramsM)
692727
for _, workspaceTag := range workspaceTags {
693728
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTag.Value), "expression.hcl", hcl.InitialPos)
694729
if diags.HasErrors() {
@@ -701,7 +736,7 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
701736
}
702737

703738
// Do not use "val.AsString()" as it can panic
704-
str, err := ctyValueString(val)
739+
str, err := tfparse.CtyValueString(val)
705740
if err != nil {
706741
return nil, BuildError{http.StatusBadRequest, "failed to marshal cty.Value as string", err}
707742
}
@@ -710,44 +745,6 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
710745
return tags, nil
711746
}
712747

713-
func buildParametersEvalContext(names, values []string) *hcl.EvalContext {
714-
m := map[string]cty.Value{}
715-
for i, name := range names {
716-
m[name] = cty.MapVal(map[string]cty.Value{
717-
"value": cty.StringVal(values[i]),
718-
})
719-
}
720-
721-
if len(m) == 0 {
722-
return nil // otherwise, panic: must not call MapVal with empty map
723-
}
724-
725-
return &hcl.EvalContext{
726-
Variables: map[string]cty.Value{
727-
"data": cty.MapVal(map[string]cty.Value{
728-
"coder_parameter": cty.MapVal(m),
729-
}),
730-
},
731-
}
732-
}
733-
734-
func ctyValueString(val cty.Value) (string, error) {
735-
switch val.Type() {
736-
case cty.Bool:
737-
if val.True() {
738-
return "true", nil
739-
} else {
740-
return "false", nil
741-
}
742-
case cty.Number:
743-
return val.AsBigFloat().String(), nil
744-
case cty.String:
745-
return val.AsString(), nil
746-
default:
747-
return "", xerrors.Errorf("only primitive types are supported - bool, number, and string")
748-
}
749-
}
750-
751748
func (b *Builder) getTemplateVersionWorkspaceTags() ([]database.TemplateVersionWorkspaceTag, error) {
752749
if b.templateVersionWorkspaceTags != nil {
753750
return *b.templateVersionWorkspaceTags, nil

enterprise/coderd/workspaces_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,7 +1527,7 @@ func TestWorkspaceTagsTerraform(t *testing.T) {
15271527
tc := tc
15281528
t.Run(tc.name, func(t *testing.T) {
15291529
t.Parallel()
1530-
ctx := testutil.Context(t, testutil.WaitShort)
1530+
ctx := testutil.Context(t, testutil.WaitSuperLong)
15311531

15321532
client, owner := coderdenttest.New(t, &coderdenttest.Options{
15331533
Options: &coderdtest.Options{
@@ -1559,7 +1559,6 @@ func TestWorkspaceTagsTerraform(t *testing.T) {
15591559
})
15601560
require.NoError(t, err, "failed to create template version")
15611561
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID)
1562-
require.NoError(t, err, "failed to create template version")
15631562
tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, tv.ID)
15641563

15651564
// Creating a workspace as a non-privileged user must succeed

provisioner/terraform/tfparse/tfparse.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,13 @@ func (p *Parser) CoderParameterDefaults(ctx context.Context, varsDefaults map[st
327327
// Issue #15795: the "default" value could also be an expression we need
328328
// to evaluate.
329329
// TODO: should we support coder_parameter default values that reference other coder_parameter data sources?
330-
evalCtx := buildEvalContext(varsDefaults, nil)
330+
evalCtx := BuildEvalContext(varsDefaults, nil)
331331
val, diags := expr.Value(evalCtx)
332332
if diags.HasErrors() {
333333
return nil, xerrors.Errorf("failed to evaluate coder_parameter %q default value %q: %s", dataResource.Name, value, diags.Error())
334334
}
335335
// Do not use "val.AsString()" as it can panic
336-
strVal, err := ctyValueString(val)
336+
strVal, err := CtyValueString(val)
337337
if err != nil {
338338
return nil, xerrors.Errorf("failed to marshal coder_parameter %q default value %q as string: %s", dataResource.Name, value, err)
339339
}
@@ -355,7 +355,7 @@ func evaluateWorkspaceTags(varsDefaults, paramsDefaults, workspaceTags map[strin
355355
}
356356
// We only add variables and coder_parameter data sources. Anything else will be
357357
// undefined and will raise a Terraform error.
358-
evalCtx := buildEvalContext(varsDefaults, paramsDefaults)
358+
evalCtx := BuildEvalContext(varsDefaults, paramsDefaults)
359359
tags := make(map[string]string)
360360
for workspaceTagKey, workspaceTagValue := range workspaceTags {
361361
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTagValue), "expression.hcl", hcl.InitialPos)
@@ -369,7 +369,7 @@ func evaluateWorkspaceTags(varsDefaults, paramsDefaults, workspaceTags map[strin
369369
}
370370

371371
// Do not use "val.AsString()" as it can panic
372-
str, err := ctyValueString(val)
372+
str, err := CtyValueString(val)
373373
if err != nil {
374374
return nil, xerrors.Errorf("failed to marshal workspace tag key %q value %q as string: %s", workspaceTagKey, workspaceTagValue, err)
375375
}
@@ -395,16 +395,17 @@ func validWorkspaceTagValues(tags map[string]string) error {
395395
return nil
396396
}
397397

398-
func buildEvalContext(varDefaults map[string]string, paramDefaults map[string]string) *hcl.EvalContext {
398+
// BuildEvalContext builds an evaluation context for the given variable and parameter defaults.
399+
func BuildEvalContext(vars map[string]string, params map[string]string) *hcl.EvalContext {
399400
varDefaultsM := map[string]cty.Value{}
400-
for varName, varDefault := range varDefaults {
401+
for varName, varDefault := range vars {
401402
varDefaultsM[varName] = cty.MapVal(map[string]cty.Value{
402403
"value": cty.StringVal(varDefault),
403404
})
404405
}
405406

406407
paramDefaultsM := map[string]cty.Value{}
407-
for paramName, paramDefault := range paramDefaults {
408+
for paramName, paramDefault := range params {
408409
paramDefaultsM[paramName] = cty.MapVal(map[string]cty.Value{
409410
"value": cty.StringVal(paramDefault),
410411
})
@@ -496,7 +497,10 @@ func compareSourcePos(x, y tfconfig.SourcePos) bool {
496497
return x.Line < y.Line
497498
}
498499

499-
func ctyValueString(val cty.Value) (string, error) {
500+
// CtyValueString converts a cty.Value to a string.
501+
// It supports only primitive types - bool, number, and string.
502+
// As a special case, it also supports map[string]interface{} with key "value".
503+
func CtyValueString(val cty.Value) (string, error) {
500504
switch val.Type() {
501505
case cty.Bool:
502506
if val.True() {
@@ -514,7 +518,7 @@ func ctyValueString(val cty.Value) (string, error) {
514518
if !ok {
515519
return "", xerrors.Errorf("map does not have key 'value'")
516520
}
517-
return ctyValueString(valval)
521+
return CtyValueString(valval)
518522
default:
519523
return "", xerrors.Errorf("only primitive types are supported - bool, number, and string")
520524
}

0 commit comments

Comments
 (0)