From 0ebaeb26c3dc8889a9a3ec29e26c9de989c5ca65 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 12:43:42 +0000 Subject: [PATCH 01/10] chore(enterprise/coderd/coderdenttest: add NewExternalProvisionerDaemonTerraform --- .../coderd/coderdenttest/coderdenttest.go | 77 ++++++++++++++----- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index b397cb05dc5d4..1125ab9128e4c 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -16,7 +16,8 @@ import ( "github.com/moby/moby/pkg/namesgenerator" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/xerrors" + + "cdr.dev/slog" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" @@ -28,6 +29,7 @@ import ( "github.com/coder/coder/v2/enterprise/coderd/license" "github.com/coder/coder/v2/enterprise/dbcrypt" "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/provisioner/terraform" "github.com/coder/coder/v2/provisionerd" provisionerdproto "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" @@ -304,14 +306,31 @@ func CreateOrganization(t *testing.T, client *codersdk.Client, opts CreateOrgani return org } +// NewExternalProvisionerDaemon runs an external provisioner daemon in a +// goroutine and returns a closer to stop it. The echo provisioner is used +// here. This is the default provisioner for tests and should be fine for +// most use cases. If you need to test terraform-specific behaviors, use +// NewExternalProvisionerDaemonTerraform instead. func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer { t.Helper() + return newExternalProvisionerDaemon(t, client, org, tags, codersdk.ProvisionerTypeEcho) +} + +// NewExternalProvisionerDaemonTerraform runs an external provisioner daemon in +// a goroutine and returns a closer to stop it. The terraform provisioner is +// used here. Avoid using this unless you need to test terraform-specific +// behaviors! +func NewExternalProvisionerDaemonTerraform(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string) io.Closer { + t.Helper() + return newExternalProvisionerDaemon(t, client, org, tags, codersdk.ProvisionerTypeTerraform) +} + +// nolint // This function is a helper for tests and should not be linted. +func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string, ty codersdk.ProvisionerType) io.Closer { + t.Helper() - // Without this check, the provisioner will silently fail. entitlements, err := client.Entitlements(context.Background()) if err != nil { - // AGPL instances will throw this error. They cannot use external - // provisioners. t.Errorf("external provisioners requires a license with entitlements. The client failed to fetch the entitlements, is this an enterprise instance of coderd?") t.FailNow() return nil @@ -319,42 +338,62 @@ func NewExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui feature := entitlements.Features[codersdk.FeatureExternalProvisionerDaemons] if !feature.Enabled || feature.Entitlement != codersdk.EntitlementEntitled { - require.NoError(t, xerrors.Errorf("external provisioner daemons require an entitled license")) + t.Errorf("external provisioner daemons require an entitled license") + t.FailNow() return nil } - echoClient, echoServer := drpc.MemTransportPipe() + provisionerClient, provisionerSrv := drpc.MemTransportPipe() ctx, cancelFunc := context.WithCancel(context.Background()) serveDone := make(chan struct{}) t.Cleanup(func() { - _ = echoClient.Close() - _ = echoServer.Close() + _ = provisionerClient.Close() + _ = provisionerSrv.Close() cancelFunc() <-serveDone }) - go func() { - defer close(serveDone) - err := echo.Serve(ctx, &provisionersdk.ServeOptions{ - Listener: echoServer, - WorkDirectory: t.TempDir(), - }) - assert.NoError(t, err) - }() + + var serveFunc func() + switch ty { + case codersdk.ProvisionerTypeTerraform: + serveFunc = func() { + defer close(serveDone) + assert.NoError(t, terraform.Serve(ctx, &terraform.ServeOptions{ + ServeOptions: &provisionersdk.ServeOptions{ + Listener: provisionerSrv, + WorkDirectory: t.TempDir(), + }, + })) + } + case codersdk.ProvisionerTypeEcho: + serveFunc = func() { + defer close(serveDone) + assert.NoError(t, echo.Serve(ctx, &provisionersdk.ServeOptions{ + Listener: provisionerSrv, + WorkDirectory: t.TempDir(), + })) + } + default: + t.Fatalf("unsupported provisioner type: %s", ty) + return nil + } + + go serveFunc() daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), Name: t.Name(), Organization: org, - Provisioners: []codersdk.ProvisionerType{codersdk.ProvisionerTypeEcho}, + Provisioners: []codersdk.ProvisionerType{ty}, Tags: tags, }) }, &provisionerd.Options{ - Logger: testutil.Logger(t).Named("provisionerd"), + Logger: testutil.Logger(t).Named("provisionerd").Leveled(slog.LevelDebug), UpdateInterval: 250 * time.Millisecond, ForceCancelInterval: 5 * time.Second, Connector: provisionerd.LocalProvisioners{ - string(database.ProvisionerTypeEcho): sdkproto.NewDRPCProvisionerClient(echoClient), + string(ty): sdkproto.NewDRPCProvisionerClient(provisionerClient), }, }) closer := coderdtest.NewProvisionerDaemonCloser(daemon) From f8a035660c7cfa63a254a0731299e7971d48da7f Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 12:44:02 +0000 Subject: [PATCH 02/10] chore(enterprise/coderd): RED: add TestWorkspaceTagsTerraform --- enterprise/coderd/workspaces_test.go | 153 +++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index e5142c1a83ee8..e5d010ce210a7 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -1,8 +1,10 @@ package coderd_test import ( + "bytes" "context" "database/sql" + "fmt" "net/http" "sync/atomic" "testing" @@ -1420,6 +1422,157 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) { }) } +// TestWorkspaceTagsTerraform tests that a workspace can be created with tags. +// This is an end-to-end-style test, meaning that we actually run the +// real Terraform provisioner and validate that the workspace is created +// successfully. The workspace itself does not specify any resources, and +// this is fine. +func TestWorkspaceTagsTerraform(t *testing.T) { + t.Parallel() + + mainTfTemplate := ` + terraform { + required_providers { + coder = { + source = "coder/coder" + } + } + } + provider "coder" {} + data "coder_workspace" "me" {} + data "coder_workspace_owner" "me" {} + %s + ` + + for _, tc := range []struct { + name string + // tags to apply to the external provisioner + provisionerTags map[string]string + // tags to apply to the create template version request + createTemplateVersionRequestTags map[string]string + // the coder_workspace_tags bit of main.tf. + // you can add more stuff here if you need + tfWorkspaceTags string + }{ + { + name: "no tags", + tfWorkspaceTags: ``, + }, + { + name: "empty tags", + tfWorkspaceTags: ` + data "coder_workspace_tags" "tags" { + tags = {} + } + `, + }, + { + name: "static tag", + provisionerTags: map[string]string{"foo": "bar"}, + tfWorkspaceTags: ` + data "coder_workspace_tags" "tags" { + tags = { + "foo" = "bar" + } + }`, + }, + { + name: "tag variable", + provisionerTags: map[string]string{"foo": "bar"}, + tfWorkspaceTags: ` + variable "foo" { + default = "bar" + } + data "coder_workspace_tags" "tags" { + tags = { + "foo" = var.foo + } + }`, + }, + { + name: "tag param", + provisionerTags: map[string]string{"foo": "bar"}, + tfWorkspaceTags: ` + data "coder_parameter" "foo" { + name = "foo" + type = "string" + default = "bar" + } + data "coder_workspace_tags" "tags" { + tags = { + "foo" = data.coder_parameter.foo.value + } + }`, + }, + { + name: "tag param with default from var", + provisionerTags: map[string]string{"foo": "bar"}, + tfWorkspaceTags: ` + variable "foo" { + type = "string" + default = "bar" + } + data "coder_parameter" "foo" { + name = "foo" + type = "string" + default = var.foo + } + data "coder_workspace_tags" "tags" { + tags = { + "foo" = data.coder_parameter.foo.value + } + }`, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + client, owner := coderdenttest.New(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + // We intentionally do not run a built-in provisioner daemon here. + IncludeProvisionerDaemon: false, + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, + }, + }) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + _ = coderdenttest.NewExternalProvisionerDaemonTerraform(t, client, owner.OrganizationID, tc.provisionerTags) + + // Creating a template as a template admin must succeed + templateFiles := map[string]string{"main.tf": fmt.Sprintf(mainTfTemplate, tc.tfWorkspaceTags)} + tarBytes := testutil.CreateTar(t, templateFiles) + fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarBytes)) + require.NoError(t, err, "failed to upload file") + tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: testutil.GetRandomName(t), + FileID: fi.ID, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + Provisioner: codersdk.ProvisionerTypeTerraform, + ProvisionerTags: tc.createTemplateVersionRequestTags, + }) + require.NoError(t, err, "failed to create template version") + coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID) + require.NoError(t, err, "failed to create template version") + tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, tv.ID) + + // Creating a workspace as a non-privileged user must succeed + ws, err := member.CreateUserWorkspace(ctx, memberUser.Username, codersdk.CreateWorkspaceRequest{ + TemplateID: tpl.ID, + Name: coderdtest.RandomUsername(t), + }) + require.NoError(t, err, "failed to create workspace") + coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, ws.LatestBuild.ID) + }) + } +} + // Blocked by autostart requirements func TestExecutorAutostartBlocked(t *testing.T) { t.Parallel() From 2279385f995dde8f50f48a76cf109c915c2af3ec Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 15:20:10 +0000 Subject: [PATCH 03/10] coderd/wsbuilder: pass template variable values to workspace tag evaluation context --- coderd/wsbuilder/wsbuilder.go | 81 ++++++++++++------------ enterprise/coderd/workspaces_test.go | 3 +- provisioner/terraform/tfparse/tfparse.go | 22 ++++--- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index d59af8cdc1b32..69feca33f46e0 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -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,35 @@ 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 { + 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 +736,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 +745,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 diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index e5d010ce210a7..c20bcba92ffc5 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -1527,7 +1527,7 @@ func TestWorkspaceTagsTerraform(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - ctx := testutil.Context(t, testutil.WaitShort) + ctx := testutil.Context(t, testutil.WaitSuperLong) client, owner := coderdenttest.New(t, &coderdenttest.Options{ Options: &coderdtest.Options{ @@ -1559,7 +1559,6 @@ func TestWorkspaceTagsTerraform(t *testing.T) { }) require.NoError(t, err, "failed to create template version") coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID) - require.NoError(t, err, "failed to create template version") tpl := coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, tv.ID) // Creating a workspace as a non-privileged user must succeed diff --git a/provisioner/terraform/tfparse/tfparse.go b/provisioner/terraform/tfparse/tfparse.go index 0eb6a0094e505..a3a7f971fac2e 100644 --- a/provisioner/terraform/tfparse/tfparse.go +++ b/provisioner/terraform/tfparse/tfparse.go @@ -327,13 +327,13 @@ func (p *Parser) CoderParameterDefaults(ctx context.Context, varsDefaults map[st // Issue #15795: the "default" value could also be an expression we need // to evaluate. // TODO: should we support coder_parameter default values that reference other coder_parameter data sources? - evalCtx := buildEvalContext(varsDefaults, nil) + evalCtx := BuildEvalContext(varsDefaults, nil) val, diags := expr.Value(evalCtx) if diags.HasErrors() { return nil, xerrors.Errorf("failed to evaluate coder_parameter %q default value %q: %s", dataResource.Name, value, diags.Error()) } // Do not use "val.AsString()" as it can panic - strVal, err := ctyValueString(val) + strVal, err := CtyValueString(val) if err != nil { return nil, xerrors.Errorf("failed to marshal coder_parameter %q default value %q as string: %s", dataResource.Name, value, err) } @@ -355,7 +355,7 @@ func evaluateWorkspaceTags(varsDefaults, paramsDefaults, workspaceTags map[strin } // We only add variables and coder_parameter data sources. Anything else will be // undefined and will raise a Terraform error. - evalCtx := buildEvalContext(varsDefaults, paramsDefaults) + evalCtx := BuildEvalContext(varsDefaults, paramsDefaults) tags := make(map[string]string) for workspaceTagKey, workspaceTagValue := range workspaceTags { expr, diags := hclsyntax.ParseExpression([]byte(workspaceTagValue), "expression.hcl", hcl.InitialPos) @@ -369,7 +369,7 @@ func evaluateWorkspaceTags(varsDefaults, paramsDefaults, workspaceTags map[strin } // Do not use "val.AsString()" as it can panic - str, err := ctyValueString(val) + str, err := CtyValueString(val) if err != nil { return nil, xerrors.Errorf("failed to marshal workspace tag key %q value %q as string: %s", workspaceTagKey, workspaceTagValue, err) } @@ -395,16 +395,17 @@ func validWorkspaceTagValues(tags map[string]string) error { return nil } -func buildEvalContext(varDefaults map[string]string, paramDefaults map[string]string) *hcl.EvalContext { +// BuildEvalContext builds an evaluation context for the given variable and parameter defaults. +func BuildEvalContext(vars map[string]string, params map[string]string) *hcl.EvalContext { varDefaultsM := map[string]cty.Value{} - for varName, varDefault := range varDefaults { + for varName, varDefault := range vars { varDefaultsM[varName] = cty.MapVal(map[string]cty.Value{ "value": cty.StringVal(varDefault), }) } paramDefaultsM := map[string]cty.Value{} - for paramName, paramDefault := range paramDefaults { + for paramName, paramDefault := range params { paramDefaultsM[paramName] = cty.MapVal(map[string]cty.Value{ "value": cty.StringVal(paramDefault), }) @@ -496,7 +497,10 @@ func compareSourcePos(x, y tfconfig.SourcePos) bool { return x.Line < y.Line } -func ctyValueString(val cty.Value) (string, error) { +// CtyValueString converts a cty.Value to a string. +// It supports only primitive types - bool, number, and string. +// As a special case, it also supports map[string]interface{} with key "value". +func CtyValueString(val cty.Value) (string, error) { switch val.Type() { case cty.Bool: if val.True() { @@ -514,7 +518,7 @@ func ctyValueString(val cty.Value) (string, error) { if !ok { return "", xerrors.Errorf("map does not have key 'value'") } - return ctyValueString(valval) + return CtyValueString(valval) default: return "", xerrors.Errorf("only primitive types are supported - bool, number, and string") } From f7cb7445620a44799521ad9b5eccf7f1761209d6 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 15:20:53 +0000 Subject: [PATCH 04/10] wsbuilder: make tests pass --- coderd/wsbuilder/wsbuilder_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/coderd/wsbuilder/wsbuilder_test.go b/coderd/wsbuilder/wsbuilder_test.go index 3f373efd3bfdb..bf02d44d96f9c 100644 --- a/coderd/wsbuilder/wsbuilder_test.go +++ b/coderd/wsbuilder/wsbuilder_test.go @@ -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{}), @@ -321,6 +326,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) { withTemplate, withInactiveVersion(richParameters), withLastBuildFound, + withTemplateVersionVariables(inactiveVersionID, nil), withRichParameters(nil), withParameterSchemas(inactiveJobID, nil), withWorkspaceTags(inactiveVersionID, workspaceTags), @@ -459,6 +465,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { withTemplate, withInactiveVersion(richParameters), withLastBuildFound, + withTemplateVersionVariables(inactiveVersionID, nil), withRichParameters(initialBuildParameters), withParameterSchemas(inactiveJobID, nil), withWorkspaceTags(inactiveVersionID, nil), @@ -511,6 +518,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { withTemplate, withInactiveVersion(richParameters), withLastBuildFound, + withTemplateVersionVariables(inactiveVersionID, nil), withRichParameters(nil), withParameterSchemas(inactiveJobID, schemas), withWorkspaceTags(inactiveVersionID, nil), @@ -542,6 +550,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { withTemplate, withInactiveVersion(richParameters), withLastBuildFound, + withTemplateVersionVariables(inactiveVersionID, nil), withRichParameters(initialBuildParameters), withParameterSchemas(inactiveJobID, nil), withWorkspaceTags(inactiveVersionID, nil), @@ -593,6 +602,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { withTemplate, withActiveVersion(version2params), withLastBuildFound, + withTemplateVersionVariables(activeVersionID, nil), withRichParameters(initialBuildParameters), withParameterSchemas(activeJobID, nil), withWorkspaceTags(activeVersionID, nil), @@ -655,6 +665,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { withTemplate, withActiveVersion(version2params), withLastBuildFound, + withTemplateVersionVariables(activeVersionID, nil), withRichParameters(initialBuildParameters), withParameterSchemas(activeJobID, nil), withWorkspaceTags(activeVersionID, nil), @@ -921,6 +932,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). From c6fe518102070af84cc64ff87dc857c5ff680247 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 15:25:57 +0000 Subject: [PATCH 05/10] fixup! wsbuilder: make tests pass --- coderd/wsbuilder/wsbuilder_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/wsbuilder/wsbuilder_test.go b/coderd/wsbuilder/wsbuilder_test.go index bf02d44d96f9c..cde362e7484b1 100644 --- a/coderd/wsbuilder/wsbuilder_test.go +++ b/coderd/wsbuilder/wsbuilder_test.go @@ -419,6 +419,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { withTemplate, withInactiveVersion(richParameters), withLastBuildFound, + withTemplateVersionVariables(inactiveVersionID, nil), withRichParameters(initialBuildParameters), withParameterSchemas(inactiveJobID, nil), withWorkspaceTags(inactiveVersionID, nil), @@ -726,6 +727,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) { withTemplate, withActiveVersion(version2params), withLastBuildFound, + withTemplateVersionVariables(activeVersionID, nil), withRichParameters(initialBuildParameters), withParameterSchemas(activeJobID, nil), withWorkspaceTags(activeVersionID, nil), From 40d3dfe401aef0ab9f2211540a1a6f603c7a6536 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 16:22:27 +0000 Subject: [PATCH 06/10] coderd/wsbuilder: improve test coverage of workspace tags --- coderd/wsbuilder/wsbuilder.go | 7 ++++++- coderd/wsbuilder/wsbuilder_test.go | 33 ++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 69feca33f46e0..2123322356a3c 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -712,7 +712,12 @@ func (b *Builder) getProvisionerTags() (map[string]string, error) { } varsM := make(map[string]string) for _, tv := range tvs { - varsM[tv.Name] = tv.Value + // 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 { diff --git a/coderd/wsbuilder/wsbuilder_test.go b/coderd/wsbuilder/wsbuilder_test.go index cde362e7484b1..d8f25c5a8cda3 100644 --- a/coderd/wsbuilder/wsbuilder_test.go +++ b/coderd/wsbuilder/wsbuilder_test.go @@ -301,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{ @@ -312,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"}, @@ -326,7 +339,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) { withTemplate, withInactiveVersion(richParameters), withLastBuildFound, - withTemplateVersionVariables(inactiveVersionID, nil), + withTemplateVersionVariables(inactiveVersionID, templateVersionVariables), withRichParameters(nil), withParameterSchemas(inactiveJobID, nil), withWorkspaceTags(inactiveVersionID, workspaceTags), @@ -334,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", From e17bceb44bc75f64fdbb4aac756adcac1e63a21e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 16:37:22 +0000 Subject: [PATCH 07/10] fix more tests --- enterprise/coderd/workspaces_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index c20bcba92ffc5..4e11b9f98ec57 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -1480,8 +1480,8 @@ func TestWorkspaceTagsTerraform(t *testing.T) { name: "tag variable", provisionerTags: map[string]string{"foo": "bar"}, tfWorkspaceTags: ` - variable "foo" { - default = "bar" + variable "foo" { + default = "bar" } data "coder_workspace_tags" "tags" { tags = { @@ -1493,8 +1493,8 @@ func TestWorkspaceTagsTerraform(t *testing.T) { name: "tag param", provisionerTags: map[string]string{"foo": "bar"}, tfWorkspaceTags: ` - data "coder_parameter" "foo" { - name = "foo" + data "coder_parameter" "foo" { + name = "foo" type = "string" default = "bar" } @@ -1509,7 +1509,7 @@ func TestWorkspaceTagsTerraform(t *testing.T) { provisionerTags: map[string]string{"foo": "bar"}, tfWorkspaceTags: ` variable "foo" { - type = "string" + type = string default = "bar" } data "coder_parameter" "foo" { From cf7d67e8c8dfc9c96f95a55fd4e935c092f72390 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 17:58:11 +0000 Subject: [PATCH 08/10] broaden test coverage --- enterprise/coderd/workspaces_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 4e11b9f98ec57..0550930b7745e 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -1523,6 +1523,32 @@ func TestWorkspaceTagsTerraform(t *testing.T) { } }`, }, + { + name: "override no tags", + provisionerTags: map[string]string{"foo": "baz"}, + createTemplateVersionRequestTags: map[string]string{"foo": "baz"}, + tfWorkspaceTags: ``, + }, + { + name: "override empty tags", + provisionerTags: map[string]string{"foo": "baz"}, + createTemplateVersionRequestTags: map[string]string{"foo": "baz"}, + tfWorkspaceTags: ` + data "coder_workspace_tags" "tags" { + tags = {} + }`, + }, + { + name: "does not override static tag", + provisionerTags: map[string]string{"foo": "bar"}, + createTemplateVersionRequestTags: map[string]string{"foo": "baz"}, + tfWorkspaceTags: ` + data "coder_workspace_tags" "tags" { + tags = { + "foo" = "bar" + } + }`, + }, } { tc := tc t.Run(tc.name, func(t *testing.T) { From 5b3c33a5f690df4641e40e6f10dc066c2297a2b4 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 18:10:06 +0000 Subject: [PATCH 09/10] formatting --- enterprise/coderd/workspaces_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 0550930b7745e..0c1c4031404eb 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -1508,12 +1508,12 @@ func TestWorkspaceTagsTerraform(t *testing.T) { name: "tag param with default from var", provisionerTags: map[string]string{"foo": "bar"}, tfWorkspaceTags: ` - variable "foo" { + variable "foo" { type = string default = "bar" } - data "coder_parameter" "foo" { - name = "foo" + data "coder_parameter" "foo" { + name = "foo" type = "string" default = var.foo } From 82d8c93edda59172a3d58676f822098f63638b31 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Tue, 17 Dec 2024 21:00:23 +0000 Subject: [PATCH 10/10] address PR comments --- .../coderd/coderdenttest/coderdenttest.go | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index 1125ab9128e4c..0d44937e4a82d 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -7,6 +7,7 @@ import ( "crypto/tls" "io" "net/http" + "os/exec" "strings" "testing" "time" @@ -326,7 +327,7 @@ func NewExternalProvisionerDaemonTerraform(t testing.TB, client *codersdk.Client } // nolint // This function is a helper for tests and should not be linted. -func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string, ty codersdk.ProvisionerType) io.Closer { +func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uuid.UUID, tags map[string]string, provisionerType codersdk.ProvisionerType) io.Closer { t.Helper() entitlements, err := client.Entitlements(context.Background()) @@ -353,39 +354,44 @@ func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui <-serveDone }) - var serveFunc func() - switch ty { + switch provisionerType { case codersdk.ProvisionerTypeTerraform: - serveFunc = func() { + // Ensure the Terraform binary is present in the path. + // If not, we fail this test rather than downloading it. + terraformPath, err := exec.LookPath("terraform") + require.NoError(t, err, "terraform binary not found in PATH") + t.Logf("using Terraform binary at %s", terraformPath) + + go func() { defer close(serveDone) assert.NoError(t, terraform.Serve(ctx, &terraform.ServeOptions{ + BinaryPath: terraformPath, + CachePath: t.TempDir(), ServeOptions: &provisionersdk.ServeOptions{ Listener: provisionerSrv, WorkDirectory: t.TempDir(), }, })) - } + }() case codersdk.ProvisionerTypeEcho: - serveFunc = func() { + go func() { defer close(serveDone) assert.NoError(t, echo.Serve(ctx, &provisionersdk.ServeOptions{ Listener: provisionerSrv, WorkDirectory: t.TempDir(), })) - } + }() default: - t.Fatalf("unsupported provisioner type: %s", ty) + t.Fatalf("unsupported provisioner type: %s", provisionerType) return nil } - go serveFunc() - daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), Name: t.Name(), Organization: org, - Provisioners: []codersdk.ProvisionerType{ty}, + Provisioners: []codersdk.ProvisionerType{provisionerType}, Tags: tags, }) }, &provisionerd.Options{ @@ -393,7 +399,7 @@ func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui UpdateInterval: 250 * time.Millisecond, ForceCancelInterval: 5 * time.Second, Connector: provisionerd.LocalProvisioners{ - string(ty): sdkproto.NewDRPCProvisionerClient(provisionerClient), + string(provisionerType): sdkproto.NewDRPCProvisionerClient(provisionerClient), }, }) closer := coderdtest.NewProvisionerDaemonCloser(daemon) 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