Files
nova-platform/adapters/terraform/adapter.py
T
CIAgent 2443909362 docs(milestone): complete v1.0-nova-platform (release v0.1.6)
---ci---
project: nova-platform
milestone: v1.0
status: complete
requirements:
  covered: [REQ-01,REQ-02,REQ-03,REQ-04,REQ-05,REQ-06,REQ-07,REQ-08,REQ-09,REQ-10,REQ-11,REQ-12,REQ-13,REQ-14,REQ-15,REQ-16,REQ-17,REQ-18,REQ-19,REQ-20,REQ-21,REQ-22,REQ-23,REQ-24,REQ-25,REQ-26,REQ-27,REQ-28,REQ-29,REQ-30,REQ-31,REQ-32,REQ-33,REQ-34,REQ-35,REQ-36,REQ-37,REQ-38]
  partial: []
---/ci---

v1.0 milestone complete: simplified infrastructure-delivery platform
derived from Nova (acdl). 6 phases (P0-P5 + P6 final). 38 REQ-IDs.
38 decisions (D-001..D-038). 76 tests pass. Engine boundary holds.
Happy paths green (check-only + CI). 13 L1 + 2 L2 modules. 5 terraform
roots. Shell reproducibility. Zero OOS files.

Tags: v0.1.0 (P0) → v0.1.1..v0.1.5 (P1..P5) → v0.1.6 (P6 = milestone
release on v0.1 patch line).
2026-08-25 02:18:35 +00:00

102 lines
3.4 KiB
Python

"""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 = ...; <inputs> }`
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 = ...; <inputs> }` 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 <stack.json> [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())