Skip to content

Commit 4dae81f

Browse files
committed
Add terraform.ephemeral_resources function
See https://developer.hashicorp.com/terraform/language/resources/ephemeral This change adds a new `terraform.ephemeral_resources` function. This correspond to the ephemeral resources introduced in Terraform v1.10. The function interface and return values are pretty much the same as other functions, so there's no new functionality.
1 parent d920149 commit 4dae81f

File tree

10 files changed

+316
-0
lines changed

10 files changed

+316
-0
lines changed

docs/functions.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,56 @@ terraform.removed_blocks({"from": "any"}, {})
732732
]
733733
```
734734

735+
## `terraform.ephemeral_resources`
736+
737+
```rego
738+
resources := terraform.ephemeral_resources(resource_type, schema, options)
739+
```
740+
741+
Returns Terraform ephemeral resources.
742+
743+
- `resource_type` (string): resource type to retrieve. "*" is a special character that returns all ephemeral resources.
744+
- `schema` (schema): schema for attributes referenced in rules.
745+
- `options` (object[string: string]): options to change the retrieve/evaluate behavior.
746+
747+
Returns:
748+
749+
- `resources` (array[object<type: string, name: string, config: body, decl_range: range>]): Terraform "ephemeral" blocks.
750+
751+
The `schema` and `options` are equivalent to the arguments of the `terraform.resources` function.
752+
753+
Examples:
754+
755+
```hcl
756+
ephemeral "random_password" "db_password" {
757+
length = 16
758+
override_special = "!#$%&*()-_=+[]{}<>:?"
759+
}
760+
```
761+
762+
```rego
763+
terraform.ephemeral_resources("random_password", {"length": "number"}, {})
764+
```
765+
766+
```json
767+
[
768+
{
769+
"type": "random_password",
770+
"name": "db_password",
771+
"config": {
772+
"owners": {
773+
"value": 16,
774+
"unknown": false,
775+
"sensitive": false,
776+
"ephemeral": false,
777+
"range": {...}
778+
}
779+
},
780+
"decl_range": {...}
781+
}
782+
]
783+
```
784+
735785
## `terraform.module_range`
736786

737787
```rego

integration/ephemerals/.tflint.hcl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
plugin "terraform" {
2+
enabled = false
3+
}
4+
5+
plugin "opa" {
6+
enabled = true
7+
8+
policy_dir = "policies"
9+
}

integration/ephemerals/main.tf

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
ephemeral "random_password" "db_password" {
2+
length = 16
3+
override_special = "!#$%&*()-_=+[]{}<>:?"
4+
}
5+
6+
resource "aws_secretsmanager_secret" "db_password" {
7+
name = "db_password"
8+
}
9+
10+
resource "aws_secretsmanager_secret_version" "db_password" {
11+
secret_id = aws_secretsmanager_secret.db_password.id
12+
secret_string_wo = ephemeral.random_password.db_password.result
13+
secret_string_wo_version = 1
14+
}
15+
16+
ephemeral "aws_secretsmanager_secret_version" "db_password" {
17+
secret_id = aws_secretsmanager_secret_version.db_password.secret_id
18+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package tflint
2+
3+
import rego.v1
4+
5+
deny_weak_password contains issue if {
6+
passwords := terraform.ephemeral_resources("random_password", {"length": "number"}, {})
7+
length := passwords[_].config.length
8+
length.value < 32
9+
10+
issue := tflint.issue("Password must be at least 32 characters long", length.range)
11+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package tflint
2+
import future.keywords
3+
4+
mock_ephemeral_resources(type, schema, options) := terraform.mock_ephemeral_resources(type, schema, options, {"main.tf": `
5+
ephemeral "random_password" "db_password" {
6+
length = 16
7+
override_special = "!#$%&*()-_=+[]{}<>:?"
8+
}`})
9+
10+
test_deny_weak_password_passed if {
11+
issues := deny_weak_password with terraform.ephemeral_resources as mock_ephemeral_resources
12+
13+
count(issues) == 1
14+
issue := issues[_]
15+
issue.msg == "Password must be at least 32 characters long"
16+
}
17+
18+
test_deny_weak_password_failed if {
19+
issues := deny_weak_password with terraform.ephemeral_resources as mock_ephemeral_resources
20+
21+
count(issues) == 0
22+
}

integration/ephemerals/result.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"issues": [
3+
{
4+
"rule": {
5+
"name": "opa_deny_weak_password",
6+
"severity": "error",
7+
"link": "policies/main.rego:5"
8+
},
9+
"message": "Password must be at least 32 characters long",
10+
"range": {
11+
"filename": "main.tf",
12+
"start": {
13+
"line": 2,
14+
"column": 22
15+
},
16+
"end": {
17+
"line": 2,
18+
"column": 24
19+
}
20+
},
21+
"callers": []
22+
}
23+
],
24+
"errors": []
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"issues": [
3+
{
4+
"rule": {
5+
"name": "opa_test_deny_weak_password_failed",
6+
"severity": "error",
7+
"link": "policies/main_test.rego:18"
8+
},
9+
"message": "test failed",
10+
"range": {
11+
"filename": "",
12+
"start": {
13+
"line": 0,
14+
"column": 0
15+
},
16+
"end": {
17+
"line": 0,
18+
"column": 0
19+
}
20+
},
21+
"callers": []
22+
}
23+
],
24+
"errors": []
25+
}

integration/integration_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,17 @@ func TestIntegration(t *testing.T) {
247247
dir: "legacy_rego_syntax",
248248
test: true,
249249
},
250+
{
251+
name: "ephemerals",
252+
command: exec.Command("tflint", "--format", "json", "--force"),
253+
dir: "ephemerals",
254+
},
255+
{
256+
name: "ephemerals (test)",
257+
command: exec.Command("tflint", "--format", "json", "--force"),
258+
dir: "ephemerals",
259+
test: true,
260+
},
250261
}
251262

