Skip to content

Commit bc9316d

Browse files
authored
Add "expr" type as a schema type (#164)
1 parent 8e5e2dd commit bc9316d

File tree

18 files changed

+1489
-26
lines changed

18 files changed

+1489
-26
lines changed

docs/functions.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,170 @@ Returns:
795795

796796
- `range` (range): a range for [DIR]/main.tf:1:1
797797

798+
## `hcl.expr_list`
799+
800+
```rego
801+
exprs := hcl.expr_list(expr)
802+
```
803+
804+
Extract a list of expressions from the given static list expression.
805+
This is equivalent to [`hcl.ExprList`](https://github.com/hashicorp/hcl/blob/v2.24.0/expr_list.go#L17).
806+
807+
- `expr` (raw_expr): static list expression which is retrieved as an [`expr` type](./schema.md#expr-type).
808+
809+
Returns:
810+
811+
- `exprs` (array[raw_expr]): expressions as elements of the list.
812+
813+
Types:
814+
815+
|Name|Type|
816+
|---|---|
817+
|`raw_expr`|`object<value: string, range: range>`|
818+
819+
Examples:
820+
821+
```hcl
822+
resource "aws_instance" "main" {
823+
lifecycle {
824+
ignore_changes = [key_name, tags]
825+
}
826+
}
827+
```
828+
829+
```rego
830+
instances := terraform.resources("*", {"lifecycle": {"ignore_changes": "expr"}}, {"expand_mode": "none"})
831+
hcl.expr_list(instances[i].config.lifecycle[j].config.ignore_changes)
832+
```
833+
834+
```json
835+
[
836+
{
837+
"value": "key_name",
838+
"range": {...}
839+
},
840+
{
841+
"value": "tags",
842+
"range": {...}
843+
}
844+
]
845+
```
846+
847+
## `hcl.expr_map`
848+
849+
```rego
850+
pairs := hcl.expr_map(expr)
851+
```
852+
853+
Extract a list of key value pairs from the given static map expression.
854+
This is equivalent to [hcl.ExprMap](https://github.com/hashicorp/hcl/blob/v2.24.0/expr_map.go#L17).
855+
856+
- `expr` (raw_expr): static map expression which is retrieved as an [`expr` type](./schema.md#expr-type).
857+
858+
Returns:
859+
860+
- `pairs` (array[key_value]): key value pairs of the map as expressions.
861+
862+
Types:
863+
864+
|Name|Type|
865+
|---|---|
866+
|`key_value`|`object<key: raw_expr, value: raw_expr>`|
867+
868+
Examples:
869+
870+
```hcl
871+
module "tunnel" {
872+
source = "./tunnel"
873+
providers = {
874+
aws.src = aws.usw1
875+
aws.dst = aws.usw2
876+
}
877+
}
878+
```
879+
880+
```rego
881+
calls := terraform.module_calls({"providers": "expr"})
882+
hcl.expr_map(calls[i].config.providers)
883+
```
884+
885+
```json
886+
[
887+
{
888+
"key": {
889+
"value": "aws.src",
890+
"range": {...}
891+
},
892+
"value": {
893+
"value": "aws.usw1",
894+
"range": {...}
895+
}
896+
},
897+
{
898+
"key": {
899+
"value": "aws.dst",
900+
"range": {...}
901+
},
902+
"value": {
903+
"value": "aws.usw2",
904+
"range": {...}
905+
}
906+
}
907+
]
908+
```
909+
910+
## `hcl.expr_call`
911+
912+
```rego
913+
call := hcl.expr_call(expr)
914+
```
915+
916+
Extract the function name and arguments from the given function call expression.
917+
This is equivalent to [hcl.ExprCall](https://github.com/hashicorp/hcl/blob/v2.24.0/expr_call.go#L17).
918+
919+
- `expr` (raw_expr): function call expression which is retrieved as an [`expr` type](./schema.md#expr-type).
920+
921+
Returns:
922+
923+
- `call` (call): function call object.
924+
925+
Types:
926+
927+
|Name|Type|
928+
|---|---|
929+
|`call`|`object<name: string, name_range: range, arguments: array[raw_expr], args_range: range>`|
930+
931+
Examples:
932+
933+
```hcl
934+
resource "aws_instance" "main" {
935+
ami = provider::custom::get_ami_id("web", "v0.9")
936+
}
937+
```
938+
939+
```rego
940+
instances := terraform.resources("aws_instance", {"ami": "expr"})
941+
hcl.expr_call(instances[i].config.ami)
942+
```
943+
944+
```json
945+
{
946+
"name": "provider::custom::get_ami_id",
947+
"name_range": {...},
948+
"arguments": [
949+
{
950+
"value": "\"web\"",
951+
"range": {...}
952+
},
953+
{
954+
"value": "\"v0.9\"",
955+
"range": {...}
956+
}
957+
],
958+
"args_range": {...}
959+
}
960+
```
961+
798962
## `tflint.issue`
799963

800964
```rego

docs/schema.md

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,33 @@ For example, a schema required to decode the top-level `instance_type` is:
1010
{"instance_type": "string"}
1111
```
1212

13-
The object's key is the attribute name and the value represents the type. The type syntax is the same as [Terraform's type constraints](https://developer.hashicorp.com/terraform/language/expressions/type-constraints).
13+
The object's key is the attribute name and the value represents the type. The type syntax is essentially the same as [Terraform's type constraints](https://developer.hashicorp.com/terraform/language/expressions/type-constraints).
14+
15+
## `any` Type
1416

1517
TFLint implicitly converts values according to their type, which is useful when working with numbers.
1618

19+
```rego
20+
{"size": "number"}
21+
```
22+
1723
```hcl
18-
resource "aws_instance" "number" {
19-
ebs_block_device {
20-
volume_size = 50
21-
}
24+
resource "aws_ebs_volume" "number" {
25+
size = 50
2226
}
2327
24-
resource "aws_instance" "string" {
25-
ebs_block_device {
26-
volume_size = "50" # => convert to number in JSON
27-
}
28+
resource "aws_ebs_volume" "string" {
29+
size = "50" # => convert to number in JSON
2830
}
2931
```
3032

31-
If you don't know the attribute type, you can use `any`. In this case no conversion is done, but the raw value from the config file is available.
33+
If you don't know the attribute type, you can use `any`. In this case no conversion is done, but the raw value from the config file is available. In the above example, the JSON will contain 50 and "50".
34+
35+
```rego
36+
{"size": "any"}
37+
```
38+
39+
## Nested Blocks
3240

3341
A schema for decoding nested blocks is:
3442

@@ -53,3 +61,46 @@ resource "aws_instance" "main" {
5361
```
5462

5563
The `__labels` is a special key that sets labels. The value defines the label name in an array, not the type. Label names are basically meaningless.
64+
65+
## `expr` Type
66+
67+
The `expr` type can be used as a special type. Attributes specified as `expr` type are not evaluated immediately, but the structure of the expression is included in the value.
68+
69+
```hcl
70+
variable "instance_type" {
71+
default = "t2.micro"
72+
}
73+
74+
resource "aws_instance" "main" {
75+
instance_type = var.instance_type
76+
}
77+
```
78+
79+
```rego
80+
{"instance_type": "string"}
81+
```
82+
83+
```json
84+
{
85+
"value": "t2.micro",
86+
"unknown": false,
87+
"sensitive": false,
88+
"ephemeral": false,
89+
"range": {...}
90+
}
91+
```
92+
93+
```rego
94+
{"instance_type": "expr"}
95+
```
96+
97+
```json
98+
{
99+
"value": "var.instance_type",
100+
"range": {...}
101+
}
102+
```
103+
104+
This is useful for writing policies over expression structures. For example, the `expr` type is the only way to handle meta-arguments such as `ignore_changes` that cannot be evaluated in the normal way.
105+
106+
The value obtained with the `expr` type is called `raw_expr` type and can be passed to HCL static analysis functions such as [`hcl.expr_list`](./functions.md#hclexpr_list), [`hcl.expr_map`](./functions.md#hclexpr_map), and [`hcl.expr_call`](./functions.md#hclexpr_call).
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package tflint
2+
3+
import rego.v1
4+
5+
aws_instances := terraform.resources("aws_instance", {"lifecycle": {"ignore_changes": "expr"}}, {"expand_mode": "none"})
6+
7+
# ignore_changes = [tags]
8+
has_tags_ignore(instance) if {
9+
ignore_changes := instance.config.lifecycle[_].config.ignore_changes
10+
11+
startswith(ignore_changes.value, "[")
12+
some i
13+
hcl.expr_list(ignore_changes)[i].value == "tags"
14+
}
15+
16+
# ignore_changes = ["tags"]
17+
has_tags_ignore(instance) if {
18+
ignore_changes := instance.config.lifecycle[_].config.ignore_changes
19+
20+
startswith(ignore_changes.value, "[")
21+
some i
22+
hcl.expr_list(ignore_changes)[i].value == `"tags"`
23+
}
24+
25+
# ignore_changes = all
26+
has_tags_ignore(instance) if {
27+
ignore_changes := instance.config.lifecycle[_].config.ignore_changes
28+
ignore_changes.value == "all"
29+
}
30+
31+
# ignore_changes = "all"
32+
has_tags_ignore(instance) if {
33+
ignore_changes := instance.config.lifecycle[_].config.ignore_changes
34+
ignore_changes.value == `"all"`
35+
}
36+
37+
deny_instance_without_tags_ignore contains issue if {
38+
not has_tags_ignore(aws_instances[i])
39+
40+
issue := tflint.issue(`instance must have "ignore_changes = [tags]"`, aws_instances[i].decl_range)
41+
}

0 commit comments

Comments
 (0)