Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit dcf5153

Browse files
authored
fix(coderd/wsbuilder): correctly evaluate dynamic workspace tag values (#15897)
Relates to #15894: - Adds `coderdenttest.NewExternalProvisionerDaemonTerraform` - Adds integration-style test coverage for creating a workspace with `coder_workspace_tags` specified in `main.tf` - Modifies `coderd/wsbuilder` to fetch template version variables and includes them in eval context for evaluating `coder_workspace_tags`
1 parent bcb15aa commit dcf5153

File tree

5 files changed

+347
-78
lines changed

5 files changed

+347
-78
lines changed

coderd/wsbuilder/wsbuilder.go

+44-42
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,40 @@ 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+
// FIXME: do this in Terraform? This is a bit of a hack.
716+
if tv.Value == "" {
717+
varsM[tv.Name] = tv.DefaultValue
718+
} else {
719+
varsM[tv.Name] = tv.Value
720+
}
721+
}
686722
parameterNames, parameterValues, err := b.getParameters()
687723
if err != nil {
688724
return nil, err // already wrapped BuildError
689725
}
726+
paramsM := make(map[string]string)
727+
for i, name := range parameterNames {
728+
paramsM[name] = parameterValues[i]
729+
}
690730

691-
evalCtx := buildParametersEvalContext(parameterNames, parameterValues)
731+
evalCtx := tfparse.BuildEvalContext(varsM, paramsM)
692732
for _, workspaceTag := range workspaceTags {
693733
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTag.Value), "expression.hcl", hcl.InitialPos)
694734
if diags.HasErrors() {
@@ -701,7 +741,7 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
701741
}
702742

