mirror of
https://github.com/coder/coder.git
synced 2025-03-14 10:09:57 +00:00
@ -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) {
|
||||
|
@ -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`
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
17
go.mod
17
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
|
||||
)
|
||||
|
22
go.sum
22
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=
|
||||
|
162
provisioner/terraform/tfparse/funcs.go
Normal file
162
provisioner/terraform/tfparse/funcs.go
Normal file
@ -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)
|
||||
}
|
@ -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)
|
||||
|
@ -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",
|
||||
|
Reference in New Issue
Block a user