#!/usr/bin/env python3 """OpenTofu operations: init and validate across directories. Handles initialization and validation of OpenTofu configurations across multiple directories (modules + environments). Supports CI mode with ``-backend=false`` to avoid state backend access. Usage:: python3 -m devx.tools.tofu_ops init --env staging python3 -m devx.tools.tofu_ops validate python3 -m devx.tools.tofu_ops validate --ci """ from __future__ import annotations import subprocess # nosec B404 from pathlib import Path import click from devx.i18n import _ DEFAULT_ENV_DIRS = ["tofu/environments/{env}", "tofu/environments/dns"] DEFAULT_VALIDATE_DIRS = [ "tofu/modules/hetzner-vm", "tofu/modules/hetzner-network", "tofu/environments/staging", "tofu/environments/production", "tofu/environments/dns", ] def _run_tofu(cmd: list[str], cwd: Path) -> None: """Run a tofu command in the given directory, raising on failure.""" click.echo(_(" -> {dir}", dir=cwd)) result = subprocess.run( # nosec B603, B607 cmd, cwd=str(cwd), capture_output=True, text=True, check=False, ) if result.returncode != 0: raise click.ClickException( _("tofu command failed in {dir}: {error}", dir=cwd, error=result.stderr.strip()), ) def tofu_init(env: str, root: str = ".", extra_dirs: list[str] | None = None) -> None: """Run ``tofu init`` in the environment directory and DNS directory. Args: env: Environment name (e.g. staging, production). root: Repository root directory. extra_dirs: Additional directory patterns to initialize. """ root_path = Path(root) dirs = [d.format(env=env) for d in (extra_dirs or DEFAULT_ENV_DIRS)] for dir_pattern in dirs: dir_path = root_path / dir_pattern if dir_path.is_dir(): click.echo(_("[tofu-init] Initializing {dir}...", dir=dir_path)) _run_tofu(["tofu", "init"], dir_path) click.echo(_("[tofu-init] Done.")) def tofu_validate( root: str = ".", dirs: list[str] | None = None, ci: bool = False, ) -> None: """Run ``tofu validate`` in all OpenTofu directories. In CI mode, runs ``tofu init -backend=false`` before validate to avoid state backend access. Args: root: Repository root directory. dirs: List of directory paths to validate (relative to root). ci: If True, use CI mode with -backend=false. """ root_path = Path(root) target_dirs = dirs or DEFAULT_VALIDATE_DIRS mode = "ci" if ci else "validate" click.echo(_("[tofu-{mode}] Validating OpenTofu configurations...", mode=mode)) for dir_rel in target_dirs: dir_path = root_path / dir_rel if not dir_path.is_dir(): continue if ci: _run_tofu(["tofu", "init", "-backend=false", "-input=false"], dir_path) _run_tofu(["tofu", "validate"], dir_path) click.echo(_("[tofu-{mode}] All configurations valid.", mode=mode)) @click.group() def cli() -> None: """OpenTofu operations.""" @cli.command() @click.option("--env", required=True, help="Environment name (staging, production).") @click.option("--root", default=".", help="Repository root directory.") def init(env: str, root: str) -> None: """Initialize OpenTofu in an environment.""" tofu_init(env, root) @cli.command() @click.option("--root", default=".", help="Repository root directory.") @click.option("--ci", is_flag=True, default=False, help="CI mode: use -backend=false.") def validate(root: str, ci: bool) -> None: """Validate OpenTofu configurations.""" tofu_validate(root, ci=ci) if __name__ == "__main__": # pragma: no cover cli() # pragma: no cover