"""ACDL Terraform adapter — compile a Target Stack IR instance to Terraform. ARCHITECTURE.md §12.2: the adapter translates the IR-typed L1 interface to a Terraform variable/output block, the L2 thin-composition tree to a root module that calls the L1 modules, the IR-typed relationships to Terraform module references, and emits a Terraform plan from the IR. The adapter is a THIN LAYER; it does not own L1/L2 content — it only translates. Substrate-agnostic in, Terraform out. Spike scope (Phase 09): handles one L1 (l1-s3, IR type aws:s3:bucket). L2 thin-composition + relationships land in Phase 10. CLI: adapter.py """ import json import os import sys # IR type -> Terraform resource type. The only substrate-specific table. # As more L1s land, this grows; the L1 content + IR do not change. TYPE_MAP = { "aws:s3:bucket": "aws_s3_bucket", } def _tf_block(block_type, name, body_lines, indent=2): head = f'{block_type} "{name}" {{' body = "\n".join(f" {l}" for l in body_lines) return f"{head}\n{body}\n}}\n" def _emit_resource(resource): rtype = resource["type"] rid = resource["id"] tf_type = TYPE_MAP.get(rtype) if not tf_type: raise ValueError(f"unknown IR type {rtype!r} (adapter spike handles aws:s3:bucket only)") body = [] inputs = resource.get("inputs", {}) # S3 bucket: bucket_name -> bucket arg; region -> provider (handled separately) if "bucket_name" in inputs: body.append(f'bucket = "{inputs["bucket_name"]}"') # NFR: versioning (default true) nfrs = resource.get("nfrs", {}) versioning = nfrs.get("versioning", True) if isinstance(nfrs, dict) else True body.append("versioning {") body.append(f' enabled = {"true" if versioning else "false"}') body.append("}") return _tf_block("resource", f'aws_s3_bucket.{rid}', body) if False else _resource_block(rid, tf_type, body) def _resource_block(rid, tf_type, body): """Emit a top-level resource block.""" head = f'resource "{tf_type}" "{rid}" {{' body_str = "\n".join(f" {l}" for l in body) return f"{head}\n{body_str}\n}}\n" def _emit_output(output_name, value_expr): return f'output "{output_name}" {{\n value = {value_expr}\n}}\n' def adapt(ir_instance, out_dir): """Emit main.tf + terraform.tf + providers.tf to out_dir for the IR instance.""" os.makedirs(out_dir, exist_ok=True) stack = ir_instance["stack"] resources = ir_instance["resources"] # --- providers.tf: aws provider, region from the first resource's inputs.region --- region = "us-east-1" for r in resources: if "region" in r.get("inputs", {}): region = r["inputs"]["region"] break providers_tf = ( f'provider "aws" {{\n' f' region = "{region}"\n' f'}}\n' ) # --- terraform.tf: required_version + required_providers + S3 backend (no DynamoDB lock per D-P09-1) --- terraform_tf = ( 'terraform {\n' ' required_version = ">= 1.9, < 1.10"\n' ' required_providers {\n' ' aws = {\n' ' source = "hashicorp/aws"\n' ' version = "~> 5.0"\n' ' }\n' ' }\n' ' backend "s3" {\n' ' bucket = "acdl-tfstate-581513795199-us-east-1"\n' ' key = "spike/l1-s3/terraform.tfstate"\n' ' region = "us-east-1"\n' ' }\n' '}\n' ) # --- main.tf: resources + outputs --- main_tf_parts = [] for r in resources: main_tf_parts.append(_emit_resource(r)) rid = r["id"] outputs = r.get("outputs", {}) for out_name in outputs: if out_name == "bucket_arn": main_tf_parts.append(_emit_output("bucket_arn", f"aws_s3_bucket.{rid}.arn")) elif out_name == "bucket_name": main_tf_parts.append(_emit_output("bucket_name", f"aws_s3_bucket.{rid}.id")) main_tf = "\n".join(main_tf_parts) with open(os.path.join(out_dir, "main.tf"), "w") as fh: fh.write(main_tf) with open(os.path.join(out_dir, "terraform.tf"), "w") as fh: fh.write(terraform_tf) with open(os.path.join(out_dir, "providers.tf"), "w") as fh: fh.write(providers_tf) return out_dir if __name__ == "__main__": if len(sys.argv) != 3: print("usage: adapter.py ", file=sys.stderr) sys.exit(2) with open(sys.argv[1], "r") as fh: ir = json.load(fh) adapt(ir, sys.argv[2]) print(f"adapter: emitted terraform to {sys.argv[2]}", file=sys.stderr)