diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 75c05c6af76b2..b1bfed79bc797 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -429,9 +429,8 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { } }`, }, - reqTags: map[string]string{"a": "b"}, - // wantTags: map[string]string{"owner": "", "scope": "organization", "a": "b"}, - expectError: `provisioner tag "a" evaluated to an empty value`, + reqTags: map[string]string{"a": "b"}, + wantTags: map[string]string{"owner": "", "scope": "organization", "a": "b"}, }, { name: "main.tf with disallowed workspace tag value", @@ -489,11 +488,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { "foo": "bar", "a": var.a, "b": data.coder_parameter.b.value, - "test": try(null_resource.test.name, "whatever"), + "test": pathexpand("~/file.txt"), } }`, }, - expectError: `Function calls not allowed; Functions may not be called here.`, + expectError: `function "pathexpand" may not be used here`, }, // We will allow coder_workspace_tags to set the scope on a template version import job // BUT the user ID will be ultimately determined by the API key in the scope. @@ -568,6 +567,42 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { }, wantTags: map[string]string{"owner": "", "scope": "organization"}, }, + { + name: "main.tf with tags from parameter with default value from variable no default", + files: map[string]string{ + `main.tf`: ` + variable "provisioner" { + type = string + } + variable "default_provisioner" { + type = string + default = "" # intentionally blank, set on template creation + } + data "coder_parameter" "provisioner" { + name = "provisioner" + mutable = false + default = var.default_provisioner + dynamic "option" { + for_each = toset(split(",", var.provisioner)) + content { + name = option.value + value = option.value + } + } + } + data "coder_workspace_tags" "tags" { + tags = { + "provisioner" : data.coder_parameter.provisioner.value + } + }`, + }, + reqTags: map[string]string{ + "provisioner": "alpha", + }, + wantTags: map[string]string{ + "provisioner": "alpha", "owner": "", "scope": "organization", + }, + }, } { tt := tt t.Run(tt.name, func(t *testing.T) { diff --git a/docs/admin/templates/extending-templates/workspace-tags.md b/docs/admin/templates/extending-templates/workspace-tags.md index 83ea983ce72ba..a684d789465c5 100644 --- a/docs/admin/templates/extending-templates/workspace-tags.md +++ b/docs/admin/templates/extending-templates/workspace-tags.md @@ -62,11 +62,6 @@ variables and parameters. This is illustrated in the table below: ## Constraints -### Default Values - -All template variables and `coder_parameter` data sources **must** provide a -default value. Failure to do so will result in an error. - ### Tagged provisioners It is possible to choose tag combinations that no provisioner can handle. This @@ -127,6 +122,6 @@ variables, and references to other resources. **Not supported** -- Function calls: `try(var.foo, "default")` +- Function calls that reference files on disk: `abspath`, `file*`, `pathexpand` - Resources: `compute_instance.dev.name` - Data sources other than `coder_parameter`: `data.local_file.hostname.content` diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index 0d44937e4a82d..d76722b5bac1a 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -389,7 +389,7 @@ func newExternalProvisionerDaemon(t testing.TB, client *codersdk.Client, org uui daemon := provisionerd.New(func(ctx context.Context) (provisionerdproto.DRPCProvisionerDaemonClient, error) { return client.ServeProvisionerDaemon(ctx, codersdk.ServeProvisionerDaemonRequest{ ID: uuid.New(), - Name: t.Name(), + Name: testutil.GetRandomName(t), Organization: org, Provisioners: []codersdk.ProvisionerType{provisionerType}, Tags: tags, diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go index 9efb002a8e910..ff3ce625707ee 100644 --- a/enterprise/coderd/provisionerdaemons_test.go +++ b/enterprise/coderd/provisionerdaemons_test.go @@ -285,7 +285,7 @@ func TestProvisionerDaemonServe(t *testing.T) { daemons, err := client.ProvisionerDaemons(context.Background()) assert.NoError(t, err, "failed to get provisioner daemons") return len(daemons) > 0 && - assert.Equal(t, t.Name(), daemons[0].Name) && + assert.NotEmpty(t, daemons[0].Name) && assert.Equal(t, provisionersdk.ScopeUser, daemons[0].Tags[provisionersdk.TagScope]) && assert.Equal(t, user.UserID.String(), daemons[0].Tags[provisionersdk.TagOwner]) }, testutil.WaitShort, testutil.IntervalMedium) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index d074444bee10a..ea479ec673152 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -1217,8 +1217,11 @@ func TestWorkspaceTagsTerraform(t *testing.T) { createTemplateVersionRequestTags map[string]string // the coder_workspace_tags bit of main.tf. // you can add more stuff here if you need - tfWorkspaceTags string - skipCreateWorkspace bool + tfWorkspaceTags string + templateImportUserVariableValues []codersdk.VariableValue + // if we need to set parameters on workspace build + workspaceBuildParameters []codersdk.WorkspaceBuildParameter + skipCreateWorkspace bool }{ { name: "no tags", @@ -1318,6 +1321,38 @@ func TestWorkspaceTagsTerraform(t *testing.T) { // matching tag foo=bar. skipCreateWorkspace: true, }, + { + name: "overrides with dynamic option from var", + provisionerTags: map[string]string{"foo": "bar"}, + createTemplateVersionRequestTags: map[string]string{"foo": "bar"}, + templateImportUserVariableValues: []codersdk.VariableValue{{Name: "default_foo", Value: "baz"}, {Name: "foo", Value: "bar,baz"}}, + workspaceBuildParameters: []codersdk.WorkspaceBuildParameter{{Name: "foo", Value: "bar"}}, + tfWorkspaceTags: ` + variable "default_foo" { + type = string + } + variable "foo" { + type = string + } + data "coder_parameter" "foo" { + name = "foo" + type = "string" + default = var.default_foo + mutable = false + dynamic "option" { + for_each = toset(split(",", var.foo)) + content { + name = option.value + value = option.value + } + } + } + data "coder_workspace_tags" "tags" { + tags = { + "foo" = data.coder_parameter.foo.value + } + }`, + }, } { tc := tc t.Run(tc.name, func(t *testing.T) { @@ -1346,11 +1381,12 @@ func TestWorkspaceTagsTerraform(t *testing.T) { 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, + Name: testutil.GetRandomName(t), + FileID: fi.ID, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + Provisioner: codersdk.ProvisionerTypeTerraform, + ProvisionerTags: tc.createTemplateVersionRequestTags, + UserVariableValues: tc.templateImportUserVariableValues, }) require.NoError(t, err, "failed to create template version") coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, tv.ID) @@ -1359,8 +1395,9 @@ func TestWorkspaceTagsTerraform(t *testing.T) { if !tc.skipCreateWorkspace { // 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), + TemplateID: tpl.ID, + Name: coderdtest.RandomUsername(t), + RichParameterValues: tc.workspaceBuildParameters, }) require.NoError(t, err, "failed to create workspace") coderdtest.AwaitWorkspaceBuildJobCompleted(t, member, ws.LatestBuild.ID) diff --git a/examples/workspace-tags/README.md b/examples/workspace-tags/README.md index 7a42bc05d1825..3a4e53c273269 100644 --- a/examples/workspace-tags/README.md +++ b/examples/workspace-tags/README.md @@ -7,7 +7,7 @@ icon: /icon/docker.png # Overview -This Coder template presents use of [Workspace Tags](https://coder.com/docs/templates/workspace-tags) [Coder Parameters](https://coder.com/docs/templates/parameters). +This Coder template presents use of [Workspace Tags](https://coder.com/docs/admin/templates/extending-templates/workspace-tags) and [Coder Parameters](https://coder.com/docs/templates/parameters). # Use case @@ -18,10 +18,7 @@ By using `coder_workspace_tags` and `coder_parameter`s, template administrators # Notes - You will need to have an [external provisioner](https://coder.com/docs/admin/provisioners#external-provisioners) with the correct tagset running in order to import this template. -- When specifying values for the `coder_workspace_tags` data source, you are restricted to using a subset of Terraform's capabilities. -- You must specify default values for all data sources and variables referenced by the `coder_workspace_tags` data source. - -See [Workspace Tags](https://coder.com/docs/templates/workspace-tags) for more information. +- When specifying values for the `coder_workspace_tags` data source, you are restricted to using a subset of Terraform's capabilities. See [here](https://coder.com/docs/admin/templates/extending-templates/workspace-tags) for more details. ## Development diff --git a/go.mod b/go.mod index ef1f48d5a72d6..08ef6698a650f 100644 --- a/go.mod +++ b/go.mod @@ -256,9 +256,9 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/DataDog/appsec-internal-go v1.8.0 // indirect - github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect - github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0 // indirect - github.com/DataDog/datadog-go/v5 v5.3.0 // indirect + github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0 // indirect + github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0 // indirect + github.com/DataDog/datadog-go/v5 v5.5.0 // indirect github.com/DataDog/go-tuf v1.1.0-0.5.2 // indirect github.com/DataDog/gostackparse v0.7.0 // indirect github.com/DataDog/sketches-go v1.4.5 // indirect @@ -444,3 +444,14 @@ require ( inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) + +require ( + github.com/aquasecurity/trivy-iac v0.8.0 + github.com/zclconf/go-cty-yaml v1.0.3 +) + +require ( + github.com/DataDog/go-sqllexer v0.0.14 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect +) diff --git a/go.sum b/go.sum index 8d3e51d71f7ca..2ce346748b303 100644 --- a/go.sum +++ b/go.sum @@ -26,14 +26,16 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/appsec-internal-go v1.8.0 h1:1Tfn3LEogntRqZtf88twSApOCAAO3V+NILYhuQIo4J4= github.com/DataDog/appsec-internal-go v1.8.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g= -github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8= -github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo= -github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0 h1:LplNAmMgZvGU7kKA0+4c1xWOjz828xweW5TCi8Mw9Q0= -github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.57.0/go.mod h1:4Vo3SJ24uzfKHUHLoFa8t8o+LH+7TCQ7sPcZDtOpSP4= -github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8= -github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0 h1:nOrRNCHyriM/EjptMrttFOQhRSmvfagESdpyknb5VPg= +github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0/go.mod h1:MfDvphBMmEMwE3a30h27AtPO7OzmvdoVTiGY1alEmo4= +github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0 h1:5hGO0Z8ih0bRojuq+1ZwLFtdgsfO3TqIjbwJAH12sOQ= +github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0/go.mod h1:jN5BsZI+VilHJV1Wac/efGxS4TPtXa1Lh9SiUyv93F4= +github.com/DataDog/datadog-go/v5 v5.5.0 h1:G5KHeB8pWBNXT4Jtw0zAkhdxEAWSpWH00geHI6LDrKU= +github.com/DataDog/datadog-go/v5 v5.5.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= github.com/DataDog/go-libddwaf/v3 v3.4.0 h1:NJ2W2vhYaOm1OWr1LJCbdgp7ezG/XLJcQKBmjFwhSuM= github.com/DataDog/go-libddwaf/v3 v3.4.0/go.mod h1:n98d9nZ1gzenRSk53wz8l6d34ikxS+hs62A31Fqmyi4= +github.com/DataDog/go-sqllexer v0.0.14 h1:xUQh2tLr/95LGxDzLmttLgTo/1gzFeOyuwrQa/Iig4Q= +github.com/DataDog/go-sqllexer v0.0.14/go.mod h1:KwkYhpFEVIq+BfobkTC1vfqm4gTi65skV/DpDBXtexc= github.com/DataDog/go-tuf v1.1.0-0.5.2 h1:4CagiIekonLSfL8GMHRHcHudo1fQnxELS9g4tiAupQ4= github.com/DataDog/go-tuf v1.1.0-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0= github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= @@ -76,11 +78,15 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/aquasecurity/trivy-iac v0.8.0 h1:NKFhk/BTwQ0jIh4t74V8+6UIGUvPlaxO9HPlSMQi3fo= +github.com/aquasecurity/trivy-iac v0.8.0/go.mod h1:ARiMeNqcaVWOXJmp8hmtMnNm/Jd836IOmDBUW5r4KEk= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= @@ -157,6 +163,8 @@ github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM= @@ -953,6 +961,8 @@ github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-yaml v1.0.3 h1:og/eOQ7lvA/WWhHGFETVWNduJM7Rjsv2RRpx1sdFMLc= +github.com/zclconf/go-cty-yaml v1.0.3/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= diff --git a/provisioner/terraform/tfparse/funcs.go b/provisioner/terraform/tfparse/funcs.go new file mode 100644 index 0000000000000..84009a44e3061 --- /dev/null +++ b/provisioner/terraform/tfparse/funcs.go @@ -0,0 +1,162 @@ +package tfparse + +import ( + "github.com/aquasecurity/trivy-iac/pkg/scanners/terraform/parser/funcs" + "github.com/hashicorp/hcl/v2/ext/tryfunc" + ctyyaml "github.com/zclconf/go-cty-yaml" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/function/stdlib" + "golang.org/x/xerrors" +) + +// Functions returns a set of functions that are safe to use in the context of +// evaluating Terraform expressions without any ability to reference local files. +// Functions that refer to file operations are replaced with stubs that return a +// descriptive error to the user. +func Functions() map[string]function.Function { + return allFunctions +} + +var ( + // Adapted from github.com/aquasecurity/trivy-iac@v0.8.0/pkg/scanners/terraform/parser/functions.go + // We cannot support all available functions here, as the result of reading a file will be different + // depending on the execution environment. + safeFunctions = map[string]function.Function{ + "abs": stdlib.AbsoluteFunc, + "basename": funcs.BasenameFunc, + "base64decode": funcs.Base64DecodeFunc, + "base64encode": funcs.Base64EncodeFunc, + "base64gzip": funcs.Base64GzipFunc, + "base64sha256": funcs.Base64Sha256Func, + "base64sha512": funcs.Base64Sha512Func, + "bcrypt": funcs.BcryptFunc, + "can": tryfunc.CanFunc, + "ceil": stdlib.CeilFunc, + "chomp": stdlib.ChompFunc, + "cidrhost": funcs.CidrHostFunc, + "cidrnetmask": funcs.CidrNetmaskFunc, + "cidrsubnet": funcs.CidrSubnetFunc, + "cidrsubnets": funcs.CidrSubnetsFunc, + "coalesce": funcs.CoalesceFunc, + "coalescelist": stdlib.CoalesceListFunc, + "compact": stdlib.CompactFunc, + "concat": stdlib.ConcatFunc, + "contains": stdlib.ContainsFunc, + "csvdecode": stdlib.CSVDecodeFunc, + "dirname": funcs.DirnameFunc, + "distinct": stdlib.DistinctFunc, + "element": stdlib.ElementFunc, + "chunklist": stdlib.ChunklistFunc, + "flatten": stdlib.FlattenFunc, + "floor": stdlib.FloorFunc, + "format": stdlib.FormatFunc, + "formatdate": stdlib.FormatDateFunc, + "formatlist": stdlib.FormatListFunc, + "indent": stdlib.IndentFunc, + "index": funcs.IndexFunc, // stdlib.IndexFunc is not compatible + "join": stdlib.JoinFunc, + "jsondecode": stdlib.JSONDecodeFunc, + "jsonencode": stdlib.JSONEncodeFunc, + "keys": stdlib.KeysFunc, + "length": funcs.LengthFunc, + "list": funcs.ListFunc, + "log": stdlib.LogFunc, + "lookup": funcs.LookupFunc, + "lower": stdlib.LowerFunc, + "map": funcs.MapFunc, + "matchkeys": funcs.MatchkeysFunc, + "max": stdlib.MaxFunc, + "md5": funcs.Md5Func, + "merge": stdlib.MergeFunc, + "min": stdlib.MinFunc, + "parseint": stdlib.ParseIntFunc, + "pow": stdlib.PowFunc, + "range": stdlib.RangeFunc, + "regex": stdlib.RegexFunc, + "regexall": stdlib.RegexAllFunc, + "replace": funcs.ReplaceFunc, + "reverse": stdlib.ReverseListFunc, + "rsadecrypt": funcs.RsaDecryptFunc, + "setintersection": stdlib.SetIntersectionFunc, + "setproduct": stdlib.SetProductFunc, + "setsubtract": stdlib.SetSubtractFunc, + "setunion": stdlib.SetUnionFunc, + "sha1": funcs.Sha1Func, + "sha256": funcs.Sha256Func, + "sha512": funcs.Sha512Func, + "signum": stdlib.SignumFunc, + "slice": stdlib.SliceFunc, + "sort": stdlib.SortFunc, + "split": stdlib.SplitFunc, + "strrev": stdlib.ReverseFunc, + "substr": stdlib.SubstrFunc, + "timestamp": funcs.TimestampFunc, + "timeadd": stdlib.TimeAddFunc, + "title": stdlib.TitleFunc, + "tostring": funcs.MakeToFunc(cty.String), + "tonumber": funcs.MakeToFunc(cty.Number), + "tobool": funcs.MakeToFunc(cty.Bool), + "toset": funcs.MakeToFunc(cty.Set(cty.DynamicPseudoType)), + "tolist": funcs.MakeToFunc(cty.List(cty.DynamicPseudoType)), + "tomap": funcs.MakeToFunc(cty.Map(cty.DynamicPseudoType)), + "transpose": funcs.TransposeFunc, + "trim": stdlib.TrimFunc, + "trimprefix": stdlib.TrimPrefixFunc, + "trimspace": stdlib.TrimSpaceFunc, + "trimsuffix": stdlib.TrimSuffixFunc, + "try": tryfunc.TryFunc, + "upper": stdlib.UpperFunc, + "urlencode": funcs.URLEncodeFunc, + "uuid": funcs.UUIDFunc, + "uuidv5": funcs.UUIDV5Func, + "values": stdlib.ValuesFunc, + "yamldecode": ctyyaml.YAMLDecodeFunc, + "yamlencode": ctyyaml.YAMLEncodeFunc, + "zipmap": stdlib.ZipmapFunc, + } + + // the below functions are not safe for usage in the context of tfparse, as their return + // values may change depending on the underlying filesystem. + stubFileFunctions = map[string]function.Function{ + "abspath": makeStubFunction("abspath", cty.String, function.Parameter{Name: "path", Type: cty.String}), + "file": makeStubFunction("file", cty.String, function.Parameter{Name: "path", Type: cty.String}), + "fileexists": makeStubFunction("fileexists", cty.String, function.Parameter{Name: "path", Type: cty.String}), + "fileset": makeStubFunction("fileset", cty.String, function.Parameter{Name: "path", Type: cty.String}, function.Parameter{Name: "pattern", Type: cty.String}), + "filebase64": makeStubFunction("filebase64", cty.String, function.Parameter{Name: "path", Type: cty.String}, function.Parameter{Name: "pattern", Type: cty.String}), + "filebase64sha256": makeStubFunction("filebase64sha256", cty.String, function.Parameter{Name: "path", Type: cty.String}), + "filebase64sha512": makeStubFunction("filebase64sha512", cty.String, function.Parameter{Name: "path", Type: cty.String}), + "filemd5": makeStubFunction("filemd5", cty.String, function.Parameter{Name: "path", Type: cty.String}), + "filesha1": makeStubFunction("filesha1", cty.String, function.Parameter{Name: "path", Type: cty.String}), + "filesha256": makeStubFunction("filesha256", cty.String, function.Parameter{Name: "path", Type: cty.String}), + "filesha512": makeStubFunction("filesha512", cty.String, function.Parameter{Name: "path", Type: cty.String}), + "pathexpand": makeStubFunction("pathexpand", cty.String, function.Parameter{Name: "path", Type: cty.String}), + } + + allFunctions = mergeMaps(safeFunctions, stubFileFunctions) +) + +// mergeMaps returns a new map which is the result of merging each key and value +// of all maps in ms, in order. Successive maps may override values of previous +// maps. +func mergeMaps[K, V comparable](ms ...map[K]V) map[K]V { + merged := make(map[K]V) + for _, m := range ms { + for k, v := range m { + merged[k] = v + } + } + return merged +} + +// makeStubFunction returns a function.Function with the required return type and parameters +// that will always return an unknown type and an error. +func makeStubFunction(name string, returnType cty.Type, params ...function.Parameter) function.Function { + var spec function.Spec + spec.Params = params + spec.Type = function.StaticReturnType(returnType) + spec.Impl = func(_ []cty.Value, _ cty.Type) (cty.Value, error) { + return cty.UnknownVal(returnType), xerrors.Errorf("function %q may not be used here", name) + } + return function.New(&spec) +} diff --git a/provisioner/terraform/tfparse/tfparse.go b/provisioner/terraform/tfparse/tfparse.go index de767a833207f..281ce55f99146 100644 --- a/provisioner/terraform/tfparse/tfparse.go +++ b/provisioner/terraform/tfparse/tfparse.go @@ -239,13 +239,6 @@ func (p *Parser) WorkspaceTagDefaults(ctx context.Context) (map[string]string, e return nil, xerrors.Errorf("eval provisioner tags: %w", err) } - // Ensure that none of the tag values are empty after evaluation. - for k, v := range evalTags { - if len(strings.TrimSpace(v)) > 0 { - continue - } - return nil, xerrors.Errorf("provisioner tag %q evaluated to an empty value, please set a default value", k) - } return evalTags, nil } @@ -477,7 +470,7 @@ func BuildEvalContext(vars map[string]string, params map[string]string) *hcl.Eva // The default function map for Terraform is not exposed, so we would essentially // have to re-implement or copy the entire map or a subset thereof. // ref: https://github.com/hashicorp/terraform/blob/e044e569c5bc81f82e9a4d7891f37c6fbb0a8a10/internal/lang/functions.go#L54 - Functions: nil, + Functions: Functions(), } if len(varDefaultsM) != 0 { evalCtx.Variables["var"] = cty.MapVal(varDefaultsM) diff --git a/provisioner/terraform/tfparse/tfparse_test.go b/provisioner/terraform/tfparse/tfparse_test.go index afbec4d0b8d4b..ceefc484b2169 100644 --- a/provisioner/terraform/tfparse/tfparse_test.go +++ b/provisioner/terraform/tfparse/tfparse_test.go @@ -268,7 +268,7 @@ func Test_WorkspaceTagDefaultsFromFile(t *testing.T) { } }`, }, - expectError: `provisioner tag "az" evaluated to an empty value, please set a default value`, + expectTags: map[string]string{"cluster": "developers", "az": "", "platform": "kubernetes", "region": "us"}, }, { name: "main.tf with missing parameter default value outside workspace tags", @@ -416,13 +416,52 @@ func Test_WorkspaceTagDefaultsFromFile(t *testing.T) { expectError: `There is no variable named "foo_bar"`, }, { - name: "main.tf with functions in workspace tags", + name: "main.tf with allowed functions in workspace tags", files: map[string]string{ "main.tf": ` provider "foo" {} resource "foo_bar" "baz" { name = "foobar" } + locals { + some_path = pathexpand("file.txt") + } + variable "region" { + type = string + default = "us" + } + data "coder_parameter" "unrelated" { + name = "unrelated" + type = "list(string)" + default = jsonencode(["a", "b"]) + } + data "coder_parameter" "az" { + name = "az" + type = "string" + default = "a" + } + data "coder_workspace_tags" "tags" { + tags = { + "platform" = "kubernetes", + "cluster" = "${"devel"}${"opers"}" + "region" = try(split(".", var.region)[1], "placeholder") + "az" = try(split(".", data.coder_parameter.az.value)[1], "placeholder") + } + }`, + }, + expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "placeholder", "az": "placeholder"}, + }, + { + name: "main.tf with disallowed functions in workspace tags", + files: map[string]string{ + "main.tf": ` + provider "foo" {} + resource "foo_bar" "baz" { + name = "foobar" + } + locals { + some_path = pathexpand("file.txt") + } variable "region" { type = string default = "region.us" @@ -443,11 +482,12 @@ func Test_WorkspaceTagDefaultsFromFile(t *testing.T) { "cluster" = "${"devel"}${"opers"}" "region" = try(split(".", var.region)[1], "placeholder") "az" = try(split(".", data.coder_parameter.az.value)[1], "placeholder") + "some_path" = pathexpand("~/file.txt") } }`, }, expectTags: nil, - expectError: `Function calls not allowed; Functions may not be called here.`, + expectError: `function "pathexpand" may not be used here`, }, { name: "supported types",