252263
dir, _ := os.Getwd()

opa/functions.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func Functions(runner tflint.Runner) []func(*rego.Rego) {
3030
importsFunc(runner).asOption(),
3131
checksFunc(runner).asOption(),
3232
removedBlocksFunc(runner).asOption(),
33+
ephemeralResourcesFunc(runner).asOption(),
3334
moduleRangeFunc(runner).asOption(),
3435
issueFunc().asOption(),
3536
}
@@ -50,6 +51,7 @@ func TesterFunctions(runner tflint.Runner) []*tester.Builtin {
5051
importsFunc(runner).asTester(),
5152
checksFunc(runner).asTester(),
5253
removedBlocksFunc(runner).asTester(),
54+
ephemeralResourcesFunc(runner).asTester(),
5355
moduleRangeFunc(runner).asTester(),
5456
issueFunc().asTester(),
5557
}
@@ -72,6 +74,7 @@ func MockFunctions() []func(*rego.Rego) {
7274
mockFunction2(importsFunc).asOption(),
7375
mockFunction2(checksFunc).asOption(),
7476
mockFunction2(removedBlocksFunc).asOption(),
77+
mockFunction3(ephemeralResourcesFunc).asOption(),
7578
}
7679
}
7780

@@ -90,6 +93,7 @@ func TesterMockFunctions() []*tester.Builtin {
9093
mockFunction2(importsFunc).asTester(),
9194
mockFunction2(checksFunc).asTester(),
9295
mockFunction2(removedBlocksFunc).asTester(),
96+
mockFunction3(ephemeralResourcesFunc).asTester(),
9397
}
9498
}
9599

@@ -562,6 +566,36 @@ func removedBlocksFunc(runner tflint.Runner) *function2 {
562566
}
563567
}
564568

569+
// terraform.ephemeral_resources: resources := terraform.ephemeral_resources(resource_type, schema, options)
570+
//
571+
// Returns Terraform ephemeral resources.
572+
//
573+
// resource_type (string) resource type to retrieve. "*" is a special character that returns all resources.
574+
// schema (schema) schema for attributes referenced in rules.
575+
// options (options) options to change the retrieve/evaluate behavior.
576+
//
577+
// Returns:
578+
//
579+
// resources (array[typed_block]) Terraform "ephemeral" blocks
580+
func ephemeralResourcesFunc(runner tflint.Runner) *function3 {
581+
return &function3{
582+
function: function{
583+
Decl: &rego.Function{
584+
Name: "terraform.ephemeral_resources",
585+
Decl: types.NewFunction(
586+
types.Args(types.S, schemaTy, optionsTy),
587+
types.NewArray(nil, typedBlockTy),
588+
),
589+
Memoize: true,
590+
Nondeterministic: true,
591+
},
592+
},
593+
Func: func(_ rego.BuiltinContext, resourceType *ast.Term, schema *ast.Term, options *ast.Term) (*ast.Term, error) {
594+
return typedBlockFunc(resourceType, schema, options, "ephemeral", runner)
595+
},
596+
}
597+
}
598+
565599
// terraform.module_range: range := terraform.module_range()
566600
//
567601
// Returns a range for the current Terraform module.

