Skip to content

Commit 5d52786

Browse files
authored
Merge pull request #366 from dflook/fallback-parse
Improve parsing of tf files
2 parents 5335987 + 1f6b98a commit 5d52786

File tree

7 files changed

+191
-3
lines changed

7 files changed

+191
-3
lines changed

.github/workflows/test-version.yaml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ jobs:
888888
with:
889889
persist-credentials: false
890890

891-
- name: Test terraform-version
891+
- name: Test tofu-version
892892
uses: ./tofu-version
893893
id: tofu-version
894894
env:
@@ -907,3 +907,29 @@ jobs:
907907
echo "::error:: Terraform version not selected"
908908
exit 1
909909
fi
910+
911+
hard_parse:
912+
runs-on: ubuntu-24.04
913+
name: Get version constraint from hard to parse file
914+
steps:
915+
- name: Checkout
916+
uses: actions/checkout@v4
917+
with:
918+
persist-credentials: false
919+
920+
- name: Test terraform-version
921+
uses: ./terraform-version
922+
id: terraform-version
923+
with:
924+
path: tests/workflows/test-version/hard-parse
925+
926+
- name: Check the version
927+
env:
928+
DETECTED_TERRAFORM_VERSION: ${{ steps.terraform-version.outputs.terraform }}
929+
run: |
930+
echo "The terraform version was $DETECTED_TERRAFORM_VERSION"
931+
932+
if [[ "$DETECTED_TERRAFORM_VERSION" != "1.10.4" ]]; then
933+
echo "::error:: Terraform constraint not parsed correctly"
934+
exit 1
935+
fi
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Fallback parsing for hcl files
3+
4+
We only need limited information from Terraform Modules:
5+
- The required_version constraint
6+
- The backend type
7+
- A list of sensitive variable names
8+
- The backend configuration for remote backends and cloud blocks
9+
10+
The easiest way to get this information is to parse the HCL files directly.
11+
This doesn't always work if our parser fails, or the files are malformed.
12+
13+
This fallback 'parser' does the stupidest thing that might work to get the information we need.
14+
15+
TODO: The backend configuration is not yet implemented.
16+
"""
17+
18+
import re
19+
from pathlib import Path
20+
from typing import Optional
21+
22+
from github_actions.debug import debug
23+
24+
25+
def get_required_version(body: str) -> Optional[str]:
26+
"""Get the required_version constraint string from a tf file"""
27+
28+
if version := re.search(r'required_version\s*=\s*"(.+)"', body):
29+
return version.group(1)
30+
31+
def get_backend_type(body: str) -> Optional[str]:
32+
"""Get the backend type from a tf file"""
33+
34+
if backend := re.search(r'backend\s*"(.+)"', body):
35+
return backend.group(1)
36+
37+
if backend := re.search(r'backend\s+(.*)\s*{', body):
38+
return backend.group(1).strip()
39+
40+
if re.search(r'cloud\s+\{', body):
41+
return 'cloud'
42+
43+
def get_sensitive_variables(body: str) -> list[str]:
44+
"""Get the sensitive variable names from a tf file"""
45+
46+
variables = []
47+
48+
found = False
49+
50+
for line in reversed(body.splitlines()):
51+
if re.search(r'sensitive\s*=\s*true', line, re.IGNORECASE) or re.search(r'sensitive\s*=\s*"true"', line, re.IGNORECASE):
52+
found = True
53+
continue
54+
55+
if found and (variable := re.search(r'variable\s*"(.+)"', line)):
56+
variables.append(variable.group(1))
57+
found = False
58+
59+
if found and (variable := re.search(r'variable\s+(.+)\{', line)):
60+
variables.append(variable.group(1))
61+
found = False
62+
63+
return variables
64+
65+
def parse(path: Path) -> dict:
66+
debug(f'Attempting to parse {path} with fallback parser')
67+
body = path.read_text()
68+
69+
module = {}
70+
71+
if constraint := get_required_version(body):
72+
module['terraform'] = [{
73+
'required_version': constraint
74+
}]
75+
76+
if backend_type := get_backend_type(body):
77+
if 'terraform' not in module:
78+
module['terraform'] = []
79+
80+
if backend_type == 'cloud':
81+
module['terraform'].append({'cloud': [{}]})
82+
else:
83+
module['terraform'].append({'backend': [{backend_type:{}}]})
84+
85+
if sensitive_variables := get_sensitive_variables(body):
86+
module['variable'] = []
87+
for variable in sensitive_variables:
88+
module['variable'].append({
89+
variable: {
90+
'sensitive': True
91+
}
92+
})
93+
94+
return module
95+
96+
if __name__ == '__main__':
97+
from pprint import pprint
98+
pprint(parse(Path('tests/workflows/test-validate/hard-parse/main.tf')))

image/src/terraform/hcl.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@
88
from pathlib import Path
99

1010
from github_actions.debug import debug
11+
import terraform.fallback_parser
1112

1213

1314
def try_load(path: Path) -> dict:
1415
try:
1516
with open(path) as f:
1617
return hcl2.load(f)
1718
except Exception as e:
19+
debug(f'Failed to load {path}')
1820
debug(str(e))
19-
return {}
21+
return terraform.fallback_parser.parse(Path(path))
2022

2123

2224
def is_loadable(path: Path) -> bool:

image/tools/convert_validate_report.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ def convert_to_github(report: Dict, base_path: str) -> Iterable[str]:
2525
params['endLine'] = diag['range']['end']['line']
2626
params['endColumn'] = diag['range']['end']['column']
2727

28+
if params.get('line') != params.get('endLine'):
29+
# GitHub can't cope with 'col' and 'endColumn' if 'line' and 'endLine' are different values.
30+
if 'col' in params:
31+
del params['col']
32+
if 'endColumn' in params:
33+
del params['endColumn']
34+
2835
summary = diag['summary'].split('\n')[0]
2936
params = ','.join(f'{k}={v}' for k, v in params.items())
3037

tests/test_validate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def test_invalid_paths():
122122

123123
expected_output = [
124124
'::error file=tests/validate/invalid/main.tf,line=2,col=1,endLine=2,endColumn=33::Duplicate resource "null_resource" configuration',
125-
'::error file=tests/validate/module/invalid.tf,line=2,col=1,endLine=5,endColumn=66::Duplicate resource "null_resource" configuration'
125+
'::error file=tests/validate/module/invalid.tf,line=2,endLine=5::Duplicate resource "null_resource" configuration'
126126
]
127127

128128
output = list(convert_to_github(input, 'tests/validate/invalid'))
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
terraform {
2+
3+
}
4+
5+
terraform {
6+
required_version = "1.10.4"
7+
}
8+
9+
locals {
10+
cloud_run_services = [
11+
{
12+
service_name = "service-1",
13+
output_topics = [
14+
{
15+
name = "topic-1",
16+
version = "v1"
17+
}
18+
]
19+
}
20+
]
21+
}
22+
23+
24+
module "pubsub" {
25+
for_each = {
26+
for service in local.cloud_run_services : service.service_name => service
27+
}
28+
source = "./module"
29+
topics = [
30+
for entity in each.value.output_topics : {
31+
topic_name = entity.version != "" ? format("Topic-%s-%s", entity.name, entity.version) : format("Topic-%s", entity.name)
32+
subscription_name = entity.version != "" ? format("Sub-%s-%s", entity.name, entity.version) : format("Sub-%s", entity.name)
33+
}
34+
]
35+
}
36+
37+
38+
variable "not" {}
39+
40+
variable "should-be-sensitive" {
41+
sensitive=true
42+
}
43+
44+
variable "not-again" {
45+
sensitive = false
46+
}
47+
48+
variable also_sensitive {
49+
sensitive = "true"
50+
}
51+
52+
terraform {
53+
backend "s3" {}
54+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
variable "topics" {}

0 commit comments

Comments
 (0)