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", 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