Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions docs/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,56 @@ terraform.removed_blocks({"from": "any"}, {})
]
```

## `terraform.ephemeral_resources`

```rego
resources := terraform.ephemeral_resources(resource_type, schema, options)
```

Returns Terraform ephemeral resources.

- `resource_type` (string): resource type to retrieve. "*" is a special character that returns all ephemeral resources.
- `schema` (schema): schema for attributes referenced in rules.
- `options` (object[string: string]): options to change the retrieve/evaluate behavior.

Returns:

- `resources` (array[object<type: string, name: string, config: body, decl_range: range>]): Terraform "ephemeral" blocks.

The `schema` and `options` are equivalent to the arguments of the `terraform.resources` function.

Examples:

```hcl
ephemeral "random_password" "db_password" {
length = 16
override_special = "!#$%&*()-_=+[]{}<>:?"
}
```

```rego
terraform.ephemeral_resources("random_password", {"length": "number"}, {})
```

```json
[
{
"type": "random_password",
"name": "db_password",
"config": {
"owners": {
"value": 16,
"unknown": false,
"sensitive": false,
"ephemeral": false,
"range": {...}
}
},
"decl_range": {...}
}
]
```

## `terraform.module_range`

```rego
Expand Down
9 changes: 9 additions & 0 deletions integration/ephemerals/.tflint.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugin "terraform" {
enabled = false
}

plugin "opa" {
enabled = true

policy_dir = "policies"
}
18 changes: 18 additions & 0 deletions integration/ephemerals/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ephemeral "random_password" "db_password" {
length = 16
override_special = "!#$%&*()-_=+[]{}<>:?"
}

resource "aws_secretsmanager_secret" "db_password" {
name = "db_password"
}

resource "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret.db_password.id
secret_string_wo = ephemeral.random_password.db_password.result
secret_string_wo_version = 1
}

ephemeral "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_secretsmanager_secret_version.db_password.secret_id
}
11 changes: 11 additions & 0 deletions integration/ephemerals/policies/main.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package tflint

import rego.v1

deny_weak_password contains issue if {
passwords := terraform.ephemeral_resources("random_password", {"length": "number"}, {})
length := passwords[_].config.length
length.value < 32

issue := tflint.issue("Password must be at least 32 characters long", length.range)
}
22 changes: 22 additions & 0 deletions integration/ephemerals/policies/main_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package tflint
import future.keywords

mock_ephemeral_resources(type, schema, options) := terraform.mock_ephemeral_resources(type, schema, options, {"main.tf": `
ephemeral "random_password" "db_password" {
length = 16
override_special = "!#$%&*()-_=+[]{}<>:?"
}`})

test_deny_weak_password_passed if {
issues := deny_weak_password with terraform.ephemeral_resources as mock_ephemeral_resources

count(issues) == 1
issue := issues[_]
issue.msg == "Password must be at least 32 characters long"
}

test_deny_weak_password_failed if {
issues := deny_weak_password with terraform.ephemeral_resources as mock_ephemeral_resources

count(issues) == 0
}
25 changes: 25 additions & 0 deletions integration/ephemerals/result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"issues": [
{
"rule": {
"name": "opa_deny_weak_password",
"severity": "error",
"link": "policies/main.rego:5"
},
"message": "Password must be at least 32 characters long",
"range": {
"filename": "main.tf",
"start": {
"line": 2,
"column": 22
},
"end": {
"line": 2,
"column": 24
}
},
"callers": []
}
],
"errors": []
}
25 changes: 25 additions & 0 deletions integration/ephemerals/result_test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"issues": [
{
"rule": {
"name": "opa_test_deny_weak_password_failed",
"severity": "error",
"link": "policies/main_test.rego:18"
},
"message": "test failed",
"range": {
"filename": "",
"start": {
"line": 0,
"column": 0
},
"end": {
"line": 0,
"column": 0
}
},
"callers": []
}
],
"errors": []
}
11 changes: 11 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,17 @@ func TestIntegration(t *testing.T) {
dir: "legacy_rego_syntax",
test: true,
},
{
name: "ephemerals",
command: exec.Command("tflint", "--format", "json", "--force"),
dir: "ephemerals",
},
{
name: "ephemerals (test)",
command: exec.Command("tflint", "--format", "json", "--force"),
dir: "ephemerals",
test: true,
},
}

dir, _ := os.Getwd()
Expand Down
34 changes: 34 additions & 0 deletions opa/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func Functions(runner tflint.Runner) []func(*rego.Rego) {
importsFunc(runner).asOption(),
checksFunc(runner).asOption(),
removedBlocksFunc(runner).asOption(),
ephemeralResourcesFunc(runner).asOption(),
moduleRangeFunc(runner).asOption(),
issueFunc().asOption(),
}
Expand All @@ -50,6 +51,7 @@ func TesterFunctions(runner tflint.Runner) []*tester.Builtin {
importsFunc(runner).asTester(),
checksFunc(runner).asTester(),
removedBlocksFunc(runner).asTester(),
ephemeralResourcesFunc(runner).asTester(),
moduleRangeFunc(runner).asTester(),
issueFunc().asTester(),
}
Expand All @@ -72,6 +74,7 @@ func MockFunctions() []func(*rego.Rego) {
mockFunction2(importsFunc).asOption(),
mockFunction2(checksFunc).asOption(),
mockFunction2(removedBlocksFunc).asOption(),
mockFunction3(ephemeralResourcesFunc).asOption(),
}
}

