"""CLI entry point for the CloudFormation Stack Updater.""" from __future__ import annotations import argparse import asyncio import sys from datetime import datetime, timezone import boto3 from cfn_updater.config import DEFAULT_CONCURRENCY, DEFAULT_PREFIX, STACK_PROFILES, TEMPLATE_URL from cfn_updater.discovery import discover_stacks from cfn_updater.permissions import validate_permissions from cfn_updater.report import format_report, generate_report, get_exit_code from cfn_updater.updater import format_dry_run_output, update_all_stacks def parse_args(argv: list[str] | None = None) -> argparse.Namespace: """Parse command-line arguments.""" parser = argparse.ArgumentParser( description="One-click updater for CL-AppPipe-* and CL-SvcPipe-* CloudFormation stacks.", ) parser.add_argument( "--prefix", type=str, default=None, help=f"Stack name prefix to match (default: all configured prefixes)", ) parser.add_argument( "--concurrency", type=int, default=DEFAULT_CONCURRENCY, help=f"Max parallel updates (default: {DEFAULT_CONCURRENCY})", ) parser.add_argument( "--dry-run", action="store_true", default=False, help="Preview mode — discover stacks without performing updates", ) parser.add_argument( "--region", type=str, default=None, help="AWS region override (default: SDK default)", ) parser.add_argument( "--profile", type=str, default=None, help="AWS profile name to use (e.g. 'audit')", ) return parser.parse_args(argv) def _resolve_prefixes(prefix: str | None) -> dict[str, str]: """Return a prefix→template_url mapping based on CLI input. If --prefix is given, look it up in STACK_PROFILES (fall back to the default template URL for unknown prefixes). If omitted, return all configured profiles. """ if prefix is not None: url = STACK_PROFILES.get(prefix, TEMPLATE_URL) return {prefix: url} return dict(STACK_PROFILES) def main(argv: list[str] | None = None) -> int: """Entry point. Returns 0 on full success, 1 if any stack failed, 2 on permission failure.""" args = parse_args(argv) # Create boto3 session with optional profile and region session_kwargs: dict = {} if args.profile: session_kwargs["profile_name"] = args.profile if args.region: session_kwargs["region_name"] = args.region session = boto3.Session(**session_kwargs) cfn_client = session.client("cloudformation") # Validate permissions missing = validate_permissions(cfn_client) if missing: print(f"Missing required permissions: {', '.join(missing)}") return 2 # Resolve which prefixes to process prefix_map = _resolve_prefixes(args.prefix) any_failed = False for prefix, template_url in prefix_map.items(): print(f"\n--- Processing prefix: {prefix} ---") print(f" Template: {template_url}") # Discover stacks for this prefix stacks = discover_stacks(cfn_client, prefix) print(f" Discovered {len(stacks)} stack(s).") if not stacks: print(" No stacks found matching the prefix. Skipping.") continue # Dry-run mode if args.dry_run: output = format_dry_run_output(stacks) print(output) continue # Run updates start_time = datetime.now(timezone.utc) results = asyncio.run( update_all_stacks(cfn_client, stacks, template_url, args.concurrency) ) end_time = datetime.now(timezone.utc) # Generate and print report report = generate_report(results, len(stacks), start_time, end_time) print(format_report(report)) if get_exit_code(report) != 0: any_failed = True return 1 if any_failed else 0 if __name__ == "__main__": sys.exit(main())