Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:51:17 +08:00
commit 04d2231fb6
14 changed files with 4569 additions and 0 deletions

319
skills/scripts/init_module.py Executable file
View 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
View 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
View 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()