703743
// Do not use "val.AsString()" as it can panic
704-
str, err := ctyValueString(val)
744+
str, err := tfparse.CtyValueString(val)
705745
if err != nil {
706746
return nil, BuildError{http.StatusBadRequest, "failed to marshal cty.Value as string", err}
707747
}
@@ -710,44 +750,6 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
710750
return tags, nil
711751
}
712752

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-
751753
func (b *Builder) getTemplateVersionWorkspaceTags() ([]database.TemplateVersionWorkspaceTag, error) {
752754
if b.templateVersionWorkspaceTags != nil {
753755
return *b.templateVersionWorkspaceTags, nil

coderd/wsbuilder/wsbuilder_test.go

+48-8
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func TestBuilder_NoOptions(t *testing.T) {
5858
withTemplate,
5959
withInactiveVersion(nil),
6060
withLastBuildFound,
61+
withTemplateVersionVariables(inactiveVersionID, nil),
6162
withRichParameters(nil),
6263
withParameterSchemas(inactiveJobID, nil),
6364
withWorkspaceTags(inactiveVersionID, nil),
@@ -113,6 +114,7 @@ func TestBuilder_Initiator(t *testing.T) {
113114
withTemplate,
114115
withInactiveVersion(nil),
115116
withLastBuildFound,
117+
withTemplateVersionVariables(inactiveVersionID, nil),
116118
withRichParameters(nil),
117119
withParameterSchemas(inactiveJobID, nil),
118120
withWorkspaceTags(inactiveVersionID, nil),
@@ -158,6 +160,7 @@ func TestBuilder_Baggage(t *testing.T) {
158160
withTemplate,
159161
withInactiveVersion(nil),
160162
withLastBuildFound,
163+
withTemplateVersionVariables(inactiveVersionID, nil),
161164
withRichParameters(nil),
162165
withParameterSchemas(inactiveJobID, nil),
163166
withWorkspaceTags(inactiveVersionID, nil),
@@ -195,6 +198,7 @@ func TestBuilder_Reason(t *testing.T) {
195198
withTemplate,
196199
withInactiveVersion(nil),
197200
withLastBuildFound,
201+
withTemplateVersionVariables(inactiveVersionID, nil),
198202
withRichParameters(nil),
199203
withParameterSchemas(inactiveJobID, nil),
200204
withWorkspaceTags(inactiveVersionID, nil),
@@ -232,6 +236,7 @@ func TestBuilder_ActiveVersion(t *testing.T) {
232236
withTemplate,
233237
withActiveVersion(nil),
234238
withLastBuildNotFound,
239+
withTemplateVersionVariables(activeVersionID, nil),
235240
withParameterSchemas(activeJobID, nil),
236241
withWorkspaceTags(activeVersionID, nil),
237242
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
@@ -296,6 +301,14 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
296301
Key: "is_debug_build",
297302
Value: `data.coder_parameter.is_debug_build.value == "true" ? "in-debug-mode" : "no-debug"`,
298303
},
304+
{
305+
Key: "variable_tag",
306+
Value: `var.tag`,
307+
},
308+
{
309+
Key: "another_variable_tag",
310+
Value: `var.tag2`,
311+
},
299312
}
300313

301314
richParameters := []database.TemplateVersionParameter{
@@ -307,6 +320,11 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
307320
{Name: "number_of_oranges", Type: "number", Description: "This is fifth parameter", Mutable: false, DefaultValue: "6", Options: json.RawMessage("[]")},
308321
}
309322

323+
templateVersionVariables := []database.TemplateVersionVariable{
324+
{Name: "tag", Description: "This is a variable tag", TemplateVersionID: inactiveVersionID, Type: "string", DefaultValue: "default-value", Value: "my-value"},
325+
{Name: "tag2", Description: "This is another variable tag", TemplateVersionID: inactiveVersionID, Type: "string", DefaultValue: "default-value-2", Value: ""},
326+
}
327+
310328
buildParameters := []codersdk.WorkspaceBuildParameter{
311329
{Name: "project", Value: "foobar-foobaz"},
312330
{Name: "is_debug_build", Value: "true"},
@@ -321,23 +339,26 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
321339
withTemplate,
322340
withInactiveVersion(richParameters),
323341
withLastBuildFound,
342+
withTemplateVersionVariables(inactiveVersionID, templateVersionVariables),
324343
withRichParameters(nil),
325344
withParameterSchemas(inactiveJobID, nil),
326345
withWorkspaceTags(inactiveVersionID, workspaceTags),
327346
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
328347

329348
// Outputs
330349
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
331-
asrt.Len(job.Tags, 10)
350+
asrt.Len(job.Tags, 12)
332351

333352
expected := database.StringMap{
334-
"actually_no": "false",
335-
"cluster_tag": "best_developers",
336-
"fruits_tag": "10",
337-
"is_debug_build": "in-debug-mode",
338-
"project_tag": "foobar-foobaz+12345",
339-
"team_tag": "godzilla",
340-
"yes_or_no": "true",
353+
"actually_no": "false",
354+
"cluster_tag": "best_developers",
355+
"fruits_tag": "10",
356+
"is_debug_build": "in-debug-mode",
357+
"project_tag": "foobar-foobaz+12345",
358+
"team_tag": "godzilla",
359+
"yes_or_no": "true",
360+
"variable_tag": "my-value",
361+
"another_variable_tag": "default-value-2",
341362

342363
"scope": "user",
343364
"version": "inactive",
@@ -413,6 +434,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
413434
withTemplate,
414435
withInactiveVersion(richParameters),
415436
withLastBuildFound,
437+
withTemplateVersionVariables(inactiveVersionID, nil),
416438
withRichParameters(initialBuildParameters),
417439
withParameterSchemas(inactiveJobID, nil),
418440
withWorkspaceTags(inactiveVersionID, nil),
@@ -459,6 +481,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
459481
withTemplate,
460482
withInactiveVersion(richParameters),
461483
withLastBuildFound,
484+
withTemplateVersionVariables(inactiveVersionID, nil),
462485
withRichParameters(initialBuildParameters),
463486
withParameterSchemas(inactiveJobID, nil),
464487
withWorkspaceTags(inactiveVersionID, nil),
@@ -511,6 +534,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
511534
withTemplate,
512535
withInactiveVersion(richParameters),
513536
withLastBuildFound,
537+
withTemplateVersionVariables(inactiveVersionID, nil),
514538
withRichParameters(nil),
515539
withParameterSchemas(inactiveJobID, schemas),
516540
withWorkspaceTags(inactiveVersionID, nil),
@@ -542,6 +566,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
542566
withTemplate,
543567
withInactiveVersion(richParameters),
544568
withLastBuildFound,
569+
withTemplateVersionVariables(inactiveVersionID, nil),
545570
withRichParameters(initialBuildParameters),
546571
withParameterSchemas(inactiveJobID, nil),
547572
withWorkspaceTags(inactiveVersionID, nil),
@@ -593,6 +618,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
593618
withTemplate,
594619
withActiveVersion(version2params),
595620
withLastBuildFound,
621+
withTemplateVersionVariables(activeVersionID, nil),
596622
withRichParameters(initialBuildParameters),
597623
withParameterSchemas(activeJobID, nil),
598624
withWorkspaceTags(activeVersionID, nil),
@@ -655,6 +681,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
655681
withTemplate,
656682
withActiveVersion(version2params),
657683
withLastBuildFound,
684+
withTemplateVersionVariables(activeVersionID, nil),
658685
withRichParameters(initialBuildParameters),
659686
withParameterSchemas(activeJobID, nil),
660687
withWorkspaceTags(activeVersionID, nil),
@@ -715,6 +742,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
715742
withTemplate,
716743
withActiveVersion(version2params),
717744
withLastBuildFound,
745+
withTemplateVersionVariables(activeVersionID, nil),
718746
withRichParameters(initialBuildParameters),
719747
withParameterSchemas(activeJobID, nil),
720748
withWorkspaceTags(activeVersionID, nil),
@@ -921,6 +949,18 @@ func withParameterSchemas(jobID uuid.UUID, schemas []database.ParameterSchema) f
921949
}
922950
}
923951

952+
func withTemplateVersionVariables(versionID uuid.UUID, params []database.TemplateVersionVariable) func(mTx *dbmock.MockStore) {
953+
return func(mTx *dbmock.MockStore) {
954+
c := mTx.EXPECT().GetTemplateVersionVariables(gomock.Any(), versionID).
955+
Times(1)
956+
if len(params) > 0 {
957+
c.Return(params, nil)
958+
} else {
959+
c.Return(nil, sql.ErrNoRows)
960+
}
961+
}
962+
}
963+
924964
func withRichParameters(params []database.WorkspaceBuildParameter) func(mTx *dbmock.MockStore) {
925965
return func(mTx *dbmock.MockStore) {
926966
c := mTx.EXPECT().GetWorkspaceBuildParameters(gomock.Any(), lastBuildID).

0 commit comments

Comments
 (0)