opa/functions_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,6 +1543,117 @@ variable "foo" {}`,
15431543
}
15441544
}
15451545

1546+
func TestEphemeralResourcesFunc(t *testing.T) {
1547+
tests := []struct {
1548+
name string
1549+
config string
1550+
resourceType string
1551+
schema map[string]any
1552+
options map[string]string
1553+
want []map[string]any
1554+
}{
1555+
{
1556+
name: "ephemeral resource",
1557+
config: `
1558+
ephemeral "aws_secretsmanager_secret_version" "db_password" {
1559+
secret_id = "secret_id"
1560+
}`,
1561+
resourceType: "aws_secretsmanager_secret_version",
1562+
schema: map[string]any{"secret_id": "string"},
1563+
want: []map[string]any{
1564+
{
1565+
"type": "aws_secretsmanager_secret_version",
1566+
"name": "db_password",
1567+
"config": map[string]any{
1568+
"secret_id": map[string]any{
1569+
"value": "secret_id",
1570+
"unknown": false,
1571+
"sensitive": false,
1572+
"ephemeral": false,
1573+
"range": map[string]any{
1574+
"filename": "main.tf",
1575+
"start": map[string]int{
1576+
"line": 3,
1577+
"column": 15,
1578+
"byte": 77,
1579+
},
1580+
"end": map[string]int{
1581+
"line": 3,
1582+
"column": 26,
1583+
"byte": 88,
1584+
},
1585+
},
1586+
},
1587+
},
1588+
"decl_range": map[string]any{
1589+
"filename": "main.tf",
1590+
"start": map[string]int{
1591+
"line": 2,
1592+
"column": 1,
1593+
"byte": 1,
1594+
},
1595+
"end": map[string]int{
1596+
"line": 2,
1597+
"column": 60,
1598+
"byte": 60,
1599+
},
1600+
},
1601+
},
1602+
},
1603+
},
1604+
}
1605+
1606+
for _, test := range tests {
1607+
t.Run(test.name, func(t *testing.T) {
1608+
resourceType, err := ast.InterfaceToValue(test.resourceType)
1609+
if err != nil {
1610+
t.Fatal(err)
1611+
}
1612+
schema, err := ast.InterfaceToValue(test.schema)
1613+
if err != nil {
1614+
t.Fatal(err)
1615+
}
1616+
options, err := ast.InterfaceToValue(test.options)
1617+
if err != nil {
1618+
t.Fatal(err)
1619+
}
1620+
config, err := ast.InterfaceToValue(map[string]string{"main.tf": test.config})
1621+
if err != nil {
1622+
t.Fatal(err)
1623+
}
1624+
want, err := ast.InterfaceToValue(test.want)
1625+
if err != nil {
1626+
t.Fatal(err)
1627+
}
1628+
1629+
runner, diags := NewTestRunner(map[string]string{"main.tf": test.config})
1630+
if diags.HasErrors() {
1631+
t.Fatal(diags)
1632+
}
1633+
1634+
ctx := rego.BuiltinContext{}
1635+
got, err := ephemeralResourcesFunc(runner).Func(ctx, ast.NewTerm(resourceType), ast.NewTerm(schema), ast.NewTerm(options))
1636+
if err != nil {
1637+
t.Fatal(err)
1638+
}
1639+
1640+
if diff := cmp.Diff(want.String(), got.Value.String()); diff != "" {
1641+
t.Error(diff)
1642+
}
1643+
1644+
ctx = rego.BuiltinContext{}
1645+
got, err = mockFunction3(ephemeralResourcesFunc).Func(ctx, ast.NewTerm(resourceType), ast.NewTerm(schema), ast.NewTerm(options), ast.NewTerm(config))
1646+
if err != nil {
1647+
t.Fatal(err)
1648+
}
1649+
1650+
if diff := cmp.Diff(want.String(), got.Value.String()); diff != "" {
1651+
t.Error(diff)
1652+
}
1653+
})
1654+
}
1655+
}
1656+
15461657
func TestModuleRangeFunc(t *testing.T) {
15471658
tests := []struct {
15481659
name string

0 commit comments

Comments
 (0)