mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(coderd/wsbuilder): correctly evaluate dynamic workspace tag values (#15897)
Relates to https://github.com/coder/coder/issues/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`
This commit is contained in:
@@ -12,9 +12,9 @@ import (
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -64,6 +64,7 @@ type Builder struct {
|
||||
templateVersion *database.TemplateVersion
|
||||
templateVersionJob *database.ProvisionerJob
|
||||
templateVersionParameters *[]database.TemplateVersionParameter
|
||||
templateVersionVariables *[]database.TemplateVersionVariable
|
||||
templateVersionWorkspaceTags *[]database.TemplateVersionWorkspaceTag
|
||||
lastBuild *database.WorkspaceBuild
|
||||
lastBuildErr *error
|
||||
@@ -617,6 +618,22 @@ func (b *Builder) getTemplateVersionParameters() ([]database.TemplateVersionPara
|
||||
return tvp, nil
|
||||
}
|
||||
|
||||
func (b *Builder) getTemplateVersionVariables() ([]database.TemplateVersionVariable, error) {
|
||||
if b.templateVersionVariables != nil {
|
||||
return *b.templateVersionVariables, nil
|
||||
}
|
||||
tvID, err := b.getTemplateVersionID()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get template version ID to get variables: %w", err)
|
||||
}
|
||||
tvs, err := b.store.GetTemplateVersionVariables(b.ctx, tvID)
|
||||
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||
return nil, xerrors.Errorf("get template version %s variables: %w", tvID, err)
|
||||
}
|
||||
b.templateVersionVariables = &tvs
|
||||
return tvs, nil
|
||||
}
|
||||
|
||||
// verifyNoLegacyParameters verifies that initiator can't start the workspace build
|
||||
// if it uses legacy parameters (database.ParameterSchemas).
|
||||
func (b *Builder) verifyNoLegacyParameters() error {
|
||||
@@ -678,17 +695,40 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
|
||||
tags[name] = value
|
||||
}
|
||||
|
||||
// Step 2: Mutate workspace tags
|
||||
// Step 2: Mutate workspace tags:
|
||||
// - Get workspace tags from the template version job
|
||||
// - Get template version variables from the template version as they can be
|
||||
// referenced in workspace tags
|
||||
// - Get parameters from the workspace build as they can also be referenced
|
||||
// in workspace tags
|
||||
// - Evaluate workspace tags given the above inputs
|
||||
workspaceTags, err := b.getTemplateVersionWorkspaceTags()
|
||||
if err != nil {
|
||||
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version workspace tags", err}
|
||||
}
|
||||
tvs, err := b.getTemplateVersionVariables()
|
||||
if err != nil {
|
||||
return nil, BuildError{http.StatusInternalServerError, "failed to fetch template version variables", err}
|
||||
}
|
||||
varsM := make(map[string]string)
|
||||
for _, tv := range tvs {
|
||||
// FIXME: do this in Terraform? This is a bit of a hack.
|
||||
if tv.Value == "" {
|
||||
varsM[tv.Name] = tv.DefaultValue
|
||||
} else {
|
||||
varsM[tv.Name] = tv.Value
|
||||
}
|
||||
}
|
||||
parameterNames, parameterValues, err := b.getParameters()
|
||||
if err != nil {
|
||||
return nil, err // already wrapped BuildError
|
||||
}
|
||||
paramsM := make(map[string]string)
|
||||
for i, name := range parameterNames {
|
||||
paramsM[name] = parameterValues[i]
|
||||
}
|
||||
|
||||
evalCtx := buildParametersEvalContext(parameterNames, parameterValues)
|
||||
evalCtx := tfparse.BuildEvalContext(varsM, paramsM)
|
||||
for _, workspaceTag := range workspaceTags {
|
||||
expr, diags := hclsyntax.ParseExpression([]byte(workspaceTag.Value), "expression.hcl", hcl.InitialPos)
|
||||
if diags.HasErrors() {
|
||||
@@ -701,7 +741,7 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
|
||||
}
|
||||
|
||||
// Do not use "val.AsString()" as it can panic
|
||||
str, err := ctyValueString(val)
|
||||
str, err := tfparse.CtyValueString(val)
|
||||
if err != nil {
|
||||
return nil, BuildError{http.StatusBadRequest, "failed to marshal cty.Value as string", err}
|
||||
}
|
||||
@@ -710,44 +750,6 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func buildParametersEvalContext(names, values []string) *hcl.EvalContext {
|
||||
m := map[string]cty.Value{}
|
||||
for i, name := range names {
|
||||
m[name] = cty.MapVal(map[string]cty.Value{
|
||||
"value": cty.StringVal(values[i]),
|
||||
})
|
||||
}
|
||||
|
||||
if len(m) == 0 {
|
||||
return nil // otherwise, panic: must not call MapVal with empty map
|
||||
}
|
||||
|
||||
return &hcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"data": cty.MapVal(map[string]cty.Value{
|
||||
"coder_parameter": cty.MapVal(m),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ctyValueString(val cty.Value) (string, error) {
|
||||
switch val.Type() {
|
||||
case cty.Bool:
|
||||
if val.True() {
|
||||
return "true", nil
|
||||
} else {
|
||||
return "false", nil
|
||||
}
|
||||
case cty.Number:
|
||||
return val.AsBigFloat().String(), nil
|
||||
case cty.String:
|
||||
return val.AsString(), nil
|
||||
default:
|
||||
return "", xerrors.Errorf("only primitive types are supported - bool, number, and string")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) getTemplateVersionWorkspaceTags() ([]database.TemplateVersionWorkspaceTag, error) {
|
||||
if b.templateVersionWorkspaceTags != nil {
|
||||
return *b.templateVersionWorkspaceTags, nil
|
||||
|
||||
@@ -58,6 +58,7 @@ func TestBuilder_NoOptions(t *testing.T) {
|
||||
withTemplate,
|
||||
withInactiveVersion(nil),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
@@ -113,6 +114,7 @@ func TestBuilder_Initiator(t *testing.T) {
|
||||
withTemplate,
|
||||
withInactiveVersion(nil),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
@@ -158,6 +160,7 @@ func TestBuilder_Baggage(t *testing.T) {
|
||||
withTemplate,
|
||||
withInactiveVersion(nil),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
@@ -195,6 +198,7 @@ func TestBuilder_Reason(t *testing.T) {
|
||||
withTemplate,
|
||||
withInactiveVersion(nil),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
@@ -232,6 +236,7 @@ func TestBuilder_ActiveVersion(t *testing.T) {
|
||||
withTemplate,
|
||||
withActiveVersion(nil),
|
||||
withLastBuildNotFound,
|
||||
withTemplateVersionVariables(activeVersionID, nil),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
|
||||
@@ -296,6 +301,14 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
||||
Key: "is_debug_build",
|
||||
Value: `data.coder_parameter.is_debug_build.value == "true" ? "in-debug-mode" : "no-debug"`,
|
||||
},
|
||||
{
|
||||
Key: "variable_tag",
|
||||
Value: `var.tag`,
|
||||
},
|
||||
{
|
||||
Key: "another_variable_tag",
|
||||
Value: `var.tag2`,
|
||||
},
|
||||
}
|
||||
|
||||
richParameters := []database.TemplateVersionParameter{
|
||||
@@ -307,6 +320,11 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
||||
{Name: "number_of_oranges", Type: "number", Description: "This is fifth parameter", Mutable: false, DefaultValue: "6", Options: json.RawMessage("[]")},
|
||||
}
|
||||
|
||||
templateVersionVariables := []database.TemplateVersionVariable{
|
||||
{Name: "tag", Description: "This is a variable tag", TemplateVersionID: inactiveVersionID, Type: "string", DefaultValue: "default-value", Value: "my-value"},
|
||||
{Name: "tag2", Description: "This is another variable tag", TemplateVersionID: inactiveVersionID, Type: "string", DefaultValue: "default-value-2", Value: ""},
|
||||
}
|
||||
|
||||
buildParameters := []codersdk.WorkspaceBuildParameter{
|
||||
{Name: "project", Value: "foobar-foobaz"},
|
||||
{Name: "is_debug_build", Value: "true"},
|
||||
@@ -321,6 +339,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
||||
withTemplate,
|
||||
withInactiveVersion(richParameters),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(inactiveVersionID, templateVersionVariables),
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, workspaceTags),
|
||||
@@ -328,16 +347,18 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
|
||||
|
||||
// Outputs
|
||||
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {
|
||||
asrt.Len(job.Tags, 10)
|
||||
asrt.Len(job.Tags, 12)
|
||||
|
||||
expected := database.StringMap{
|
||||
"actually_no": "false",
|
||||
"cluster_tag": "best_developers",
|
||||
"fruits_tag": "10",
|
||||
"is_debug_build": "in-debug-mode",
|
||||
"project_tag": "foobar-foobaz+12345",
|
||||
"team_tag": "godzilla",
|
||||
"yes_or_no": "true",
|
||||
"actually_no": "false",
|
||||
"cluster_tag": "best_developers",
|
||||
"fruits_tag": "10",
|
||||
"is_debug_build": "in-debug-mode",
|
||||
"project_tag": "foobar-foobaz+12345",
|
||||
"team_tag": "godzilla",
|
||||
"yes_or_no": "true",
|
||||
"variable_tag": "my-value",
|
||||
"another_variable_tag": "default-value-2",
|
||||
|
||||
"scope": "user",
|
||||
"version": "inactive",
|
||||
@@ -413,6 +434,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withTemplate,
|
||||
withInactiveVersion(richParameters),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
@@ -459,6 +481,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withTemplate,
|
||||
withInactiveVersion(richParameters),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
@@ -511,6 +534,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withTemplate,
|
||||
withInactiveVersion(richParameters),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||
withRichParameters(nil),
|
||||
withParameterSchemas(inactiveJobID, schemas),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
@@ -542,6 +566,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withTemplate,
|
||||
withInactiveVersion(richParameters),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(inactiveVersionID, nil),
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(inactiveJobID, nil),
|
||||
withWorkspaceTags(inactiveVersionID, nil),
|
||||
@@ -593,6 +618,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withTemplate,
|
||||
withActiveVersion(version2params),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(activeVersionID, nil),
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
@@ -655,6 +681,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withTemplate,
|
||||
withActiveVersion(version2params),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(activeVersionID, nil),
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
@@ -715,6 +742,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
|
||||
withTemplate,
|
||||
withActiveVersion(version2params),
|
||||
withLastBuildFound,
|
||||
withTemplateVersionVariables(activeVersionID, nil),
|
||||
withRichParameters(initialBuildParameters),
|
||||
withParameterSchemas(activeJobID, nil),
|
||||
withWorkspaceTags(activeVersionID, nil),
|
||||
@@ -921,6 +949,18 @@ func withParameterSchemas(jobID uuid.UUID, schemas []database.ParameterSchema) f
|
||||
}
|
||||
}
|
||||
|
||||
func withTemplateVersionVariables(versionID uuid.UUID, params []database.TemplateVersionVariable) func(mTx *dbmock.MockStore) {
|
||||
return func(mTx *dbmock.MockStore) {
|
||||
c := mTx.EXPECT().GetTemplateVersionVariables(gomock.Any(), versionID).
|
||||
Times(1)
|
||||
if len(params) > 0 {
|
||||
c.Return(params, nil)
|
||||
} else {
|
||||
c.Return(nil, sql.ErrNoRows)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withRichParameters(params []database.WorkspaceBuildParameter) func(mTx *dbmock.MockStore) {
|
||||
return func(mTx *dbmock.MockStore) {
|
||||
c := mTx.EXPECT().GetWorkspaceBuildParameters(gomock.Any(), lastBuildID).
|
||||
|
||||
Reference in New Issue
Block a user