phase: 9, status: plan-as-execute, persona: platform-engineer, task: T-9.5..T-9.7+T-9.9
---ci---
project: acdl
phase: 9
milestone: v1.1
status: plan-as-execute
persona: platform-engineer
task: [T-9.5, T-9.6, T-9.7, T-9.9]
requirements.covered: [REQ-26]
---/ci---
Waves 2+3: Terraform adapter + generated spike TF + run script.
- T-9.5: adapters/terraform/adapter.py - compiles an IR instance to a
Terraform root module. TYPE_MAP {aws:s3:bucket -> aws_s3_bucket}. Thin
layer; does not own L1 content. Emits main.tf (resource + outputs) +
terraform.tf (required_version/providers + S3 backend, NO
dynamodb_table per D-P09-1) + providers.tf (aws provider region from
the IR). CLI: adapter.py <ir_instance.json> <out_dir>.
- T-9.6: terraform/spike/{main.tf,terraform.tf,providers.tf} - generated
by running the adapter against modules-ir/l1/l1-s3/spike_instance.json.
Committed so verify_phase09.sh can validate/plan without regenerating
(D-P09-4); the verify script will regenerate + diff to prove
reproducibility.
- T-9.7: scripts/run_spike_plan.sh - loads rotated spike key from
gitignored .env.secrets, exports AWS env vars, cd terraform/spike,
terraform init -lock=false, terraform validate, terraform plan
-lock=false -out=tfplan. Plan-only; no apply.
- T-9.9: .gitignore - add terraform/spike/.terraform/ + tfplan +
*.tfstate*.
EXECUTE: ran scripts/run_spike_plan.sh against real AWS via the rotated
spike key (D-039). terraform plan succeeded: 1 to add (the S3 bucket),
outputs computed. One non-blocking deprecation warning (aws_s3_bucket
versioning block -> use aws_s3_bucket_versioning in v1.2). No long-lived
credential in the workflow (key loaded from .env.secrets at runtime).
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""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 <ir_instance.json> <out_dir>
|
||||
"""
|
||||
|
||||
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 <ir_instance.json> <out_dir>", 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)
|
||||
Reference in New Issue
Block a user