Skip to content

Commit

Permalink
Add terraform.removed_blocks function (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
wata727 authored Feb 23, 2024
1 parent 3d29dc1 commit df7a75e
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 0 deletions.
48 changes: 48 additions & 0 deletions docs/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,54 @@ terraform.checks({"assert": {"condition": "bool"}}, {})
]
```

## `terraform.removed_blocks`

```rego
blocks := terraform.removed_blocks(schema, options)
```

Returns Terraform removed blocks.

- `schema` (schema): schema for attributes referenced in rules.
- `options` (object[string: string]): options to change the retrieve/evaluate behavior.

Returns:

- `blocks` (array[object<config: body, decl_range: range>]): Terraform "removed" blocks.

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

Examples:

```hcl
removed {
from = aws_instance.example
lifecycle {
destroy = false
}
}
```

```rego
terraform.removed_blocks({"from": "any"}, {})
```

```json
[
{
"config": {
"from": {
"unknown": true,
"sensitive": false,
"range": {...}
}
},
"decl_range": {...}
}
]
```

## `terraform.module_range`

```rego
Expand Down
11 changes: 11 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,17 @@ func TestIntegration(t *testing.T) {
dir: "checks",
test: true,
},
{
name: "removed blocks",
command: exec.Command("tflint", "--format", "json", "--force"),
dir: "removed",
},
{
name: "removed blocks (test)",
command: exec.Command("tflint", "--format", "json", "--force"),
dir: "removed",
test: true,
},
}

dir, _ := os.Getwd()
Expand Down
9 changes: 9 additions & 0 deletions integration/removed/.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"
}
7 changes: 7 additions & 0 deletions integration/removed/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
removed {
from = aws_instance.example

lifecycle {
destroy = false
}
}
8 changes: 8 additions & 0 deletions integration/removed/policies/main.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package tflint

deny_removed_blocks[issue] {
moved := terraform.removed_blocks({}, {})
count(moved) > 0

issue := tflint.issue("removed blocks are not allowed", moved[0].decl_range)
}
25 changes: 25 additions & 0 deletions integration/removed/policies/main_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package tflint
import future.keywords

mock_removed_blocks(schema, options) := terraform.mock_removed_blocks(schema, options, {"main.tf": `
removed {
from = aws_instance.example
lifecycle {
destroy = false
}
}`})

test_deny_removed_blocks_passed if {
issues := deny_removed_blocks with terraform.removed_blocks as mock_removed_blocks

count(issues) == 1
issue := issues[_]
issue.msg == "removed blocks are not allowed"
}

test_deny_removed_blocks_failed if {
issues := deny_removed_blocks with terraform.removed_blocks as mock_removed_blocks

count(issues) == 0
}
25 changes: 25 additions & 0 deletions integration/removed/result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"issues": [
{
"rule": {
"name": "opa_deny_removed_blocks",
"severity": "error",
"link": "policies/main.rego:3"
},
"message": "removed blocks are not allowed",
"range": {
"filename": "main.tf",
"start": {
"line": 1,
"column": 1
},
"end": {
"line": 1,
"column": 8
}
},
"callers": []
}
],
"errors": []
}
25 changes: 25 additions & 0 deletions integration/removed/result_test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"issues": [
{
"rule": {
"name": "opa_test_deny_removed_blocks_failed",
"severity": "error",
"link": "policies/main_test.rego:21"
},
"message": "test failed",
"range": {
"filename": "",
"start": {
"line": 0,
"column": 0
},
"end": {
"line": 0,
"column": 0
}
},
"callers": []
}
],
"errors": []
}
33 changes: 33 additions & 0 deletions opa/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func Functions(runner tflint.Runner) []func(*rego.Rego) {
movedBlocksFunc(runner).asOption(),
importsFunc(runner).asOption(),
checksFunc(runner).asOption(),
removedBlocksFunc(runner).asOption(),
moduleRangeFunc(runner).asOption(),
issueFunc().asOption(),
}
Expand All @@ -48,6 +49,7 @@ func TesterFunctions(runner tflint.Runner) []*tester.Builtin {
movedBlocksFunc(runner).asTester(),
importsFunc(runner).asTester(),
checksFunc(runner).asTester(),
removedBlocksFunc(runner).asTester(),
moduleRangeFunc(runner).asTester(),
issueFunc().asTester(),
}
Expand All @@ -69,6 +71,7 @@ func MockFunctions() []func(*rego.Rego) {
mockFunction2(movedBlocksFunc).asOption(),
mockFunction2(importsFunc).asOption(),
mockFunction2(checksFunc).asOption(),
mockFunction2(removedBlocksFunc).asOption(),
}
}

Expand All @@ -86,6 +89,7 @@ func TesterMockFunctions() []*tester.Builtin {
mockFunction2(movedBlocksFunc).asTester(),
mockFunction2(importsFunc).asTester(),
mockFunction2(checksFunc).asTester(),
mockFunction2(removedBlocksFunc).asTester(),
}
}

Expand Down Expand Up @@ -529,6 +533,35 @@ func checksFunc(runner tflint.Runner) *function2 {
}
}

// terraform.removed_blocks: blocks := terraform.removed_blocks(schema, options)
//
// Returns Terraform removed blocks.
//
// schema (schema) schema for attributes referenced in rules.
// options (options) options to change the retrieve/evaluate behavior.
//
// Returns:
//
// blocks (array[block]) Terraform "removed" blocks
func removedBlocksFunc(runner tflint.Runner) *function2 {
return &function2{
function: function{
Decl: &rego.Function{
Name: "terraform.removed_blocks",
Decl: types.NewFunction(
types.Args(schemaTy, optionsTy),
types.NewArray(nil, blockTy),
),
Memoize: true,
Nondeterministic: true,
},
},
Func: func(_ rego.BuiltinContext, schema *ast.Term, options *ast.Term) (*ast.Term, error) {
return blockFunc(schema, options, "removed", runner)
},
}
}

// terraform.module_range: range := terraform.module_range()
//
// Returns a range for the current Terraform module.
Expand Down
103 changes: 103 additions & 0 deletions opa/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,109 @@ check "health_check" {
}
}

func TestRemovedBlocksFunc(t *testing.T) {
tests := []struct {
name string
config string
schema map[string]any
options map[string]string
want []map[string]any
}{
{
name: "removed block",
config: `
removed {
from = var.foo
}
variable "foo" {}`,
schema: map[string]any{"from": "any"},
want: []map[string]any{
{
"config": map[string]any{
"from": map[string]any{
"unknown": true,
"sensitive": false,
"range": map[string]any{
"filename": "main.tf",
"start": map[string]int{
"line": 3,
"column": 9,
"byte": 19,
},
"end": map[string]int{
"line": 3,
"column": 16,
"byte": 26,
},
},
},
},
"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": 8,
"byte": 8,
},
},
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
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 := removedBlocksFunc(runner).Func(ctx, 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 = mockFunction2(removedBlocksFunc).Func(ctx, 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

0 comments on commit df7a75e

Please sign in to comment.