Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add terraform.removed_blocks function #87

Merged
merged 1 commit into from
Feb 23, 2024
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
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