Skip to content
Closed
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ ftf add-variable [OPTIONS] /path/to/module

#### Validate Directory

Perform validation on Terraform directories including formatting checks and security scans using Checkov.
Perform validation on Terraform directories including formatting checks, provider block checks, and security scans using Checkov.

```bash
ftf validate-directory /path/to/module [OPTIONS]
Expand All @@ -140,6 +140,7 @@ ftf validate-directory /path/to/module [OPTIONS]

**Notes**:
- Runs `terraform fmt` for formatting verification.
- Checks for provider blocks in all .tf files (provider blocks are not allowed in modules; use exposed providers in facets.yaml instead).
- Runs `terraform init` to ensure initialization completeness.
- Uses Checkov to scan Terraform files for security misconfigurations.
- Designed for fast feedback on module quality and security.
Expand Down
4 changes: 4 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ def test_generate_module(temp_module_dir): # temp_module_dir fixture from conft
assert os.path.exists(os.path.join(temp_module_dir, 'expected_file'))
```

### Provider Block Validation

All validation commands now check for provider blocks in any .tf file (including nested folders). If a provider block is found, validation fails with a clear error. Modules must not contain provider blocks; use exposed providers in facets.yaml instead. This is covered by tests in `tests/test_utils.py`.

## Fixtures

Common test fixtures are provided in `tests/conftest.py`:
Expand Down
2 changes: 2 additions & 0 deletions ftf_cli/commands/validate_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
validate_facets_yaml,
validate_boolean,
validate_facets_tf_vars,
validate_no_provider_blocks,
)
from checkov.runner_filter import RunnerFilter
from checkov.terraform.runner import Runner
Expand Down Expand Up @@ -55,6 +56,7 @@ def validate_directory(path, check_only, skip_terraform_validation):
click.echo(line)

validate_facets_tf_vars(path)
validate_no_provider_blocks(path)
click.echo(
"✅ Terraform files are correctly formatted."
if check_only
Expand Down
24 changes: 23 additions & 1 deletion ftf_cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,4 +932,26 @@ def transform_properties_to_terraform(properties_obj, level=1):
# Fallback for unknown types or empty objects
if not properties_obj:
return "object({})"
return "any"
return "any"

def validate_no_provider_blocks(path):
"""
Recursively scan all *.tf files under path, parse with python-hcl2, and raise UsageError if any provider block is found.
Fail fast on parse errors.
"""
tf_files = glob.glob(os.path.join(path, "**", "*.tf"), recursive=True)
provider_violations = []
for tf_file in tf_files:
try:
with open(tf_file, "r") as fp:
terraform_tree = hcl2.load(fp)
if "provider" in terraform_tree and terraform_tree["provider"]:
provider_violations.append(tf_file)
except Exception as e:
raise click.UsageError(f"❌ Failed to parse {tf_file}: {e}")
if provider_violations:
file_list = "\n".join(os.path.relpath(f, path) for f in provider_violations)
raise click.UsageError(
f"❌ Provider blocks are not allowed in module files. Found provider block(s) in:\n{file_list}\nUse exposed providers in facets.yaml instead."
)
click.echo("✅ No provider blocks found in Terraform files.")
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"questionary",
"facets-hcl",
"ruamel.yaml",
"python-hcl2",
],
packages=find_packages(
include=["ftf_cli", "ftf_cli.commands", "ftf_cli.commands.templates"]
Expand All @@ -36,6 +37,7 @@
"pytest>=8.3.5",
"pytest-mock",
"pyhcl>=0.4.5",
"python-hcl2",
],
},
entry_points="""
Expand Down
25 changes: 25 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ def test_lookup_tree_mixed_nested_lists():

import pytest
from ftf_cli.utils import properties_to_lookup_tree, transform_properties_to_terraform
from ftf_cli.utils import validate_no_provider_blocks
import os


class TestPropertiesToLookupTree:
Expand Down Expand Up @@ -545,3 +547,26 @@ def test_indentation_levels(self):
assert any(' level1 = object({' in line for line in lines)
assert any(' level2 = object({' in line for line in lines)
assert any(' field = string' in line for line in lines)


def test_validate_no_provider_blocks_detects_provider(tmp_path):
tf_file = tmp_path / "main.tf"
tf_file.write_text('provider "aws" { region = "us-west-2" }')
with pytest.raises(Exception) as excinfo:
validate_no_provider_blocks(str(tmp_path))
assert "Provider blocks are not allowed" in str(excinfo.value)


def test_validate_no_provider_blocks_allows_no_provider(tmp_path):
tf_file = tmp_path / "main.tf"
tf_file.write_text('resource "aws_instance" "example" { ami = "ami-123" }')
# Should not raise
validate_no_provider_blocks(str(tmp_path))


def test_validate_no_provider_blocks_parse_error(tmp_path):
tf_file = tmp_path / "main.tf"
tf_file.write_text('this is not valid hcl')
with pytest.raises(Exception) as excinfo:
validate_no_provider_blocks(str(tmp_path))
assert "Failed to parse" in str(excinfo.value)