diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index d59af8cdc1b32..2123322356a3c 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,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 diff --git a/coderd/wsbuilder/wsbuilder_test.go b/coderd/wsbuilder/wsbuilder_test.go index 3f373efd3bfdb..d8f25c5a8cda3 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{}), @@ -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). diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index b397cb05dc5d4..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" @@ -16,7 +17,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 +30,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 +307,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, provisionerType 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 +339,67 @@ 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) - }() + + switch provisionerType { + case codersdk.ProvisionerTypeTerraform: + // 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: + 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", provisionerType) + return nil + } 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{provisionerType}, 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(provisionerType): sdkproto.NewDRPCProvisionerClient(provisionerClient), }, }) closer := coderdtest.NewProvisionerDaemonCloser(daemon) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index e5142c1a83ee8..0c1c4031404eb 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,182 @@ 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 + } + }`, + }, + { + 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) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitSuperLong) + + 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) + 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() 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") } 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