Initial commit
This commit is contained in:
319
skills/scripts/init_module.py
Executable file
319
skills/scripts/init_module.py
Executable file
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Terraform Module Scaffolding Tool
|
||||
Creates a new Terraform module with proper structure and template files
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
# Template content for module files
|
||||
TEMPLATES = {
|
||||
"main.tf": '''# {module_name} module - Main configuration
|
||||
|
||||
resource "example_resource" "main" {{
|
||||
# TODO: Replace with actual resources
|
||||
name = var.name
|
||||
|
||||
tags = merge(
|
||||
var.tags,
|
||||
{{
|
||||
Module = "{module_name}"
|
||||
}}
|
||||
)
|
||||
}}
|
||||
''',
|
||||
|
||||
"variables.tf": '''# Input variables for {module_name} module
|
||||
|
||||
variable "name" {{
|
||||
type = string
|
||||
description = "Name for the {module_name} resources"
|
||||
}}
|
||||
|
||||
variable "tags" {{
|
||||
type = map(string)
|
||||
description = "Common tags to apply to all resources"
|
||||
default = {{}}
|
||||
}}
|
||||
|
||||
variable "environment" {{
|
||||
type = string
|
||||
description = "Environment name (dev, staging, prod)"
|
||||
|
||||
validation {{
|
||||
condition = contains(["dev", "staging", "prod"], var.environment)
|
||||
error_message = "Environment must be dev, staging, or prod."
|
||||
}}
|
||||
}}
|
||||
''',
|
||||
|
||||
"outputs.tf": '''# Output values for {module_name} module
|
||||
|
||||
output "id" {{
|
||||
description = "ID of the created {module_name} resource"
|
||||
value = example_resource.main.id
|
||||
}}
|
||||
|
||||
output "arn" {{
|
||||
description = "ARN of the created {module_name} resource"
|
||||
value = example_resource.main.arn
|
||||
}}
|
||||
''',
|
||||
|
||||
"versions.tf": '''# Provider and Terraform version constraints
|
||||
|
||||
terraform {{
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {{
|
||||
aws = {{
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
''',
|
||||
|
||||
"README.md": '''# {module_title} Module
|
||||
|
||||
Terraform module for managing {module_name}.
|
||||
|
||||
## Usage
|
||||
|
||||
```hcl
|
||||
module "{module_name}" {{
|
||||
source = "./modules/{module_name}"
|
||||
|
||||
name = "example"
|
||||
environment = "dev"
|
||||
|
||||
tags = {{
|
||||
Project = "MyProject"
|
||||
Owner = "TeamName"
|
||||
}}
|
||||
}}
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
| Name | Version |
|
||||
|------|---------|
|
||||
| terraform | >= 1.5.0 |
|
||||
| aws | ~> 5.0 |
|
||||
|
||||
## Inputs
|
||||
|
||||
| Name | Description | Type | Default | Required |
|
||||
|------|-------------|------|---------|:--------:|
|
||||
| name | Name for the {module_name} resources | `string` | n/a | yes |
|
||||
| environment | Environment name (dev, staging, prod) | `string` | n/a | yes |
|
||||
| tags | Common tags to apply to all resources | `map(string)` | `{{}}` | no |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Description |
|
||||
|------|-------------|
|
||||
| id | ID of the created {module_name} resource |
|
||||
| arn | ARN of the created {module_name} resource |
|
||||
|
||||
## Examples
|
||||
|
||||
See the `examples/` directory for complete usage examples.
|
||||
|
||||
---
|
||||
|
||||
<!-- BEGIN_TF_DOCS -->
|
||||
<!-- Run: terraform-docs markdown . > README.md -->
|
||||
<!-- END_TF_DOCS -->
|
||||
''',
|
||||
|
||||
"examples/complete/main.tf": '''# Complete example for {module_name} module
|
||||
|
||||
module "{module_name}" {{
|
||||
source = "../.."
|
||||
|
||||
name = "example-{module_name}"
|
||||
environment = "dev"
|
||||
|
||||
tags = {{
|
||||
Project = "Example"
|
||||
Environment = "dev"
|
||||
ManagedBy = "Terraform"
|
||||
}}
|
||||
}}
|
||||
|
||||
output "{module_name}_id" {{
|
||||
description = "ID of the {module_name}"
|
||||
value = module.{module_name}.id
|
||||
}}
|
||||
''',
|
||||
|
||||
"examples/complete/versions.tf": '''terraform {{
|
||||
required_version = ">= 1.5.0"
|
||||
|
||||
required_providers {{
|
||||
aws = {{
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
provider "aws" {{
|
||||
region = "us-east-1"
|
||||
}}
|
||||
''',
|
||||
|
||||
"examples/complete/README.md": '''# Complete Example
|
||||
|
||||
This example demonstrates the complete usage of the {module_name} module with all available options.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
terraform destroy
|
||||
```
|
||||
'''
|
||||
}
|
||||
|
||||
|
||||
def create_module_structure(module_name: str, base_path: str = ".") -> Dict:
|
||||
"""
|
||||
Create the module directory structure with template files
|
||||
|
||||
Args:
|
||||
module_name: Name of the module
|
||||
base_path: Base directory where module should be created
|
||||
|
||||
Returns:
|
||||
Dict with status and details
|
||||
"""
|
||||
result = {
|
||||
"success": False,
|
||||
"module_name": module_name,
|
||||
"path": None,
|
||||
"files_created": [],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
try:
|
||||
# Create base module directory
|
||||
module_path = Path(base_path) / module_name
|
||||
if module_path.exists():
|
||||
result["errors"].append(f"Module directory already exists: {module_path}")
|
||||
return result
|
||||
|
||||
module_path.mkdir(parents=True, exist_ok=True)
|
||||
result["path"] = str(module_path.absolute())
|
||||
|
||||
# Create examples directory
|
||||
examples_path = module_path / "examples" / "complete"
|
||||
examples_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Format module name for titles (replace hyphens with spaces, capitalize)
|
||||
module_title = module_name.replace("-", " ").replace("_", " ").title()
|
||||
|
||||
# Create files from templates
|
||||
for filename, template in TEMPLATES.items():
|
||||
file_path = module_path / filename
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
content = template.format(
|
||||
module_name=module_name,
|
||||
module_title=module_title
|
||||
)
|
||||
|
||||
file_path.write_text(content)
|
||||
result["files_created"].append(str(file_path.relative_to(base_path)))
|
||||
|
||||
result["success"] = True
|
||||
|
||||
except Exception as e:
|
||||
result["errors"].append(f"Error creating module: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Initialize a new Terraform module with standard structure",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Create module in current directory
|
||||
%(prog)s my-vpc
|
||||
|
||||
# Create module in specific path
|
||||
%(prog)s my-vpc --path ./modules
|
||||
|
||||
# Output as JSON
|
||||
%(prog)s my-vpc --json
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"module_name",
|
||||
help="Name of the module to create (use lowercase with hyphens)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--path",
|
||||
default=".",
|
||||
help="Base directory where module should be created (default: current directory)"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output result as JSON"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate module name
|
||||
if not args.module_name.replace("-", "").replace("_", "").isalnum():
|
||||
print("Error: Module name should only contain lowercase letters, numbers, hyphens, and underscores",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Create module
|
||||
result = create_module_structure(args.module_name, args.path)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
if result["success"]:
|
||||
print(f"✅ Successfully created module: {args.module_name}")
|
||||
print(f"📁 Location: {result['path']}")
|
||||
print(f"\n📝 Created {len(result['files_created'])} files:")
|
||||
for file in result["files_created"]:
|
||||
print(f" - {file}")
|
||||
print(f"\n🚀 Next steps:")
|
||||
print(f" 1. cd {result['path']}")
|
||||
print(f" 2. Update main.tf with your resources")
|
||||
print(f" 3. Run: terraform init")
|
||||
print(f" 4. Run: terraform validate")
|
||||
print(f" 5. Generate docs: terraform-docs markdown . > README.md")
|
||||
else:
|
||||
print(f"❌ Failed to create module: {args.module_name}", file=sys.stderr)
|
||||
for error in result["errors"]:
|
||||
print(f" Error: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0 if result["success"] else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
232
skills/scripts/inspect_state.py
Executable file
232
skills/scripts/inspect_state.py
Executable file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Terraform State Inspector & Drift Detector
|
||||
Analyzes Terraform state and detects configuration drift
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Dict, List, Any
|
||||
from datetime import datetime
|
||||
|
||||
def run_command(cmd: List[str], cwd: str = ".") -> Dict[str, Any]:
|
||||
"""Run a command and return the result"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return {"success": True, "stdout": result.stdout, "stderr": result.stderr}
|
||||
except subprocess.CalledProcessError as e:
|
||||
return {"success": False, "stdout": e.stdout, "stderr": e.stderr, "returncode": e.returncode}
|
||||
|
||||
def check_state_health(working_dir: str) -> Dict[str, Any]:
|
||||
"""Check the health of the Terraform state"""
|
||||
print("🔍 Checking Terraform state health...\n")
|
||||
|
||||
# Check if state exists
|
||||
result = run_command(["terraform", "state", "list"], working_dir)
|
||||
if not result["success"]:
|
||||
return {
|
||||
"healthy": False,
|
||||
"error": "Unable to read state. Is Terraform initialized?",
|
||||
"details": result["stderr"]
|
||||
}
|
||||
|
||||
resources = result["stdout"].strip().split("\n") if result["stdout"].strip() else []
|
||||
|
||||
return {
|
||||
"healthy": True,
|
||||
"resource_count": len(resources),
|
||||
"resources": resources
|
||||
}
|
||||
|
||||
def detect_drift(working_dir: str) -> Dict[str, Any]:
|
||||
"""Run terraform plan to detect drift"""
|
||||
print("🔄 Detecting configuration drift...\n")
|
||||
|
||||
result = run_command(["terraform", "plan", "-detailed-exitcode", "-no-color"], working_dir)
|
||||
|
||||
# Exit codes: 0 = no changes, 1 = error, 2 = changes detected
|
||||
if result["returncode"] == 0:
|
||||
return {
|
||||
"drift_detected": False,
|
||||
"message": "No drift detected - infrastructure matches configuration"
|
||||
}
|
||||
elif result["returncode"] == 2:
|
||||
return {
|
||||
"drift_detected": True,
|
||||
"message": "Drift detected - infrastructure differs from configuration",
|
||||
"plan_output": result["stdout"]
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": True,
|
||||
"message": "Error running terraform plan",
|
||||
"details": result["stderr"]
|
||||
}
|
||||
|
||||
def analyze_state_resources(working_dir: str) -> Dict[str, Any]:
|
||||
"""Analyze resources in the state file"""
|
||||
print("📊 Analyzing state resources...\n")
|
||||
|
||||
result = run_command(["terraform", "show", "-json"], working_dir)
|
||||
if not result["success"]:
|
||||
return {"error": "Unable to read state JSON", "details": result["stderr"]}
|
||||
|
||||
try:
|
||||
state_data = json.loads(result["stdout"])
|
||||
except json.JSONDecodeError:
|
||||
return {"error": "Unable to parse state JSON"}
|
||||
|
||||
resources = state_data.get("values", {}).get("root_module", {}).get("resources", [])
|
||||
|
||||
# Categorize resources by type
|
||||
resource_types = {}
|
||||
for resource in resources:
|
||||
res_type = resource.get("type", "unknown")
|
||||
resource_types[res_type] = resource_types.get(res_type, 0) + 1
|
||||
|
||||
# Identify potentially problematic resources
|
||||
issues = []
|
||||
for resource in resources:
|
||||
# Check for resources with tainted status
|
||||
if resource.get("tainted", False):
|
||||
issues.append(f"⚠️ Resource {resource['address']} is tainted")
|
||||
|
||||
return {
|
||||
"total_resources": len(resources),
|
||||
"resource_types": resource_types,
|
||||
"issues": issues
|
||||
}
|
||||
|
||||
def check_provider_versions(working_dir: str) -> Dict[str, Any]:
|
||||
"""Check provider versions and constraints"""
|
||||
print("📦 Checking provider versions...\n")
|
||||
|
||||
result = run_command(["terraform", "version", "-json"], working_dir)
|
||||
if not result["success"]:
|
||||
return {"error": "Unable to get version info"}
|
||||
|
||||
try:
|
||||
version_data = json.loads(result["stdout"])
|
||||
return {
|
||||
"terraform_version": version_data.get("terraform_version"),
|
||||
"provider_versions": version_data.get("provider_selections", {})
|
||||
}
|
||||
except json.JSONDecodeError:
|
||||
return {"error": "Unable to parse version JSON"}
|
||||
|
||||
def check_backend_config(working_dir: str) -> Dict[str, Any]:
|
||||
"""Check backend configuration"""
|
||||
print("🗄️ Checking backend configuration...\n")
|
||||
|
||||
result = run_command(["terraform", "show", "-json"], working_dir)
|
||||
if not result["success"]:
|
||||
return {"error": "Unable to read backend config"}
|
||||
|
||||
try:
|
||||
state_data = json.loads(result["stdout"])
|
||||
backend = state_data.get("values", {}).get("backend", {})
|
||||
|
||||
return {
|
||||
"backend_type": backend.get("type", "local"),
|
||||
"config": backend.get("config", {})
|
||||
}
|
||||
except json.JSONDecodeError:
|
||||
return {"error": "Unable to parse backend config"}
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: inspect_state.py <terraform-directory> [--check-drift]")
|
||||
sys.exit(1)
|
||||
|
||||
working_dir = sys.argv[1]
|
||||
check_drift_flag = "--check-drift" in sys.argv
|
||||
|
||||
print("=" * 70)
|
||||
print("🏗️ TERRAFORM STATE INSPECTOR")
|
||||
print("=" * 70)
|
||||
print(f"Working Directory: {working_dir}")
|
||||
print(f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
|
||||
# Check state health
|
||||
state_health = check_state_health(working_dir)
|
||||
if not state_health.get("healthy"):
|
||||
print(f"❌ State Health: UNHEALTHY")
|
||||
print(f" Error: {state_health.get('error')}")
|
||||
print(f" Details: {state_health.get('details')}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✅ State Health: HEALTHY")
|
||||
print(f" Total Resources: {state_health['resource_count']}\n")
|
||||
|
||||
# Check provider versions
|
||||
versions = check_provider_versions(working_dir)
|
||||
if "error" not in versions:
|
||||
print(f"📦 Terraform Version: {versions['terraform_version']}")
|
||||
print(f" Providers:")
|
||||
for provider, version in versions.get('provider_versions', {}).items():
|
||||
print(f" • {provider}: {version}")
|
||||
print()
|
||||
|
||||
# Check backend
|
||||
backend = check_backend_config(working_dir)
|
||||
if "error" not in backend:
|
||||
print(f"🗄️ Backend Type: {backend['backend_type']}")
|
||||
if backend['backend_type'] != 'local':
|
||||
print(f" Configuration: {backend.get('config', {})}")
|
||||
print()
|
||||
|
||||
# Analyze resources
|
||||
analysis = analyze_state_resources(working_dir)
|
||||
if "error" not in analysis:
|
||||
print(f"📊 Resource Analysis:")
|
||||
print(f" Total Resources: {analysis['total_resources']}")
|
||||
print(f" Resource Types:")
|
||||
for res_type, count in sorted(analysis['resource_types'].items()):
|
||||
print(f" • {res_type}: {count}")
|
||||
|
||||
if analysis['issues']:
|
||||
print(f"\n ⚠️ Issues Found:")
|
||||
for issue in analysis['issues']:
|
||||
print(f" {issue}")
|
||||
else:
|
||||
print(f"\n ✅ No issues detected")
|
||||
print()
|
||||
|
||||
# Check for drift if requested
|
||||
if check_drift_flag:
|
||||
drift = detect_drift(working_dir)
|
||||
if drift.get("error"):
|
||||
print(f"❌ Drift Detection Failed:")
|
||||
print(f" {drift['message']}")
|
||||
print(f" {drift.get('details', '')}")
|
||||
elif drift.get("drift_detected"):
|
||||
print(f"⚠️ DRIFT DETECTED")
|
||||
print(f" {drift['message']}")
|
||||
print(f"\n Run 'terraform plan' for detailed differences")
|
||||
else:
|
||||
print(f"✅ No Drift Detected")
|
||||
print(f" {drift['message']}")
|
||||
print()
|
||||
|
||||
print("=" * 70)
|
||||
print("✅ State inspection complete!")
|
||||
|
||||
# Recommendations
|
||||
print("\n💡 Recommendations:")
|
||||
if state_health['resource_count'] == 0:
|
||||
print(" • No resources in state - consider running 'terraform apply'")
|
||||
if backend.get('backend_type') == 'local':
|
||||
print(" • Using local backend - consider remote backend for team collaboration")
|
||||
if not check_drift_flag:
|
||||
print(" • Run with --check-drift flag to detect configuration drift")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
227
skills/scripts/validate_module.py
Executable file
227
skills/scripts/validate_module.py
Executable file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Terraform Module Validator
|
||||
Validates Terraform modules against best practices
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any
|
||||
|
||||
class ModuleValidator:
|
||||
def __init__(self, module_path: str):
|
||||
self.module_path = Path(module_path)
|
||||
self.issues = []
|
||||
self.warnings = []
|
||||
self.suggestions = []
|
||||
|
||||
def validate(self) -> Dict[str, Any]:
|
||||
"""Run all validation checks"""
|
||||
print(f"🔍 Validating module: {self.module_path}\n")
|
||||
|
||||
self.check_required_files()
|
||||
self.check_variables_file()
|
||||
self.check_outputs_file()
|
||||
self.check_readme()
|
||||
self.check_versions_file()
|
||||
self.check_examples()
|
||||
self.check_naming_conventions()
|
||||
|
||||
return {
|
||||
"valid": len(self.issues) == 0,
|
||||
"issues": self.issues,
|
||||
"warnings": self.warnings,
|
||||
"suggestions": self.suggestions
|
||||
}
|
||||
|
||||
def check_required_files(self):
|
||||
"""Check for required module files"""
|
||||
required_files = ['main.tf', 'variables.tf', 'outputs.tf']
|
||||
|
||||
for file in required_files:
|
||||
if not (self.module_path / file).exists():
|
||||
self.issues.append(f"Missing required file: {file}")
|
||||
|
||||
def check_variables_file(self):
|
||||
"""Check variables.tf for best practices"""
|
||||
vars_file = self.module_path / 'variables.tf'
|
||||
if not vars_file.exists():
|
||||
return
|
||||
|
||||
content = vars_file.read_text()
|
||||
|
||||
# Check for variable descriptions
|
||||
variable_blocks = re.findall(r'variable\s+"([^"]+)"\s*{([^}]+)}', content, re.DOTALL)
|
||||
|
||||
for var_name, var_content in variable_blocks:
|
||||
if 'description' not in var_content:
|
||||
self.warnings.append(f"Variable '{var_name}' missing description")
|
||||
|
||||
if 'type' not in var_content:
|
||||
self.warnings.append(f"Variable '{var_name}' missing type constraint")
|
||||
|
||||
# Check for sensitive variables without sensitive flag
|
||||
if any(keyword in var_name.lower() for keyword in ['password', 'secret', 'key', 'token']):
|
||||
if 'sensitive' not in var_content or 'sensitive = true' not in var_content:
|
||||
self.warnings.append(f"Variable '{var_name}' appears sensitive but not marked as sensitive")
|
||||
|
||||
def check_outputs_file(self):
|
||||
"""Check outputs.tf for best practices"""
|
||||
outputs_file = self.module_path / 'outputs.tf'
|
||||
if not outputs_file.exists():
|
||||
return
|
||||
|
||||
content = outputs_file.read_text()
|
||||
|
||||
# Check for output descriptions
|
||||
output_blocks = re.findall(r'output\s+"([^"]+)"\s*{([^}]+)}', content, re.DOTALL)
|
||||
|
||||
if len(output_blocks) == 0:
|
||||
self.suggestions.append("Consider adding outputs to expose useful resource attributes")
|
||||
|
||||
for output_name, output_content in output_blocks:
|
||||
if 'description' not in output_content:
|
||||
self.warnings.append(f"Output '{output_name}' missing description")
|
||||
|
||||
# Check for sensitive outputs
|
||||
if any(keyword in output_name.lower() for keyword in ['password', 'secret', 'key', 'token']):
|
||||
if 'sensitive' not in output_content or 'sensitive = true' not in output_content:
|
||||
self.warnings.append(f"Output '{output_name}' appears sensitive but not marked as sensitive")
|
||||
|
||||
def check_readme(self):
|
||||
"""Check for README documentation"""
|
||||
readme_files = ['README.md', 'readme.md', 'README.txt']
|
||||
has_readme = any((self.module_path / f).exists() for f in readme_files)
|
||||
|
||||
if not has_readme:
|
||||
self.issues.append("Missing README.md - modules should be documented")
|
||||
return
|
||||
|
||||
# Find which readme exists
|
||||
readme_path = None
|
||||
for f in readme_files:
|
||||
if (self.module_path / f).exists():
|
||||
readme_path = self.module_path / f
|
||||
break
|
||||
|
||||
if readme_path:
|
||||
content = readme_path.read_text()
|
||||
|
||||
# Check for key sections
|
||||
required_sections = ['Usage', 'Inputs', 'Outputs']
|
||||
for section in required_sections:
|
||||
if section.lower() not in content.lower():
|
||||
self.suggestions.append(f"README missing '{section}' section")
|
||||
|
||||
# Check for examples
|
||||
if 'example' not in content.lower():
|
||||
self.suggestions.append("README should include usage examples")
|
||||
|
||||
def check_versions_file(self):
|
||||
"""Check for versions.tf or terraform block"""
|
||||
versions_file = self.module_path / 'versions.tf'
|
||||
|
||||
# Check versions.tf
|
||||
if versions_file.exists():
|
||||
content = versions_file.read_text()
|
||||
if 'required_version' not in content:
|
||||
self.warnings.append("versions.tf should specify required_version")
|
||||
if 'required_providers' not in content:
|
||||
self.warnings.append("versions.tf should specify required_providers with versions")
|
||||
else:
|
||||
# Check main.tf for terraform block
|
||||
main_file = self.module_path / 'main.tf'
|
||||
if main_file.exists():
|
||||
content = main_file.read_text()
|
||||
if 'terraform' not in content or 'required_version' not in content:
|
||||
self.warnings.append("Module should specify Terraform version requirements")
|
||||
else:
|
||||
self.warnings.append("Consider creating versions.tf to specify version constraints")
|
||||
|
||||
def check_examples(self):
|
||||
"""Check for example usage"""
|
||||
examples_dir = self.module_path / 'examples'
|
||||
|
||||
if not examples_dir.exists():
|
||||
self.suggestions.append("Consider adding 'examples/' directory with usage examples")
|
||||
elif examples_dir.is_dir():
|
||||
example_subdirs = [d for d in examples_dir.iterdir() if d.is_dir()]
|
||||
if len(example_subdirs) == 0:
|
||||
self.suggestions.append("examples/ directory is empty - add example configurations")
|
||||
|
||||
def check_naming_conventions(self):
|
||||
"""Check file and resource naming conventions"""
|
||||
tf_files = list(self.module_path.glob('*.tf'))
|
||||
|
||||
for tf_file in tf_files:
|
||||
# Check for snake_case file names
|
||||
if not re.match(r'^[a-z0-9_]+\.tf$', tf_file.name):
|
||||
self.warnings.append(f"File '{tf_file.name}' should use snake_case naming")
|
||||
|
||||
# Check file content for naming
|
||||
content = tf_file.read_text()
|
||||
|
||||
# Check resource names use snake_case
|
||||
resources = re.findall(r'resource\s+"[^"]+"\s+"([^"]+)"', content)
|
||||
for resource_name in resources:
|
||||
if not re.match(r'^[a-z0-9_]+$', resource_name):
|
||||
self.warnings.append(f"Resource name '{resource_name}' should use snake_case")
|
||||
|
||||
# Check for hard-coded values that should be variables
|
||||
if re.search(r'= "us-east-1"', content):
|
||||
self.suggestions.append("Consider making region configurable via variable")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: validate_module.py <module-directory>")
|
||||
sys.exit(1)
|
||||
|
||||
module_path = sys.argv[1]
|
||||
|
||||
if not os.path.isdir(module_path):
|
||||
print(f"❌ Error: {module_path} is not a directory")
|
||||
sys.exit(1)
|
||||
|
||||
print("=" * 70)
|
||||
print("🏗️ TERRAFORM MODULE VALIDATOR")
|
||||
print("=" * 70)
|
||||
print()
|
||||
|
||||
validator = ModuleValidator(module_path)
|
||||
result = validator.validate()
|
||||
|
||||
# Print results
|
||||
if result['issues']:
|
||||
print("❌ ISSUES (Must Fix):")
|
||||
for issue in result['issues']:
|
||||
print(f" • {issue}")
|
||||
print()
|
||||
|
||||
if result['warnings']:
|
||||
print("⚠️ WARNINGS (Should Fix):")
|
||||
for warning in result['warnings']:
|
||||
print(f" • {warning}")
|
||||
print()
|
||||
|
||||
if result['suggestions']:
|
||||
print("💡 SUGGESTIONS (Consider):")
|
||||
for suggestion in result['suggestions']:
|
||||
print(f" • {suggestion}")
|
||||
print()
|
||||
|
||||
# Summary
|
||||
print("=" * 70)
|
||||
if result['valid']:
|
||||
print("✅ Module validation PASSED!")
|
||||
if not result['warnings'] and not result['suggestions']:
|
||||
print(" No issues, warnings, or suggestions - excellent work!")
|
||||
else:
|
||||
print("❌ Module validation FAILED!")
|
||||
print(f" {len(result['issues'])} issues must be fixed before using this module")
|
||||
print("=" * 70)
|
||||
|
||||
sys.exit(0 if result['valid'] else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user