Skip to content

Commit 01f1c94

Browse files
committed
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` (cherry picked from commit dcf5153)
1 parent 4c452af commit 01f1c94

File tree

5 files changed

+347
-78
lines changed

5 files changed

+347
-78
lines changed

coderd/wsbuilder/wsbuilder.go

Lines changed: 44 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,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

Lines changed: 48 additions & 8 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy