"""Nova Platform — Terraform Adapter. The ONLY engine-specific code in the platform (per REQ-09, verified by tests/test_engine_boundary.py). Loads modules/registry.json internally to map module -> terraform_dir (per D-037/C-1 grill fix — the resolver does NOT put a `source` field in the stack; the adapter resolves it here, inside the engine boundary). Stateless assembler: no `terraform` CLI invocation, no state files, no plan files. Emits Terraform HCL: one `module "x" { source = ...; }` block per stack resource. """ import json import os from pathlib import Path def _load_registry(repo_root): """Load modules/registry.json -> {module_name: terraform_dir}.""" registry_path = os.path.join(str(repo_root), "modules", "registry.json") with open(registry_path) as fh: registry = json.load(fh) return {name: list(versions.values())[0].get("terraform_dir") for name, versions in registry.items() if list(versions.values())[0].get("terraform_dir")} def _tf_value(value): """Render a Python value as an HCL expression.""" if isinstance(value, bool): return "true" if value else "false" if isinstance(value, (int, float)): return str(value) if isinstance(value, list): return "[" + ", ".join(_tf_value(v) for v in value) + "]" if isinstance(value, dict): return "{ " + ", ".join(f"{k} = {_tf_value(v)}" for k, v in value.items()) + " }" return json.dumps(str(value)) def _emit_module_block(resource, terraform_dirs, repo_root): """Emit one `module "x" { source = ...; }` block.""" module_name = resource["module"] rid = module_name.replace("-", "_") tf_dir = terraform_dirs.get(module_name) if tf_dir is None: raise ValueError(f"module '{module_name}' has no terraform_dir in registry") source = os.path.join(str(repo_root), tf_dir) lines = [f'module "{rid}" {{', f' source = "{source}"'] for key, val in resource.get("inputs", {}).items(): if key == "region": continue lines.append(f" {key} = {_tf_value(val)}") lines.append("}") return "\n".join(lines) def adapt(stack, repo_root): """Compile a flat stack dict to Terraform HCL. Args: stack: a flat stack dict conforming to schemas/stack.schema.json (NO `source` field per D-037 — the adapter resolves module -> terraform_dir via the registry). repo_root: Path to the repo root (the adapter loads modules/registry.json from here). Returns: A string of Terraform HCL with one `module "x" {}` block per stack resource. """ terraform_dirs = _load_registry(repo_root) blocks = [] for resource in stack.get("resources", []): blocks.append(_emit_module_block(resource, terraform_dirs, repo_root)) return "\n\n".join(blocks) + "\n" def main(argv=None): import sys argv = argv or sys.argv[1:] if len(argv) < 1: print("usage: adapter.py [out.tf]", file=sys.stderr) return 2 stack_path = argv[0] out_path = argv[1] if len(argv) > 1 else None repo_root = Path(__file__).resolve().parent.parent.parent with open(stack_path) as fh: stack = json.load(fh) hcl = adapt(stack, repo_root) if out_path: with open(out_path, "w") as fh: fh.write(hcl) else: print(hcl) return 0 if __name__ == "__main__": import sys sys.exit(main())