Expand All @@ -90,6 +93,7 @@ func TesterMockFunctions() []*tester.Builtin {
mockFunction2(importsFunc).asTester(),
mockFunction2(checksFunc).asTester(),
mockFunction2(removedBlocksFunc).asTester(),
mockFunction3(ephemeralResourcesFunc).asTester(),
}
}

Expand Down Expand Up @@ -562,6 +566,36 @@ func removedBlocksFunc(runner tflint.Runner) *function2 {
}
}

// terraform.ephemeral_resources: resources := terraform.ephemeral_resources(resource_type, schema, options)
//
// Returns Terraform ephemeral resources.
//
// resource_type (string) resource type to retrieve. "*" is a special character that returns all resources.
// schema (schema) schema for attributes referenced in rules.
// options (options) options to change the retrieve/evaluate behavior.
//
// Returns:
//
// resources (array[typed_block]) Terraform "ephemeral" blocks
func ephemeralResourcesFunc(runner tflint.Runner) *function3 {
return &function3{
function: function{
Decl: &rego.Function{
Name: "terraform.ephemeral_resources",
Decl: types.NewFunction(
types.Args(types.S, schemaTy, optionsTy),
types.NewArray(nil, typedBlockTy),
),
Memoize: true,
Nondeterministic: true,
},
},
Func: func(_ rego.BuiltinContext, resourceType *ast.Term, schema *ast.Term, options *ast.Term) (*ast.Term, error) {
return typedBlockFunc(resourceType, schema, options, "ephemeral", runner)
},
}
}

// terraform.module_range: range := terraform.module_range()
//
// Returns a range for the current Terraform module.
Expand Down
111 changes: 111 additions & 0 deletions opa/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,117 @@ variable "foo" {}`,
}
}

func TestEphemeralResourcesFunc(t *testing.T) {
tests := []struct {
name string
config string
resourceType string
schema map[string]any
options map[string]string
want []map[string]any
}{
{
name: "ephemeral resource",
config: `
ephemeral "aws_secretsmanager_secret_version" "db_password" {
secret_id = "secret_id"
}`,
resourceType: "aws_secretsmanager_secret_version",
schema: map[string]any{"secret_id": "string"},
want: []map[string]any{
{
"type": "aws_secretsmanager_secret_version",
"name": "db_password",
"config": map[string]any{
"secret_id": map[string]any{
"value": "secret_id",
"unknown": false,
"sensitive": false,
"ephemeral": false,
"range": map[string]any{
"filename": "main.tf",
"start": map[string]int{
"line": 3,
"column": 15,
"byte": 77,
},
"end": map[string]int{
"line": 3,
"column": 26,
"byte": 88,
},
},
},
},
"decl_range": map[string]any{
"filename": "main.tf",
"start": map[string]int{
"line": 2,
"column": 1,
"byte": 1,
},
"end": map[string]int{
"line": 2,
"column": 60,
"byte": 60,
},
},
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resourceType, err := ast.InterfaceToValue(test.resourceType)
if err != nil {
t.Fatal(err)
}
schema, err := ast.InterfaceToValue(test.schema)
if err != nil {
t.Fatal(err)
}
options, err := ast.InterfaceToValue(test.options)
if err != nil {
t.Fatal(err)
}
config, err := ast.InterfaceToValue(map[string]string{"main.tf": test.config})
if err != nil {
t.Fatal(err)
}
want, err := ast.InterfaceToValue(test.want)
if err != nil {
t.Fatal(err)
}

runner, diags := NewTestRunner(map[string]string{"main.tf": test.config})
if diags.HasErrors() {
t.Fatal(diags)
}

ctx := rego.BuiltinContext{}
got, err := ephemeralResourcesFunc(runner).Func(ctx, ast.NewTerm(resourceType), ast.NewTerm(schema), ast.NewTerm(options))
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(want.String(), got.Value.String()); diff != "" {
t.Error(diff)
}

ctx = rego.BuiltinContext{}
got, err = mockFunction3(ephemeralResourcesFunc).Func(ctx, ast.NewTerm(resourceType), ast.NewTerm(schema), ast.NewTerm(options), ast.NewTerm(config))
if err != nil {
t.Fatal(err)
}

if diff := cmp.Diff(want.String(), got.Value.String()); diff != "" {
t.Error(diff)
}
})
}
}

func TestModuleRangeFunc(t *testing.T) {
tests := []struct {
name string
Expand Down