diff --git a/README.md b/README.md index 82fadcf..eee95f6 100644 --- a/README.md +++ b/README.md @@ -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] @@ -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. diff --git a/TESTING.md b/TESTING.md index 9257f66..a11c31a 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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`: diff --git a/ftf_cli/commands/validate_directory.py b/ftf_cli/commands/validate_directory.py index 244ea59..05055e7 100644 --- a/ftf_cli/commands/validate_directory.py +++ b/ftf_cli/commands/validate_directory.py @@ -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 @@ -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 diff --git a/ftf_cli/utils.py b/ftf_cli/utils.py index 41970d0..012844d 100644 --- a/ftf_cli/utils.py +++ b/ftf_cli/utils.py @@ -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" \ No newline at end of file + 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.") \ No newline at end of file diff --git a/setup.py b/setup.py index a9803bb..5435157 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ "questionary", "facets-hcl", "ruamel.yaml", + "python-hcl2", ], packages=find_packages( include=["ftf_cli", "ftf_cli.commands", "ftf_cli.commands.templates"] @@ -36,6 +37,7 @@ "pytest>=8.3.5", "pytest-mock", "pyhcl>=0.4.5", + "python-hcl2", ], }, entry_points=""" diff --git a/tests/test_utils.py b/tests/test_utils.py index 9d061db..6373e0c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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: @@ -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)