From 987a85f35358aecdc04bcac98be21241bac4e62b Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 1 Apr 2025 18:50:47 -0400 Subject: [PATCH] upgrade tool wip --- Makefile | 2 +- .../main.tf | 13 - tests/fixtures/0.12/backup-test/main.tf | 21 - .../terraform-upgrade-dryrun-report.md | 12 - .../upgrade-report-20250327-195934.md | 29 -- .../upgrade-logs/upgrade-progress.json | 7 - .../simple/upgrade-logs/upgrade-progress.json | 2 +- tf_bulk_upgrade.py | 429 ++++++++++++++++++ tf_upgrade/env_validator.py | 107 +++-- tf_upgrade/utils/terraform.py | 48 +- tf_upgrade/version_upgraders/v0_13.py | 95 +++- 11 files changed, 610 insertions(+), 155 deletions(-) delete mode 100644 tests/fixtures/0.12/backup-test/.terraform-upgrade-backup-20250327195934/main.tf delete mode 100644 tests/fixtures/0.12/backup-test/main.tf delete mode 100644 tests/fixtures/0.12/backup-test/terraform-upgrade-dryrun-report.md delete mode 100644 tests/fixtures/0.12/backup-test/terraform-upgrade-reports/upgrade-report-20250327-195934.md delete mode 100644 tests/fixtures/0.12/backup-test/upgrade-logs/upgrade-progress.json create mode 100755 tf_bulk_upgrade.py diff --git a/Makefile b/Makefile index 75b8fb36..33c231a4 100644 --- a/Makefile +++ b/Makefile @@ -152,7 +152,7 @@ verify-backup: ## Verify backup functionality @echo "Verifying backup functionality..." | tee -a $(VERIFICATION_LOG) @cp -r $(TEST_FIXTURES_DIR)/0.12/simple $(TEST_FIXTURES_DIR)/0.12/backup-test @python -m tf_upgrade.cli upgrade $(TEST_FIXTURES_DIR)/0.12/backup-test --target-version 0.13 | tee -a $(VERIFICATION_LOG) - @if [ -d "$(TEST_FIXTURES_DIR)/0.12/backup-test/.terraform-upgrade-backup-*" ]; then \ + @if ls $(TEST_FIXTURES_DIR)/0.12/backup-test/.terraform-upgrade-backup-* > /dev/null 2>&1; then \ echo "✅ Backup created successfully"; \ else \ echo "❌ Backup creation failed"; \ diff --git a/tests/fixtures/0.12/backup-test/.terraform-upgrade-backup-20250327195934/main.tf b/tests/fixtures/0.12/backup-test/.terraform-upgrade-backup-20250327195934/main.tf deleted file mode 100644 index 4d50d6a8..00000000 --- a/tests/fixtures/0.12/backup-test/.terraform-upgrade-backup-20250327195934/main.tf +++ /dev/null @@ -1,13 +0,0 @@ -provider "aws" { - version = "~> 2.0" - region = "us-west-2" -} - -resource "aws_s3_bucket" "simple" { - bucket = "my-simple-bucket" - acl = "private" -} - -output "bucket_id" { - value = "${aws_s3_bucket.simple.id}" -} diff --git a/tests/fixtures/0.12/backup-test/main.tf b/tests/fixtures/0.12/backup-test/main.tf deleted file mode 100644 index 3760d41a..00000000 --- a/tests/fixtures/0.12/backup-test/main.tf +++ /dev/null @@ -1,21 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - } -} - -provider "aws" { - version = "~> 2.0" - region = "us-west-2" -} - -resource "aws_s3_bucket" "simple" { - bucket = "my-simple-bucket" - acl = "private" -} - -output "bucket_id" { - value = "${aws_s3_bucket.simple.id}" -} diff --git a/tests/fixtures/0.12/backup-test/terraform-upgrade-dryrun-report.md b/tests/fixtures/0.12/backup-test/terraform-upgrade-dryrun-report.md deleted file mode 100644 index ae9bf5f1..00000000 --- a/tests/fixtures/0.12/backup-test/terraform-upgrade-dryrun-report.md +++ /dev/null @@ -1,12 +0,0 @@ - -## Summary - -- Current Version: 0.12 -- Upgrade Path: 0.13 → 0.14 → 0.15 → 1.0 -- Complexity Level: Low -- Complexity Score: 8.5 - -### Deprecated Syntax - -- interpolation_only: `"${aws_s3_bucket.simple.id}"` in main.tf -- quoted_interpolation: `"${aws_s3_bucket.simple.id}"` in main.tf diff --git a/tests/fixtures/0.12/backup-test/terraform-upgrade-reports/upgrade-report-20250327-195934.md b/tests/fixtures/0.12/backup-test/terraform-upgrade-reports/upgrade-report-20250327-195934.md deleted file mode 100644 index 3baaec34..00000000 --- a/tests/fixtures/0.12/backup-test/terraform-upgrade-reports/upgrade-report-20250327-195934.md +++ /dev/null @@ -1,29 +0,0 @@ -# Terraform Upgrade Report - -Report generated on 2025-03-27 19:59:34 - -## Progress - -| Step | Status | Duration | Details | -|------|--------|----------|--------| -| 1: Upgrading to Terraform 0.13 | ❌ Failed | 2s | **Error:** Failed to upgrade to Terraform 0.13 | - -## Summary - -- **Status:** ❌ Failed -- **Total Duration:** 2s -- **Steps Completed:** 1/1 -- **Successful Steps:** 0 -- **Failed Steps:** 1 - -## Details - -- **Started:** 2025-03-27 19:59:34 -- **Completed:** 2025-03-27 19:59:36 - -### Failed Steps - -#### Step 1: Upgrading to Terraform 0.13 - -- **Details:** Failed to upgrade to Terraform 0.13 - diff --git a/tests/fixtures/0.12/backup-test/upgrade-logs/upgrade-progress.json b/tests/fixtures/0.12/backup-test/upgrade-logs/upgrade-progress.json deleted file mode 100644 index 3ccc29a5..00000000 --- a/tests/fixtures/0.12/backup-test/upgrade-logs/upgrade-progress.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "started_at": "2025-03-27T19:59:24.695824", - "steps": [], - "completed": 0, - "total": 0, - "status": "in_progress" -} \ No newline at end of file diff --git a/tests/fixtures/0.12/simple/upgrade-logs/upgrade-progress.json b/tests/fixtures/0.12/simple/upgrade-logs/upgrade-progress.json index 3ccc29a5..fa985564 100644 --- a/tests/fixtures/0.12/simple/upgrade-logs/upgrade-progress.json +++ b/tests/fixtures/0.12/simple/upgrade-logs/upgrade-progress.json @@ -1,5 +1,5 @@ { - "started_at": "2025-03-27T19:59:24.695824", + "started_at": "2025-04-01T18:46:37.696653", "steps": [], "completed": 0, "total": 0, diff --git a/tf_bulk_upgrade.py b/tf_bulk_upgrade.py new file mode 100755 index 00000000..b6258969 --- /dev/null +++ b/tf_bulk_upgrade.py @@ -0,0 +1,429 @@ +#!/usr/bin/env python3 +""" +Terraform Bulk Upgrade Tool + +This script enables bulk upgrades of Terraform configurations +across an entire repository, creating separate Git branches +and PRs for each directory where changes are made. +""" + +import argparse +import logging +import os +import subprocess +import sys + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger("tf_bulk_upgrade") + + +def git_cmd(args, cwd, check=True): + """Run a git command and return the output.""" + cmd = ["git"] + args + try: + result = subprocess.run( + cmd, + cwd=cwd, + check=check, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + return result.stdout.strip(), result.stderr.strip(), result.returncode == 0 + except subprocess.CalledProcessError as e: + logger.error(f"Git command failed: {e}") + return None, str(e), False + + +def is_git_repo(path): + """Check if a directory is a git repository.""" + return os.path.isdir(os.path.join(path, ".git")) + + +def get_repo_root(path): + """Get the git repository root directory.""" + output, error, success = git_cmd(["rev-parse", "--show-toplevel"], path) + if success: + return output + return None + + +def create_branch(repo_path, branch_name): + """Create a new git branch.""" + # Check if branch already exists + output, _, success = git_cmd(["branch", "--list", branch_name], repo_path) + if output: + logger.info(f"Branch {branch_name} already exists") + # Checkout the branch + _, _, success = git_cmd(["checkout", branch_name], repo_path) + return success + + # Create and checkout new branch from current HEAD + _, _, success = git_cmd(["checkout", "-b", branch_name], repo_path) + if success: + logger.info(f"Created and checked out branch: {branch_name}") + return True + return False + + +def commit_changes(repo_path, message): + """Commit changes in the git repository.""" + # Add all changes + _, _, add_success = git_cmd(["add", "."], repo_path) + if not add_success: + logger.error("Failed to add changes to git index") + return False + + # Commit changes + _, _, commit_success = git_cmd(["commit", "-m", message], repo_path, check=False) + if commit_success: + logger.info(f"Committed changes with message: {message}") + return True + else: + # Check if there were no changes to commit + status, _, _ = git_cmd(["status", "--porcelain"], repo_path) + if not status: + logger.info("No changes to commit") + return True + logger.error("Failed to commit changes") + return False + + +def push_branch(repo_path, branch_name, remote="origin"): + """Push a branch to the remote repository.""" + _, _, success = git_cmd(["push", "-u", remote, branch_name], repo_path) + if success: + logger.info(f"Pushed branch {branch_name} to {remote}") + return True + return False + + +def run_upgrade_tool(directory, target_version): + """ + Run the terraform upgrade tool on a directory. + + Args: + directory: Directory to upgrade + target_version: Target Terraform version + + Returns: + bool: True if successful, False otherwise + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + + cmd = [ + sys.executable, + "-m", + "tf_upgrade.cli", + "upgrade", + directory, + "--target-version", + target_version, + ] + + try: + logger.info(f"Running: {' '.join(cmd)}") + result = subprocess.run( + cmd, + cwd=script_dir, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + if result.returncode == 0: + logger.info( + f"Successfully upgraded {directory} to version {target_version}" + ) + logger.debug(result.stdout) + return True + else: + logger.error(f"Failed to upgrade {directory}: {result.stderr}") + return False + + except Exception as e: + logger.error(f"Error running upgrade tool: {str(e)}") + return False + + +def upgrade_tf_directory(directory, target_version, branch_suffix=None): + """ + Upgrade a Terraform directory to the target version and commit changes to a branch. + + Args: + directory: The directory to upgrade + target_version: Target Terraform version (e.g. "0.13") + branch_suffix: Suffix for the branch name (default: None) + + Returns: + tuple: (success, branch_name) + """ + # Check if directory is in a git repo + repo_root = get_repo_root(directory) + if not repo_root: + logger.error(f"Directory {directory} is not in a git repository") + return False, None + + # Save current branch + current_branch, _, _ = git_cmd(["rev-parse", "--abbrev-ref", "HEAD"], repo_root) + + # Create normalized path for branch naming + rel_path = os.path.relpath(directory, repo_root) + safe_path = rel_path.replace("/", "-").replace(".", "-") + + # Create branch name + branch_name = f"tf-upgrade-{target_version}-{safe_path}" + if branch_suffix: + branch_name += f"-{branch_suffix}" + + # Create branch for this upgrade + if not create_branch(repo_root, branch_name): + logger.error(f"Failed to create branch {branch_name}") + return False, None + + # Run the upgrade + logger.info(f"Upgrading directory {directory} to Terraform {target_version}") + try: + # Run the upgrade tool + success = run_upgrade_tool(directory, target_version) + if not success: + logger.error(f"Failed to upgrade directory {directory}") + git_cmd(["checkout", current_branch], repo_root) + return False, None + + # Check if any files were modified by looking for backup directories + backup_dirs = [ + d + for d in os.listdir(directory) + if d.startswith(".terraform-upgrade-backup-") + ] + if not backup_dirs: + logger.info(f"No changes were made in {directory}") + git_cmd(["checkout", current_branch], repo_root) + return False, None + + # Commit the changes + commit_message = f"Upgrade Terraform to {target_version} in {rel_path}" + if not commit_changes(repo_root, commit_message): + logger.error(f"Failed to commit changes for {directory}") + git_cmd(["checkout", current_branch], repo_root) + return False, None + + # Return to original branch + git_cmd(["checkout", current_branch], repo_root) + return True, branch_name + + except Exception as e: + logger.error(f"Error upgrading {directory}: {str(e)}") + # Return to original branch + git_cmd(["checkout", current_branch], repo_root) + return False, None + + +def run_scan_command(repo_path, recursive=True, max_depth=20): + """ + Run the terraform scan command to find terraform directories + + Args: + repo_path: Repository path + recursive: Whether to scan recursively + max_depth: Maximum directory depth for recursion + + Returns: + dict: Scan results including terraform_dirs + """ + script_dir = os.path.dirname(os.path.abspath(__file__)) + recursive_flag = "--recursive" if recursive else "--no-recursive" + + cmd = [ + sys.executable, + "-m", + "tf_upgrade.cli", + "scan", + repo_path, + recursive_flag, + "--max-depth", + str(max_depth), + ] + + try: + logger.info(f"Running: {' '.join(cmd)}") + result = subprocess.run( + cmd, + cwd=script_dir, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + if result.returncode != 0: + logger.error(f"Failed to scan repository: {result.stderr}") + return {"terraform_dirs": []} + + # Parse output to find terraform directories + terraform_dirs = [] + + # Look for lines with "Found X Terraform configurations" + for line in result.stdout.splitlines(): + if "directories scanned" in line: + logger.info(line) + + # Let's look for the files specifically + find_cmd = ["find", repo_path, "-name", "*.tf"] + find_result = subprocess.run( + find_cmd, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + if find_result.returncode == 0: + # Get unique directories containing .tf files + for file_path in find_result.stdout.splitlines(): + dir_path = os.path.dirname(file_path) + if dir_path not in terraform_dirs: + terraform_dirs.append(dir_path) + + return {"terraform_dirs": terraform_dirs} + + except Exception as e: + logger.error(f"Error scanning repository: {str(e)}") + return {"terraform_dirs": []} + + +def get_terraform_directories(repo_path, skip_dirs=None): + """ + Find all directories containing Terraform files in a repository. + + Args: + repo_path: Repository path + skip_dirs: List of directories to skip (default: None) + + Returns: + list: Directories containing Terraform files + """ + if skip_dirs is None: + skip_dirs = [] + + # Add default directories to skip + skip_dirs.extend([".git", ".terraform", "node_modules"]) + + # Run the scan command to find Terraform directories + result = run_scan_command(repo_path, recursive=True, max_depth=20) + + # Filter directories that should be skipped + terraform_dirs = [] + for dir_path in result.get("terraform_dirs", []): + skip = False + for skip_dir in skip_dirs: + if skip_dir in dir_path.split(os.path.sep): + skip = True + break + if not skip: + terraform_dirs.append(dir_path) + + return terraform_dirs + + +def main(): + parser = argparse.ArgumentParser( + description="Bulk upgrade Terraform configurations with separate PRs" + ) + parser.add_argument("repo_path", help="Path to the repository") + parser.add_argument( + "--target-version", help="Target Terraform version", default="1.0" + ) + parser.add_argument("--push", help="Push branches to remote", action="store_true") + parser.add_argument( + "--skip-dirs", help="Directories to skip (comma-separated)", default="" + ) + parser.add_argument( + "--limit", help="Limit number of directories to upgrade", type=int, default=0 + ) + parser.add_argument( + "--dry-run", + help="Don't make any changes, just show what would be done", + action="store_true", + ) + parser.add_argument( + "--branch-suffix", help="Optional suffix for branch names", default="" + ) + + args = parser.parse_args() + + repo_path = os.path.abspath(args.repo_path) + if not os.path.isdir(repo_path): + logger.error(f"Repository path does not exist: {repo_path}") + return 1 + + if not is_git_repo(repo_path): + logger.error(f"Not a git repository: {repo_path}") + return 1 + + # Parse skip directories + skip_dirs = [d.strip() for d in args.skip_dirs.split(",") if d.strip()] + + # Find Terraform directories + logger.info(f"Scanning repository for Terraform directories: {repo_path}") + tf_dirs = get_terraform_directories(repo_path, skip_dirs) + logger.info(f"Found {len(tf_dirs)} Terraform directories") + + # Apply limit if specified + if args.limit > 0 and len(tf_dirs) > args.limit: + logger.info(f"Limiting to {args.limit} directories") + tf_dirs = tf_dirs[: args.limit] + + # Perform upgrades + successful_upgrades = [] + failed_upgrades = [] + + for directory in tf_dirs: + logger.info(f"Processing directory: {directory}") + + if args.dry_run: + logger.info( + f"DRY RUN: Would upgrade {directory} to Terraform {args.target_version}" + ) + continue + + success, branch_name = upgrade_tf_directory( + directory, args.target_version, branch_suffix=args.branch_suffix + ) + + if success: + successful_upgrades.append((directory, branch_name)) + if args.push: + push_branch(repo_path, branch_name) + else: + failed_upgrades.append(directory) + + # Print summary + logger.info("=" * 50) + logger.info("UPGRADE SUMMARY") + logger.info("=" * 50) + logger.info(f"Total directories processed: {len(tf_dirs)}") + logger.info(f"Successful upgrades: {len(successful_upgrades)}") + logger.info(f"Failed upgrades: {len(failed_upgrades)}") + + if successful_upgrades: + logger.info("\nSuccessfully upgraded directories:") + for directory, branch in successful_upgrades: + logger.info(f" - {directory} (branch: {branch})") + + if failed_upgrades: + logger.info("\nFailed upgrade directories:") + for directory in failed_upgrades: + logger.info(f" - {directory}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tf_upgrade/env_validator.py b/tf_upgrade/env_validator.py index f35856d1..6ddf67c7 100644 --- a/tf_upgrade/env_validator.py +++ b/tf_upgrade/env_validator.py @@ -185,7 +185,7 @@ def check_tf_upgrade_readiness(dir_path): def validate_environment(dir_path): """ - Perform comprehensive environment validation for Terraform upgrading. + Validate the environment for terraform upgrades. Args: dir_path: Directory to validate @@ -193,33 +193,63 @@ def validate_environment(dir_path): Returns: Dictionary with validation results """ + from tf_upgrade.utils.git import validate_git_access + from tf_upgrade.utils.terraform import get_terraform_binary + results = { "status": "PASS", - "terraform_versions": {}, - "config_files": {}, "errors": [], "warnings": [], + "messages": [], + "terraform_versions": {}, + "config_files": {}, + "aws_profile": {}, + "git_access": {}, } - # Check for required binaries - terraform_versions = { - "0.12": ["terraform_0.12.31"], - "0.13": ["terraform_0.13.7"], - "0.14": ["terraform_0.14.11"], - "0.15": ["terraform_0.15.5"], - "1.0": ["terraform_1.0.11", "terraform_1.0.10"], - "1.10": ["terraform_1.10.5"], + # Check required terraform versions + terraform_paths = ["/apps/terraform/bin", "/usr/local/bin", "/usr/bin"] + required_versions = ["0.12", "0.13", "0.14", "0.15", "1.0"] + + # Map of binary names to version identifiers + version_binary_map = { + "0.12": ["terraform_0.12", "terraform_0.12.31"], + "0.13": ["terraform_0.13", "terraform_0.13.7"], + "0.14": ["terraform_0.14", "terraform_0.14.11"], + "0.15": ["terraform_0.15", "terraform_0.15.5"], + "1.0": [ + "terraform_1.0", + "terraform_1.0.11", + "terraform_current", + "terraform_1.3.10", + ], } - # Check in both standard PATH and in /apps/terraform/bin - terraform_paths = ["/apps/terraform/bin"] + for version in required_versions: + binaries = version_binary_map.get(version, [f"terraform_{version}"]) + + # Special handling for terraform_current + # which is a symlink to the latest 1.x version + if version == "1.0": + # Try checking terraform_current explicitly + terraform_current = "/apps/terraform/bin/terraform_current" + if os.path.exists(terraform_current) and os.access( + terraform_current, os.X_OK + ): + results["terraform_versions"][version] = True + results["messages"].append( + f"Found Terraform {version} as {terraform_current}" + ) + continue - for version, binaries in terraform_versions.items(): found = False for binary in binaries: # Check in standard PATH if shutil.which(binary) is not None: found = True + results["messages"].append( + f"Found Terraform {version} as {binary} in PATH" + ) break # Check in terraform_paths @@ -227,6 +257,9 @@ def validate_environment(dir_path): full_path = os.path.join(path, binary) if os.path.exists(full_path) and os.access(full_path, os.X_OK): found = True + results["messages"].append( + f"Found Terraform {version} as {full_path}" + ) break results["terraform_versions"][version] = found @@ -244,36 +277,40 @@ def validate_environment(dir_path): # Check terraform configuration files config_files = find_terraform_config_files(dir_path) results["config_files"] = { - "tf_control": config_files.get("tf_control"), - "tf_control_tfrc": config_files.get("tf_control_tfrc"), - "terraform_files": len(config_files.get("tf_files", [])), - "tfvars_files": len(config_files.get("tfvars_files", [])), + "tf_files": len(config_files["tf_files"]), + "tfvars_files": len(config_files["tfvars_files"]), + "tf_control": config_files["tf_control"], } - if not config_files.get("tf_files"): - results["warnings"].append( - "No Terraform configuration files found in directory" - ) - - # Check if terraform readiness - readiness = check_tf_upgrade_readiness(dir_path) - if not readiness["ready"]: - results["status"] = "FAIL" - results["errors"].extend(readiness["errors"]) - results["warnings"].extend(readiness["warnings"]) - # Check AWS profile aws_success, aws_profile, aws_account = validate_aws_profile(dir_path) + results["aws_profile"] = { + "success": aws_success, + "profile": aws_profile, + "account": aws_account, + } + if not aws_success: - results["warnings"].append(f"AWS profile validation failed for '{aws_profile}'") + if aws_profile: + results["warnings"].append(f"AWS profile '{aws_profile}' validation failed") + else: + results["warnings"].append("No AWS profile found") - # Check git repo status + # Check Git repository access git_results = validate_git_access(dir_path) results["git_access"] = git_results - if git_results.get("is_git_repo") and not git_results.get("can_read"): - results["errors"].append("Cannot read from Git repository") + if not git_results.get("is_git_repo", False): + results["warnings"].append("Directory is not in a Git repository") + elif not git_results.get("can_push", False): + results["warnings"].append("Cannot push to Git repository") + + # Basic terraform binary access check + try: + terraform_binary = get_terraform_binary(dir_path) + results["messages"].append(f"Terraform binary found: {terraform_binary}") + except Exception as e: + results["errors"].append(f"Error finding terraform binary: {str(e)}") results["status"] = "FAIL" - # Return final validation results return results diff --git a/tf_upgrade/utils/terraform.py b/tf_upgrade/utils/terraform.py index 0dbf260d..6c4ef054 100644 --- a/tf_upgrade/utils/terraform.py +++ b/tf_upgrade/utils/terraform.py @@ -272,19 +272,21 @@ def get_terraform_binary(dir_path, version=None): "0.15": "terraform_0.15.5", "1.0": "terraform_1.0.11", "1.10": "terraform_1.10.5", + "1.3": "terraform_current", # Add explicit handling for terraform_current + "current": "terraform_current", } binary = version_map.get(version, f"terraform_{version}") logger.info(f"Using specified terraform version: {binary}") - # Check if binary exists in PATH - binary_path = shutil.which(binary) + # Check if binary exists in PATH - ensure we're using strings not bytes + binary_path = shutil.which(str(binary)) if binary_path: - return binary_path + return str(binary_path) # Check in terraform_paths for path in terraform_paths: - full_path = os.path.join(path, binary) + full_path = os.path.join(str(path), str(binary)) if os.path.exists(full_path) and os.access(full_path, os.X_OK): logger.info(f"Found terraform binary: {full_path}") return full_path @@ -295,9 +297,10 @@ def get_terraform_binary(dir_path, version=None): ) # Check standard terraform binary - if shutil.which("terraform") is not None: + terraform_path = shutil.which("terraform") + if terraform_path is not None: logger.warning("Falling back to standard terraform binary") - return "terraform" + return str(terraform_path) else: logger.error("No terraform binary found") raise FileNotFoundError(f"Terraform binary {binary} not found") @@ -311,12 +314,13 @@ def get_terraform_binary(dir_path, version=None): # If the command is not an absolute path, check if it exists in PATH if not os.path.isabs(tfcommand): # First check in PATH - if shutil.which(tfcommand) is not None: - return tfcommand + tf_path = shutil.which(str(tfcommand)) + if tf_path is not None: + return str(tf_path) # Then check in terraform_paths for path in terraform_paths: - full_path = os.path.join(path, tfcommand) + full_path = os.path.join(str(path), str(tfcommand)) if os.path.exists(full_path) and os.access(full_path, os.X_OK): logger.info(f"Found terraform command: {full_path}") return full_path @@ -325,9 +329,10 @@ def get_terraform_binary(dir_path, version=None): logger.warning( f"Terraform command {tfcommand} specified in .tf-control " f"not found" ) - if shutil.which("terraform") is not None: + terraform_path = shutil.which("terraform") + if terraform_path is not None: logger.warning("Falling back to standard terraform binary") - return "terraform" + return str(terraform_path) else: logger.error("No terraform binary found") raise FileNotFoundError(f"Terraform binary {tfcommand} not found") @@ -335,13 +340,26 @@ def get_terraform_binary(dir_path, version=None): logger.info(f"Using terraform command from .tf-control: {tfcommand}") return tfcommand + # Try explicitly using terraform_current for 1.x version + terraform_current = "/apps/terraform/bin/terraform_current" + if os.path.exists(terraform_current) and os.access(terraform_current, os.X_OK): + logger.info("Using terraform_current from /apps/terraform/bin") + return terraform_current + # Default to standard terraform command - if shutil.which("terraform") is not None: + terraform_path = shutil.which("terraform") + if terraform_path is not None: logger.info("Using default terraform binary") - return "terraform" + return str(terraform_path) # Last resort: check in /apps/terraform/bin for any version for path in terraform_paths: + # Look specifically for terraform_current first + current_path = os.path.join(str(path), "terraform_current") + if os.path.exists(current_path) and os.access(current_path, os.X_OK): + logger.info(f"Using terraform_current: {current_path}") + return current_path + # List all terraform binaries and take the newest available one if os.path.isdir(path): terraform_binaries = [ @@ -354,7 +372,7 @@ def get_terraform_binary(dir_path, version=None): if terraform_binaries: # Sort to get the latest version terraform_binaries.sort(reverse=True) - binary = os.path.join(path, terraform_binaries[0]) + binary = os.path.join(str(path), str(terraform_binaries[0])) logger.info(f"Using available terraform binary: {binary}") return binary @@ -555,7 +573,7 @@ def get_available_terraform_versions(): # This is a placeholder implementation # In a real implementation, we would scan for installed terraform versions # or query the HashiCorp releases API - return ["0.12.31", "0.13.7", "0.14.11", "0.15.5", "1.0.11", "1.10.5"] + return ["0.12.31", "0.13.7", "0.14.11", "0.15.5", "1.3.10", "1.10.5"] except Exception as e: logger.error(f"Error getting available Terraform versions: {e}") return [] diff --git a/tf_upgrade/version_upgraders/v0_13.py b/tf_upgrade/version_upgraders/v0_13.py index b1b2d080..b5f212bf 100644 --- a/tf_upgrade/version_upgraders/v0_13.py +++ b/tf_upgrade/version_upgraders/v0_13.py @@ -3,6 +3,7 @@ """ import logging +import os from tf_upgrade.utils.terraform import run_terraform_command from tf_upgrade.utils.terraform_runner import TerraformRunner @@ -15,6 +16,18 @@ class Terraform013Upgrader(TerraformUpgrader): """Handles upgrades from Terraform 0.12 to 0.13.""" + def __init__(self, directory, config_manager=None, backup=True): + """ + Initialize the 0.13 upgrader. + + Args: + directory: Directory containing Terraform files to upgrade + config_manager: Optional configuration manager + backup: Whether to create backups before modifications + """ + super().__init__(directory, config_manager, backup) + self.progress = None + @property def source_version(self): return "0.12" @@ -23,6 +36,11 @@ def source_version(self): def target_version(self): return "0.13" + @property + def version(self): + """Get the target Terraform version for this upgrader.""" + return self.target_version + def pre_upgrade_check(self): """Verify configuration is compatible with 0.13 upgrade.""" # Run validations specific to 0.12 to 0.13 upgrade @@ -86,52 +104,87 @@ def apply_syntax_transformations(self): return result + def update_required_providers(self): + """ + Update the required_providers block for Terraform 0.13 compatibility. + This creates or updates the versions.tf + file with proper provider source attributes. + """ + try: + # Check if versions.tf exists + versions_file = os.path.join(self.directory, "versions.tf") + versions_content = """terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + } + required_version = ">= 0.13" +} +""" + # Write the versions.tf file if it doesn't exist + with open(versions_file, "w") as f: + f.write(versions_content) + + logger.info(f"Created/updated versions.tf file in {self.directory}") + return True + except Exception as e: + logger.error(f"Failed to update required providers: {str(e)}") + raise + def run_built_in_upgrade_commands(self): """Run the terraform 0.13upgrade command.""" # Set up upgraded terraform runner runner = TerraformRunner(self.directory, "0.13") + logs = {} # First run init init_success, init_output, init_log = runner.init() + logs["init"] = init_log if not init_success: return { "success": False, "details": f"terraform init failed. See {init_log}", - "logs": {"init": init_log}, + "logs": logs, } - self.progress.start_step("Updating required providers") + # Update required providers try: self.update_required_providers() - self.progress.complete_step(success=True) except Exception as e: - self.progress.complete_step(success=False, details=str(e)) - return False + return { + "success": False, + "details": f"Failed to update required providers: {str(e)}", + "logs": logs, + } - self.progress.start_step("Running Terraform 0.13upgrade") + # Run 0.13upgrade command success, output, log_file = run_terraform_command( - "0.13upgrade -yes", self.directory, terraform_version=self.version + "0.13upgrade -yes", self.directory, terraform_version="0.13" ) - if not success: - self.progress.complete_step( - success=False, - details=( - f"Terraform 0.13upgrade failed. " f"See log for details: {log_file}" - ), - ) - return False + logs["0.13upgrade"] = log_file - self.progress.complete_step(success=True) + if not success: + return { + "success": False, + "details": f"0.13upgrade failed. See log for details: {log_file}", + "logs": logs, + } # Validate the upgraded configuration - self.progress.start_step("Validating upgraded configuration") success, details = self.validate_configuration() if not success: - self.progress.complete_step(success=False, details=details) - return False - self.progress.complete_step(success=True) + return { + "success": False, + "details": details, + "logs": logs, + } - return True + return { + "success": True, + "details": "Successfully upgraded to Terraform 0.13", + "logs": logs, + } def post_upgrade_actions(self): """Perform post-upgrade actions like formatting."""