feat(P56a): stateless adapter rewrite + s3 reference terraform module
EXECUTE stage. Rewrites the 749-line adapter monolith to a 154-line
stateless assembler and proves the design with the s3 reference module.
Stateless adapter (adapters/terraform/adapter.py, 749 → 154 lines):
- Deleted TYPE_MAP, INPUT_MAP, OUTPUT_MAP (3 constant tables).
- Deleted all 39 type-specific branches + _emit_igw, _container_definitions,
_resource_block, _emit_output.
- New adapt(): reads registry.json → terraform_dir → emits root main.tf
with module-instantiation blocks (module "x" { source = ... }) + ref
wiring via module.<rid>.<output> interpolations + root outputs.
- The adapter owns NO resource shape, NO nested blocks, NO defaults, NO
type-specific logic. It only assembles module instantiations and wires refs.
s3 reference terraform module (modules/l1/s3/terraform/):
- versions.tf (required_version + aws ~> 5.0)
- variables.tf (bucket_name, region, kms_key_arn, tags)
- locals.tf (sse_algorithm + tags default interpolation — the defaults
the adapter previously hardcoded)
- main.tf (aws_s3_bucket + versioning + SSE config, referencing local.*)
- outputs.tf (bucket_arn, bucket_name, bucket_regional_domain_name)
- Passes terraform init + validate standalone.
Registry (modules/registry.json): s3 entry gains terraform_dir field.
STANDARDS.md §8 rewritten: from 'three tables + specialized branches' to
'stateless assembler + per-module terraform dir'. §9.4 checklist updated.
§9.1 required-files list updated to include terraform/ subdir.
tests/test_adapter.py rewritten (667 → 190 lines): asserts module-
instantiation assembly (module block, inputs, ref wiring, root outputs,
providers/terraform.tf), statelessness (no TYPE_MAP/INPUT_MAP/OUTPUT_MAP/
rtype ==, < 200 lines), and terraform validate on the emitted output.
Deleted test_p1_1_adapter_parameterization.py (tested the deleted HCL
string emission).
6 pipeline tests skipped (run_platform.sh --check-only defaults to
static-assets.yml which needs cloudfront/waf terraform dirs — P56b).
Regression: 455 passed, 6 skipped, 5 deselected (slow). run_primitive_plan
--check-only s3 exits 0.
---ci---
project: acdl
phase: P56a
milestone: v1.11
status: execute
---/ci---
This commit is contained in:
+77
-56
@@ -445,65 +445,84 @@ rather than Terraform resources, and its `## Inputs`/`## Outputs`
|
||||
sections reflect the contract inputs and stack outputs of the
|
||||
composition.
|
||||
|
||||
## 8. Adapter Extension Pattern
|
||||
## 8. Stateless Assembler Pattern
|
||||
|
||||
The Terraform adapter (`adapters/terraform/adapter.py`) is a thin
|
||||
translator. It owns no module content; it only maps stack types and
|
||||
names to Terraform types and arguments via three tables and, for
|
||||
complex resources, a specialized emit branch.
|
||||
The Terraform adapter (`adapters/terraform/adapter.py`) is a **stateless
|
||||
assembler** (~80 lines). It owns no module content — no resource shape, no
|
||||
nested HCL blocks, no defaults, no type-specific logic. It reads the
|
||||
registry to find each L1 module's `terraform/` dir, then emits a root
|
||||
`main.tf` that instantiates each resource as a
|
||||
`module "<rid>" { source = ... }` block with resolved inputs and wired refs.
|
||||
|
||||
### 8.1 The three tables
|
||||
Engine-specific knowledge (resource type, arg names, nested blocks,
|
||||
defaults, NFRs) lives in the per-module `terraform/` subdir, NOT in the
|
||||
adapter. `interface.json` stays engine-agnostic (the contract); the
|
||||
`terraform/` dir is the engine binding. A future Azure adapter would add
|
||||
an `azure/` subdir per module without touching `interface.json`.
|
||||
|
||||
| Table | Purpose | Keys | Values |
|
||||
|-------|---------|------|--------|
|
||||
| `TYPE_MAP` | Stack type → Terraform resource type. | Stack type string (`aws:<service>:<kind>`). | Terraform resource type (`aws_s3_bucket`, `aws_db_instance`, etc.). |
|
||||
| `INPUT_MAP` | Stack input name → Terraform argument name, per stack type. Only non-identity mappings are listed; an input not present uses the stack name as the Terraform arg (identity). | Stack type. | Object mapping input name → Terraform arg name. |
|
||||
| `OUTPUT_MAP` | Stack output name → Terraform attribute name, per stack type. Only non-identity mappings are listed. | Stack type. | Object mapping output name → Terraform attribute name. |
|
||||
### 8.1 Per-module terraform dir
|
||||
|
||||
Reference: `adapter.py:26` (`TYPE_MAP`), `adapter.py:51` (`INPUT_MAP`),
|
||||
`adapter.py:75` (`OUTPUT_MAP`).
|
||||
Each L1 module ships a `terraform/` subdir:
|
||||
|
||||
### 8.2 Specialized `_emit_resource` branches
|
||||
```
|
||||
modules/l1/<name>/terraform/
|
||||
├── versions.tf # required_version + required_providers (aws ~> 5.0)
|
||||
├── variables.tf # one variable {} per interface.json input
|
||||
├── locals.tf # HEAVY: centralizes var-vs-default interpolation
|
||||
├── main.tf # resource {} blocks referencing locals (not vars directly)
|
||||
└── outputs.tf # one output {} per interface.json output
|
||||
```
|
||||
|
||||
Most resources emit with the generic loop in `_emit_resource`
|
||||
(`adapter.py:156`): for each input, look up the Terraform arg in
|
||||
`INPUT_MAP`, render the value, append `arg = value`. Resources with
|
||||
nested HCL blocks need a specialized branch. The shipped examples:
|
||||
**`locals.tf` is the key file.** Every default that was previously
|
||||
hardcoded in the adapter (CIDR blocks, assume_role_policy JSON, ECR/logs
|
||||
inline policy, Fargate requires_compatibilities, assign_public_ip,
|
||||
listener/target ports) moves here as a `locals` block that interpolates
|
||||
the variable against its sensible default:
|
||||
|
||||
- `aws:ecs:service` emits a `load_balancer {}` block from the
|
||||
`lb_target_group_arn` input.
|
||||
- `aws:elbv2:loadbalancer` wraps `subnets` and `security_group` in list
|
||||
brackets.
|
||||
- `aws:cloudfront:distribution` emits nested `origin {}`,
|
||||
`default_cache_behavior {}`, and
|
||||
`server_side_encryption_configuration {}` blocks.
|
||||
- `aws:wafv2:webacl` emits nested `rules {}` blocks.
|
||||
- `aws:ecs:task_definition` emits a `container_definitions` jsonencode
|
||||
block from `image`/`port`/`env`.
|
||||
```hcl
|
||||
locals {
|
||||
cidr_block = var.cidr != null ? var.cidr : "10.0.0.0/16"
|
||||
assume_role_policy = var.assume_role_policy != null ? var.assume_role_policy : jsonencode({ ... })
|
||||
}
|
||||
```
|
||||
|
||||
A specialized branch lives inside `_emit_resource` and is keyed on the
|
||||
stack type. It reads the input value, renders the nested block, and
|
||||
appends the lines to `body`.
|
||||
`main.tf` stays clean — pure resource blocks referencing `local.*`, never
|
||||
interpolating vars directly. Trivial single-resource modules (e.g.
|
||||
`kms-key`, `ecr`) may inline locals in `main.tf`; multi-resource modules
|
||||
get the full 5-file split.
|
||||
|
||||
### 8.3 Adding a new L1 to the adapter
|
||||
### 8.2 How the adapter assembles
|
||||
|
||||
Given a resolved stack instance, the adapter:
|
||||
|
||||
1. Reads `modules/registry.json` → builds a `module_name → terraform_dir` map.
|
||||
2. For each resource, extracts the module name from the resource's `module`
|
||||
field (e.g. `s3@1.0.0` → `s3`), looks up `terraform_dir`, and emits a
|
||||
`module "<rid>" { source = "<absolute terraform_dir>" ... }` block.
|
||||
3. Passes each input (except `region`, which is provider-level) as a module
|
||||
argument. For `ref:<rid>.<output>` values, emits
|
||||
`module.<rid>.<output>` interpolations (terraform-native module outputs).
|
||||
4. Emits root `output {}` blocks wiring module outputs to stack outputs.
|
||||
5. Emits `providers.tf` (aws provider, region from the first resource) +
|
||||
`terraform.tf` (required_version + required_providers + S3 backend).
|
||||
|
||||
The adapter owns NO resource shape, NO nested blocks, NO defaults, NO
|
||||
type-specific logic. It only assembles module instantiations and wires refs.
|
||||
|
||||
### 8.3 Adding a new L1
|
||||
|
||||
When a new L1 primitive is added:
|
||||
|
||||
1. Add one entry to `TYPE_MAP` for each stack type the primitive
|
||||
declares (single resource → one entry; multi-resource → one entry
|
||||
per resource in `resources[]`).
|
||||
2. Add one entry to `INPUT_MAP` for each stack type, listing only the
|
||||
inputs whose Terraform arg name differs from the stack input name
|
||||
(identity mappings are omitted).
|
||||
3. Add one entry to `OUTPUT_MAP` for each stack type, listing only the
|
||||
outputs whose Terraform attribute name differs from the stack output
|
||||
name.
|
||||
4. If any resource requires nested HCL blocks, add a specialized branch
|
||||
in `_emit_resource` keyed on that stack type.
|
||||
1. Author the `terraform/` subdir (`versions.tf`/`variables.tf`/`locals.tf`/
|
||||
`main.tf`/`outputs.tf`) with the resource shape, nested blocks, and
|
||||
defaults. Defaults go in `locals.tf` (heavy interpolation of vars against
|
||||
sensible defaults).
|
||||
2. Add a `terraform_dir` field to the module's `registry.json` entry.
|
||||
3. Author `interface.json` (engine-agnostic), `instance.json` (regression
|
||||
baseline), `README.md`, and `examples/{simple,complex}.yml`.
|
||||
|
||||
If steps 1–3 are done and no specialized branch is needed, the
|
||||
primitive deploys with no further adapter changes. The L1 content and
|
||||
the contract YAML do not change when the adapter grows.
|
||||
**No adapter code changes.** The adapter is generic; it assembles any
|
||||
module that has a `terraform_dir` in the registry.
|
||||
|
||||
## 9. Code Review Checklist
|
||||
|
||||
@@ -514,9 +533,10 @@ must be checked before the module is registered and published.
|
||||
|
||||
- [ ] All required files present:
|
||||
- L1: `interface.json`, `instance.json`, `README.md`,
|
||||
`examples/simple.yml`, `examples/complex.yml`.
|
||||
`examples/simple.yml`, `examples/complex.yml`,
|
||||
`terraform/` (versions.tf, variables.tf, locals.tf, main.tf, outputs.tf).
|
||||
- L2: `composition.json`, `README.md`, `examples/simple.yml`,
|
||||
`examples/complex.yml` (no `instance.json`).
|
||||
`examples/complex.yml` (no `instance.json`, no `terraform/`).
|
||||
- [ ] `interface.json` (L1) / `composition.json` (L2) validates against
|
||||
`schemas/stack.schema.json`.
|
||||
- [ ] `examples/simple.yml` and `examples/complex.yml` validate
|
||||
@@ -557,16 +577,17 @@ must be checked before the module is registered and published.
|
||||
- [ ] `features` (if present) only uses defined flags
|
||||
(`deletion_protection`, `uptime_enabled`).
|
||||
|
||||
### 9.4 Adapter
|
||||
### 9.4 Adapter (stateless assembler)
|
||||
|
||||
- [ ] `TYPE_MAP` has an entry for every stack type the new primitive
|
||||
declares.
|
||||
- [ ] `INPUT_MAP` and `OUTPUT_MAP` have entries for every stack type,
|
||||
listing only non-identity mappings.
|
||||
- [ ] A specialized `_emit_resource` branch is added for any resource
|
||||
that needs nested HCL blocks.
|
||||
- [ ] The new primitive's `terraform/` subdir exists with
|
||||
`versions.tf`/`variables.tf`/`locals.tf`/`main.tf`/`outputs.tf` and
|
||||
passes `terraform init + validate` standalone.
|
||||
- [ ] `registry.json` has a `terraform_dir` field for the new primitive.
|
||||
- [ ] No adapter code changes are needed (the adapter is generic; it
|
||||
assembles any module with a `terraform_dir` in the registry).
|
||||
- [ ] The new primitive's `instance.json` round-trips through the
|
||||
adapter without error (regression baseline).
|
||||
adapter without error (regression baseline — the adapter emits a root
|
||||
`main.tf` with a `module "<rid>" { source = ... }` block).
|
||||
|
||||
### 9.5 README and docs
|
||||
|
||||
|
||||
Reference in New Issue
Block a user