Skip to content

Commit 4ba0b39

Browse files
authored
feat(provisioner/terraform/tfparse): add support for built-in Terraform functions (#16183)
Relates to #15977 Adds support for some functions in `tfparse` (only functions that do not reference local files). NOTE: for now, I'm importing trivy-iac. If we prefer to avoid a little dependency, I can do a little copying instead.
1 parent 7bef4df commit 4ba0b39

File tree

6 files changed

+222
-5
lines changed

6 files changed

+222
-5
lines changed

coderd/templateversions_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -489,11 +489,11 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
489489
"foo": "bar",
490490
"a": var.a,
491491
"b": data.coder_parameter.b.value,
492-
"test": try(null_resource.test.name, "whatever"),
492+
"test": pathexpand("~/file.txt"),
493493
}
494494
}`,
495495
},
496-
expectError: `Function calls not allowed; Functions may not be called here.`,
496+
expectError: `function "pathexpand" may not be used here`,
497497
},
498498
// We will allow coder_workspace_tags to set the scope on a template version import job
499499
// BUT the user ID will be ultimately determined by the API key in the scope.

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,11 @@ require (
437437
sigs.k8s.io/yaml v1.4.0 // indirect
438438
)
439439

440+
require (
441+
github.com/aquasecurity/trivy-iac v0.8.0
442+
github.com/zclconf/go-cty-yaml v1.0.3
443+
)
444+
440445
require (
441446
github.com/DataDog/datadog-agent/pkg/proto v0.58.0 // indirect
442447
github.com/DataDog/datadog-agent/pkg/trace v0.58.0 // indirect
@@ -445,6 +450,8 @@ require (
445450
github.com/DataDog/go-runtime-metrics-internal v0.0.0-20241106155157-194426bbbd59 // indirect
446451
github.com/DataDog/go-sqllexer v0.0.14 // indirect
447452
github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 // indirect
453+
github.com/apparentlymart/go-cidr v1.1.0 // indirect
454+
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
448455
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect
449456
github.com/json-iterator/go v1.1.12 // indirect
450457
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,13 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X
8888
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
8989
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
9090
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
91+
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
92+
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
9193
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
9294
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
9395
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
96+
github.com/aquasecurity/trivy-iac v0.8.0 h1:NKFhk/BTwQ0jIh4t74V8+6UIGUvPlaxO9HPlSMQi3fo=
97+
github.com/aquasecurity/trivy-iac v0.8.0/go.mod h1:ARiMeNqcaVWOXJmp8hmtMnNm/Jd836IOmDBUW5r4KEk=
9498
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
9599
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
96100
github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs=
@@ -167,6 +171,8 @@ github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
167171
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
168172
github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
169173
github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
174+
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
175+
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
170176
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
171177
github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
172178
github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM=
@@ -965,6 +971,8 @@ github.com/zclconf/go-cty v1.16.0 h1:xPKEhst+BW5D0wxebMZkxgapvOE/dw7bFTlgSc9nD6w
965971
github.com/zclconf/go-cty v1.16.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
966972
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
967973
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
974+
github.com/zclconf/go-cty-yaml v1.0.3 h1:og/eOQ7lvA/WWhHGFETVWNduJM7Rjsv2RRpx1sdFMLc=
975+
github.com/zclconf/go-cty-yaml v1.0.3/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
968976
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
969977
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
970978
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package tfparse
2+
3+
import (
4+
"github.com/aquasecurity/trivy-iac/pkg/scanners/terraform/parser/funcs"
5+
"github.com/hashicorp/hcl/v2/ext/tryfunc"
6+
ctyyaml "github.com/zclconf/go-cty-yaml"
7+
"github.com/zclconf/go-cty/cty"
8+
"github.com/zclconf/go-cty/cty/function"
9+
"github.com/zclconf/go-cty/cty/function/stdlib"
10+
"golang.org/x/xerrors"
11+
)
12+
13+
// Functions returns a set of functions that are safe to use in the context of
14+
// evaluating Terraform expressions without any ability to reference local files.
15+
// Functions that refer to file operations are replaced with stubs that return a
16+
// descriptive error to the user.
17+
func Functions() map[string]function.Function {
18+
return allFunctions
19+
}
20+
21+
var (
22+
// Adapted from github.com/aquasecurity/trivy-iac@v0.8.0/pkg/scanners/terraform/parser/functions.go
23+
// We cannot support all available functions here, as the result of reading a file will be different
24+
// depending on the execution environment.
25+
safeFunctions = map[string]function.Function{
26+
"abs": stdlib.AbsoluteFunc,
27+
"basename": funcs.BasenameFunc,
28+
"base64decode": funcs.Base64DecodeFunc,
29+
"base64encode": funcs.Base64EncodeFunc,
30+
"base64gzip": funcs.Base64GzipFunc,
31+
"base64sha256": funcs.Base64Sha256Func,
32+
"base64sha512": funcs.Base64Sha512Func,
33+
"bcrypt": funcs.BcryptFunc,
34+
"can": tryfunc.CanFunc,
35+
"ceil": stdlib.CeilFunc,
36+
"chomp": stdlib.ChompFunc,
37+
"cidrhost": funcs.CidrHostFunc,
38+
"cidrnetmask": funcs.CidrNetmaskFunc,
39+
"cidrsubnet": funcs.CidrSubnetFunc,
40+
"cidrsubnets": funcs.CidrSubnetsFunc,
41+
"coalesce": funcs.CoalesceFunc,
42+
"coalescelist": stdlib.CoalesceListFunc,
43+
"compact": stdlib.CompactFunc,
44+
"concat": stdlib.ConcatFunc,
45+
"contains": stdlib.ContainsFunc,
46+
"csvdecode": stdlib.CSVDecodeFunc,
47+
"dirname": funcs.DirnameFunc,
48+
"distinct": stdlib.DistinctFunc,
49+
"element": stdlib.ElementFunc,
50+
"chunklist": stdlib.ChunklistFunc,
51+
"flatten": stdlib.FlattenFunc,
52+
"floor": stdlib.FloorFunc,
53+
"format": stdlib.FormatFunc,
54+
"formatdate": stdlib.FormatDateFunc,
55+
"formatlist": stdlib.FormatListFunc,
56+
"indent": stdlib.IndentFunc,
57+
"index": funcs.IndexFunc, // stdlib.IndexFunc is not compatible
58+
"join": stdlib.JoinFunc,
59+
"jsondecode": stdlib.JSONDecodeFunc,
60+
"jsonencode": stdlib.JSONEncodeFunc,
61+
"keys": stdlib.KeysFunc,
62+
"length": funcs.LengthFunc,
63+
"list": funcs.ListFunc,
64+
"log": stdlib.LogFunc,
65+
"lookup": funcs.LookupFunc,
66+
"lower": stdlib.LowerFunc,
67+
"map": funcs.MapFunc,
68+
"matchkeys": funcs.MatchkeysFunc,
69+
"max": stdlib.MaxFunc,
70+
"md5": funcs.Md5Func,
71+
"merge": stdlib.MergeFunc,
72+
"min": stdlib.MinFunc,
73+
"parseint": stdlib.ParseIntFunc,
74+
"pow": stdlib.PowFunc,
75+
"range": stdlib.RangeFunc,
76+
"regex": stdlib.RegexFunc,
77+
"regexall": stdlib.RegexAllFunc,
78+
"replace": funcs.ReplaceFunc,
79+
"reverse": stdlib.ReverseListFunc,
80+
"rsadecrypt": funcs.RsaDecryptFunc,
81+
"setintersection": stdlib.SetIntersectionFunc,
82+
"setproduct": stdlib.SetProductFunc,
83+
"setsubtract": stdlib.SetSubtractFunc,
84+
"setunion": stdlib.SetUnionFunc,
85+
"sha1": funcs.Sha1Func,
86+
"sha256": funcs.Sha256Func,
87+
"sha512": funcs.Sha512Func,
88+
"signum": stdlib.SignumFunc,
89+
"slice": stdlib.SliceFunc,
90+
"sort": stdlib.SortFunc,
91+
"split": stdlib.SplitFunc,
92+
"strrev": stdlib.ReverseFunc,
93+
"substr": stdlib.SubstrFunc,
94+
"timestamp": funcs.TimestampFunc,
95+
"timeadd": stdlib.TimeAddFunc,
96+
"title": stdlib.TitleFunc,
97+
"tostring": funcs.MakeToFunc(cty.String),
98+
"tonumber": funcs.MakeToFunc(cty.Number),
99+
"tobool": funcs.MakeToFunc(cty.Bool),
100+
"toset": funcs.MakeToFunc(cty.Set(cty.DynamicPseudoType)),
101+
"tolist": funcs.MakeToFunc(cty.List(cty.DynamicPseudoType)),
102+
"tomap": funcs.MakeToFunc(cty.Map(cty.DynamicPseudoType)),
103+
"transpose": funcs.TransposeFunc,
104+
"trim": stdlib.TrimFunc,
105+
"trimprefix": stdlib.TrimPrefixFunc,
106+
"trimspace": stdlib.TrimSpaceFunc,
107+
"trimsuffix": stdlib.TrimSuffixFunc,
108+
"try": tryfunc.TryFunc,
109+
"upper": stdlib.UpperFunc,
110+
"urlencode": funcs.URLEncodeFunc,
111+
"uuid": funcs.UUIDFunc,
112+
"uuidv5": funcs.UUIDV5Func,
113+
"values": stdlib.ValuesFunc,
114+
"yamldecode": ctyyaml.YAMLDecodeFunc,
115+
"yamlencode": ctyyaml.YAMLEncodeFunc,
116+
"zipmap": stdlib.ZipmapFunc,
117+
}
118+
119+
// the below functions are not safe for usage in the context of tfparse, as their return
120+
// values may change depending on the underlying filesystem.
121+
stubFileFunctions = map[string]function.Function{
122+
"abspath": makeStubFunction("abspath", cty.String, function.Parameter{Name: "path", Type: cty.String}),
123+
"file": makeStubFunction("file", cty.String, function.Parameter{Name: "path", Type: cty.String}),
124+
"fileexists": makeStubFunction("fileexists", cty.String, function.Parameter{Name: "path", Type: cty.String}),
125+
"fileset": makeStubFunction("fileset", cty.String, function.Parameter{Name: "path", Type: cty.String}, function.Parameter{Name: "pattern", Type: cty.String}),
126+
"filebase64": makeStubFunction("filebase64", cty.String, function.Parameter{Name: "path", Type: cty.String}, function.Parameter{Name: "pattern", Type: cty.String}),
127+
"filebase64sha256": makeStubFunction("filebase64sha256", cty.String, function.Parameter{Name: "path", Type: cty.String}),
128+
"filebase64sha512": makeStubFunction("filebase64sha512", cty.String, function.Parameter{Name: "path", Type: cty.String}),
129+
"filemd5": makeStubFunction("filemd5", cty.String, function.Parameter{Name: "path", Type: cty.String}),
130+
"filesha1": makeStubFunction("filesha1", cty.String, function.Parameter{Name: "path", Type: cty.String}),
131+
"filesha256": makeStubFunction("filesha256", cty.String, function.Parameter{Name: "path", Type: cty.String}),
132+
"filesha512": makeStubFunction("filesha512", cty.String, function.Parameter{Name: "path", Type: cty.String}),
133+
"pathexpand": makeStubFunction("pathexpand", cty.String, function.Parameter{Name: "path", Type: cty.String}),
134+
}
135+
136+
allFunctions = mergeMaps(safeFunctions, stubFileFunctions)
137+
)
138+
139+
// mergeMaps returns a new map which is the result of merging each key and value
140+
// of all maps in ms, in order. Successive maps may override values of previous
141+
// maps.
142+
func mergeMaps[K, V comparable](ms ...map[K]V) map[K]V {
143+
merged := make(map[K]V)
144+
for _, m := range ms {
145+
for k, v := range m {
146+
merged[k] = v
147+
}
148+
}
149+
return merged
150+
}
151+
152+
// makeStubFunction returns a function.Function with the required return type and parameters
153+
// that will always return an unknown type and an error.
154+
func makeStubFunction(name string, returnType cty.Type, params ...function.Parameter) function.Function {
155+
var spec function.Spec
156+
spec.Params = params
157+
spec.Type = function.StaticReturnType(returnType)
158+
spec.Impl = func(_ []cty.Value, _ cty.Type) (cty.Value, error) {
159+
return cty.UnknownVal(returnType), xerrors.Errorf("function %q may not be used here", name)
160+
}
161+
return function.New(&spec)
162+
}

provisioner/terraform/tfparse/tfparse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ func BuildEvalContext(vars map[string]string, params map[string]string) *hcl.Eva
477477
// The default function map for Terraform is not exposed, so we would essentially
478478
// have to re-implement or copy the entire map or a subset thereof.
479479
// ref: https://github.com/hashicorp/terraform/blob/e044e569c5bc81f82e9a4d7891f37c6fbb0a8a10/internal/lang/functions.go#L54
480-
Functions: nil,
480+
Functions: Functions(),
481481
}
482482
if len(varDefaultsM) != 0 {
483483
evalCtx.Variables["var"] = cty.MapVal(varDefaultsM)

provisioner/terraform/tfparse/tfparse_test.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,13 +416,52 @@ func Test_WorkspaceTagDefaultsFromFile(t *testing.T) {
416416
expectError: `There is no variable named "foo_bar"`,
417417
},
418418
{
419-
name: "main.tf with functions in workspace tags",
419+
name: "main.tf with allowed functions in workspace tags",
420420
files: map[string]string{
421421
"main.tf": `
422422
provider "foo" {}
423423
resource "foo_bar" "baz" {
424424
name = "foobar"
425425
}
426+
locals {
427+
some_path = pathexpand("file.txt")
428+
}
429+
variable "region" {
430+
type = string
431+
default = "us"
432+
}
433+
data "coder_parameter" "unrelated" {
434+
name = "unrelated"
435+
type = "list(string)"
436+
default = jsonencode(["a", "b"])
437+
}
438+
data "coder_parameter" "az" {
439+
name = "az"
440+
type = "string"
441+
default = "a"
442+
}
443+
data "coder_workspace_tags" "tags" {
444+
tags = {
445+
"platform" = "kubernetes",
446+
"cluster" = "${"devel"}${"opers"}"
447+
"region" = try(split(".", var.region)[1], "placeholder")
448+
"az" = try(split(".", data.coder_parameter.az.value)[1], "placeholder")
449+
}
450+
}`,
451+
},
452+
expectTags: map[string]string{"platform": "kubernetes", "cluster": "developers", "region": "placeholder", "az": "placeholder"},
453+
},
454+
{
455+
name: "main.tf with disallowed functions in workspace tags",
456+
files: map[string]string{
457+
"main.tf": `
458+
provider "foo" {}
459+
resource "foo_bar" "baz" {
460+
name = "foobar"
461+
}
462+
locals {
463+
some_path = pathexpand("file.txt")
464+
}
426465
variable "region" {
427466
type = string
428467
default = "region.us"
@@ -443,11 +482,12 @@ func Test_WorkspaceTagDefaultsFromFile(t *testing.T) {
443482
"cluster" = "${"devel"}${"opers"}"
444483
"region" = try(split(".", var.region)[1], "placeholder")
445484
"az" = try(split(".", data.coder_parameter.az.value)[1], "placeholder")
485+
"some_path" = pathexpand("~/file.txt")
446486
}
447487
}`,
448488
},
449489
expectTags: nil,
450-
expectError: `Function calls not allowed; Functions may not be called here.`,
490+
expectError: `function "pathexpand" may not be used here`,
451491
},
452492
{
453493
name: "supported types",

0 commit comments

Comments
 (0)
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