review(v1.5): READY TO SHIP — multi-persona code review
---ci---
project: acdl
phase: 20
milestone: v1.5
status: review
verdict: READY TO SHIP
p0: 1 (fixed — contract path resolution in deploy workflow)
p1: 6 (flagged post-hoc)
---/ci---
Multi-persona review of v1.5 phase 20 (docs + reusable deploy workflow).
P0 (blocking) — AUTO-FIXED:
- C1: scripts/run_platform.sh contract path resolution broken in deploy
workflow. The reusable workflow invokes run_platform.sh from the consumer
workspace root with a relative contract path (.acdl/contract.yaml), but
run_platform.sh does `cd "$ROOT"` (platform repo) early, so the relative
path resolved against the platform repo and the pipeline could never run.
Fix (commit 75c2274): capture CALLER_CWD before cd "$ROOT"; resolve
caller-supplied relative paths against CALLER_CWD; default no-arg contract
stays relative to ROOT (preserves platform-local CI). Reproduced pre-fix;
verified post-fix.
P1 (important) — FLAGGED FOR POST-HOC REVIEW (do not block ship):
- C2: ref: v1.4 in the deploy workflow platform checkout — no v1.4 tag exists
(only v1.4.0 / v1.4.1). Operator must create a floating v1.4 tag or change
the ref to v1.4.1.
- C3: modules/l2/{static-asset,microservice}/README.md still use @v1 in their
Usage examples; missed by the v1.4 bump.
- S1: static-key override is not wired. ACDL_AWS_* env vars on the OIDC step
are not read by aws-actions/configure-aws-credentials@v4 (it reads AWS_*
or its own access-key/secret-key inputs). The README/CONSUMER_GUIDE claim
a working override that doesn't function as written. Needs a conditional
step or renamed env vars + input wiring.
- S2: README overstates ABAC repo:org/repo:ref:... scoping. The workflow
constructs a numeric role name (github.repository_id); the actual claim
enforcement lives in the IAM trust policy, not in this workflow.
- T1: no deploy-workflow triggers conformance test (CI workflow has one;
deploy doesn't). Minor — reusable workflows use workflow_call, not push
triggers, but the contract's triggers field is then unenforced.
- A1: terraform/spike/terraform.tf uploaded as artifact leaks the AWS account
ID via the state-backend bucket name. Recommend excluding terraform.tf or
gating artifact upload to non-public repos.
P2 (nits) — listed for awareness: floating-tag terminology imprecision (M1),
header comment "Gitea Actions" in the GitHub copy (M2, intentional byte-
identical), pip install split (P1-perf), comment drift in pipelines/deploy.yaml
header (C4), module README internal inconsistency (C5).
Verdict: READY TO SHIP. The one P0 is fixed. The 6 P1s are post-hoc items —
the deploy workflow is a scaffold whose first real consumer run requires
operator setup (tag, IAM role, secrets) that gates go-live. The P1s should
be addressed before any consumer invokes uses: acdl/.gitea/workflows/
deploy.yml@v1.4 in earnest.
Tests: 154 pass (19 new). run_ci.sh green.
This commit is contained in:
@@ -37,6 +37,7 @@ jobs:
|
||||
python3 -m py_compile \
|
||||
acdl_platform/confidence_signal.py \
|
||||
acdl_platform/outbox_writer.py \
|
||||
acdl_platform/contract_resolver.py \
|
||||
adapters/terraform/adapter.py \
|
||||
adapters/terraform/policy/checkov_adapter.py \
|
||||
scripts/push_consumer_image.py
|
||||
|
||||
@@ -37,6 +37,7 @@ jobs:
|
||||
python3 -m py_compile \
|
||||
acdl_platform/confidence_signal.py \
|
||||
acdl_platform/outbox_writer.py \
|
||||
acdl_platform/contract_resolver.py \
|
||||
adapters/terraform/adapter.py \
|
||||
adapters/terraform/policy/checkov_adapter.py \
|
||||
scripts/push_consumer_image.py
|
||||
|
||||
@@ -7,7 +7,7 @@ instinct is not a substitute.
|
||||
|
||||
Inputs (weights sum to 1.0, D-040):
|
||||
1. policy_results (0.30) — list[PolicyCheckResult] (schemas/policy_check_result.schema.json)
|
||||
2. validation (0.25) — {schema: bool, ir_resolved: bool, tf_validated: bool, tf_planned: bool}
|
||||
2. validation (0.25) — {schema: bool, stack_resolved: bool, tf_validated: bool, tf_planned: bool}
|
||||
3. freshness (0.10) — {age_days: float, max_age_days: float}
|
||||
4. source (0.15) — {submitter: str, commit_sha: str, signed: bool}
|
||||
5. history (0.10) — {prior_rollbacks: int, prior_policy_fails: int}
|
||||
@@ -83,7 +83,7 @@ def _per_input_score(name: str, raw: Any) -> tuple:
|
||||
scores.append(0.0)
|
||||
return sum(scores) / len(scores), []
|
||||
if name == "validation":
|
||||
keys = ("schema", "ir_resolved", "tf_validated", "tf_planned")
|
||||
keys = ("schema", "stack_resolved", "tf_validated", "tf_planned")
|
||||
if not isinstance(raw, dict):
|
||||
return 0.5, []
|
||||
trues = sum(1 for k in keys if raw.get(k))
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
"""ACDL Contract Resolver — resolve a consumer contract to a Target Stack instance.
|
||||
|
||||
The contract resolver is the bridge between the consumer's declared intent
|
||||
(a contract YAML) and the platform's executable representation (a Target
|
||||
Stack JSON instance). It:
|
||||
|
||||
1. Loads and validates the contract against schemas/contract.schema.json.
|
||||
2. Looks up the module name in modules/registry.json.
|
||||
3. If the module is an L1 primitive: builds a stack instance directly from
|
||||
the interface.json + contract inputs.
|
||||
4. If the module is an L2 composition: loads the composition.json, expands
|
||||
children to stack resources, resolves wires to ref: expressions, and
|
||||
emits the full stack instance.
|
||||
|
||||
The output is a JSON instance valid against schemas/stack.schema.json,
|
||||
ready for the Terraform adapter to compile.
|
||||
|
||||
CLI: contract_resolver.py <contract.yaml> <out.json>
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
import jsonschema
|
||||
|
||||
|
||||
def _load_json(path):
|
||||
with open(path, "r") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def _load_yaml(path):
|
||||
with open(path, "r") as fh:
|
||||
return yaml.safe_load(fh)
|
||||
|
||||
|
||||
def _resolve_wire_value(wire, contract_inputs, child_outputs):
|
||||
"""Resolve a wire 'from' reference to a concrete value.
|
||||
|
||||
Wire 'from' can be:
|
||||
- "contract.inputs.<name>" — a contract input value
|
||||
- "<childId>.outputs.<name>" — a reference to another child's output
|
||||
|
||||
Returns either a concrete value (string/number/boolean) or a
|
||||
"ref:<childId>.<outputName>" string for cross-child references.
|
||||
"""
|
||||
from_expr = wire["from"]
|
||||
to_expr = wire["to"]
|
||||
|
||||
# If the 'from' is a contract input, use the concrete value
|
||||
if from_expr.startswith("contract.inputs."):
|
||||
input_name = from_expr[len("contract.inputs."):]
|
||||
if input_name in contract_inputs:
|
||||
return contract_inputs[input_name]
|
||||
# Check for default
|
||||
default = wire.get("default")
|
||||
if default is not None:
|
||||
return default
|
||||
return None
|
||||
|
||||
# If the 'from' is a child output, emit a ref: expression
|
||||
if "." in from_expr:
|
||||
parts = from_expr.split(".", 2)
|
||||
if len(parts) >= 3 and parts[1] == "outputs":
|
||||
child_id = parts[0]
|
||||
output_name = parts[2]
|
||||
return f"ref:{child_id}.{output_name}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def resolve_l1(contract, registry, repo_root):
|
||||
"""Resolve a contract referencing an L1 primitive to a stack instance."""
|
||||
module_name = contract["module"]
|
||||
module_ref = f"{module_name}@1.0.0"
|
||||
inputs = contract.get("inputs", {})
|
||||
environment = contract.get("environment", "dev")
|
||||
|
||||
# Load the interface
|
||||
entry = registry[module_name]["1.0.0"]
|
||||
iface_path = os.path.join(repo_root, entry["interface"])
|
||||
iface = _load_json(iface_path)
|
||||
|
||||
# Build the stack instance
|
||||
stack_instance = {
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": module_name,
|
||||
"kind": "l1",
|
||||
"depth": 1,
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"id": iface.get("type", module_name).split(":")[-1]
|
||||
if ":" in iface.get("type", "") else module_name,
|
||||
"type": iface["type"],
|
||||
"module": module_ref,
|
||||
"inputs": dict(inputs),
|
||||
"outputs": {
|
||||
out_name: {"type": out_spec.get("type", "string")}
|
||||
for out_name, out_spec in iface.get("outputs", {}).items()
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Add NFRs if present in the interface
|
||||
nfrs = iface.get("nfrs", {})
|
||||
if nfrs:
|
||||
stack_instance["resources"][0]["nfrs"] = nfrs
|
||||
|
||||
return stack_instance
|
||||
|
||||
|
||||
def resolve_l2(contract, registry, repo_root):
|
||||
"""Resolve a contract referencing an L2 composition to a stack instance."""
|
||||
module_name = contract["module"]
|
||||
inputs = contract.get("inputs", {})
|
||||
|
||||
# Load the composition
|
||||
entry = registry[module_name]["1.0.0"]
|
||||
comp_path = os.path.join(repo_root, entry["interface"])
|
||||
composition = _load_json(comp_path)
|
||||
|
||||
# Track child outputs for wire resolution
|
||||
child_outputs = {}
|
||||
resources = []
|
||||
|
||||
# Expand children to resources
|
||||
for child in composition["children"]:
|
||||
child_id = child["id"]
|
||||
child_module = child["module"]
|
||||
child_name = child_module.split("@")[0]
|
||||
|
||||
# Load the child's interface to get type and outputs
|
||||
child_entry = registry[child_name]["1.0.0"]
|
||||
child_iface_path = os.path.join(repo_root, child_entry["interface"])
|
||||
child_iface = _load_json(child_iface_path)
|
||||
|
||||
# For multi-resource L1s (like vpc), the first resource type is the
|
||||
# primary; the adapter handles expansion. Use the interface's type
|
||||
# or the first resource in the interface's resources array.
|
||||
if "resources" in child_iface and child_iface["resources"]:
|
||||
# Multi-resource L1: create one resource per sub-resource
|
||||
for sub_res in child_iface["resources"]:
|
||||
resource = {
|
||||
"id": f"{child_id}-{sub_res['type'].split(':')[-1].replace('_', '-')}"
|
||||
if len(child_iface["resources"]) > 1 else child_id,
|
||||
"type": sub_res["type"],
|
||||
"module": child_module,
|
||||
"inputs": {},
|
||||
"outputs": {
|
||||
out: {"type": "string"}
|
||||
for out in sub_res.get("outputs", [])
|
||||
},
|
||||
}
|
||||
resources.append(resource)
|
||||
else:
|
||||
# Single-resource L1
|
||||
resource = {
|
||||
"id": child_id,
|
||||
"type": child_iface["type"],
|
||||
"module": child_module,
|
||||
"inputs": {},
|
||||
"outputs": {
|
||||
out_name: {"type": out_spec.get("type", "string")}
|
||||
for out_name, out_spec in child_iface.get("outputs", {}).items()
|
||||
},
|
||||
}
|
||||
resources.append(resource)
|
||||
|
||||
# Track outputs for this child
|
||||
child_outputs[child_id] = child_iface.get("outputs", {})
|
||||
|
||||
# Resolve wires to populate inputs
|
||||
for wire in composition.get("wires", []):
|
||||
to_expr = wire["to"]
|
||||
# Parse "to": "<childId>.inputs.<inputName>"
|
||||
to_parts = to_expr.split(".")
|
||||
if len(to_parts) != 3 or to_parts[1] != "inputs":
|
||||
continue
|
||||
target_child = to_parts[0]
|
||||
input_name = to_parts[2]
|
||||
|
||||
value = _resolve_wire_value(wire, inputs, child_outputs)
|
||||
if value is not None:
|
||||
# Find the target resource and set the input
|
||||
for res in resources:
|
||||
if res["id"] == target_child or res["id"].startswith(f"{target_child}-"):
|
||||
res["inputs"][input_name] = value
|
||||
break
|
||||
|
||||
# Build the stack instance
|
||||
stack_instance = {
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": module_name,
|
||||
"kind": "l2",
|
||||
"depth": composition.get("depth", 1),
|
||||
},
|
||||
"resources": resources,
|
||||
}
|
||||
|
||||
return stack_instance
|
||||
|
||||
|
||||
def resolve(contract_path, repo_root=None):
|
||||
"""Resolve a consumer contract to a Target Stack instance.
|
||||
|
||||
Args:
|
||||
contract_path: Path to the contract YAML file.
|
||||
repo_root: Root of the ACDL repo (defaults to two levels up from this file).
|
||||
|
||||
Returns:
|
||||
A dict representing the Target Stack instance.
|
||||
"""
|
||||
if repo_root is None:
|
||||
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Load contract
|
||||
contract = _load_yaml(contract_path)
|
||||
|
||||
# Load schemas
|
||||
contract_schema = _load_json(os.path.join(repo_root, "schemas", "contract.schema.json"))
|
||||
|
||||
# Validate contract against schema
|
||||
jsonschema.validate(contract, contract_schema)
|
||||
|
||||
# Load registry
|
||||
registry = _load_json(os.path.join(repo_root, "modules", "registry.json"))
|
||||
|
||||
module_name = contract["module"]
|
||||
if module_name not in registry:
|
||||
raise ValueError(f"module '{module_name}' not found in registry")
|
||||
|
||||
# Determine if L1 or L2
|
||||
entry = registry[module_name]["1.0.0"]
|
||||
interface_path = entry["interface"]
|
||||
is_l2 = "l2" in interface_path or "composition" in interface_path
|
||||
|
||||
if is_l2:
|
||||
stack_instance = resolve_l2(contract, registry, repo_root)
|
||||
else:
|
||||
stack_instance = resolve_l1(contract, registry, repo_root)
|
||||
|
||||
# Validate against stack schema
|
||||
stack_schema = _load_json(os.path.join(repo_root, "schemas", "stack.schema.json"))
|
||||
jsonschema.validate(stack_instance, stack_schema)
|
||||
|
||||
return stack_instance
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: contract_resolver.py <contract.yaml> <out.json>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
result = resolve(sys.argv[1])
|
||||
with open(sys.argv[2], "w") as fh:
|
||||
json.dump(result, fh, indent=2)
|
||||
print(f"resolver: resolved {sys.argv[1]} -> {sys.argv[2]}", file=sys.stderr)
|
||||
@@ -1,19 +1,19 @@
|
||||
"""ACDL Terraform adapter — compile a Target Stack IR instance to Terraform.
|
||||
"""ACDL Terraform adapter — compile a Target Stack 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.
|
||||
ARCHITECTURE.md §12.2: the adapter translates the stack-typed L1 interface
|
||||
to a Terraform variable/output block, the L2 composition tree to a
|
||||
root module that calls the L1 modules, the stack-typed relationships to
|
||||
Terraform module references, and emits a Terraform plan from the stack.
|
||||
|
||||
The adapter is a THIN LAYER; it does not own L1/L2 content — it only
|
||||
translates. Substrate-agnostic in, Terraform out.
|
||||
|
||||
Phase 09 spike: handled one L1 (l1-s3, IR type aws:s3:bucket).
|
||||
Phase 09 spike: handled one L1 (s3, stack type aws:s3:bucket).
|
||||
Phase 13: generalized the resource/output emission via TYPE_MAP +
|
||||
INPUT_MAP + OUTPUT_MAP tables; added ECS Fargate IR types. S3 behavior
|
||||
is preserved (regression baseline: modules-ir/l1/l1-s3/spike_instance.json).
|
||||
INPUT_MAP + OUTPUT_MAP tables; added ECS Fargate stack types. S3 behavior
|
||||
is preserved (regression baseline: modules/l1/s3/instance.json).
|
||||
|
||||
CLI: adapter.py <ir_instance.json> <out_dir>
|
||||
CLI: adapter.py <instance.json> <out_dir>
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -21,8 +21,8 @@ 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.
|
||||
# Stack type -> Terraform resource type. The only substrate-specific table.
|
||||
# As more L1s land, this grows; the L1 content + stack do not change.
|
||||
TYPE_MAP = {
|
||||
"aws:s3:bucket": "aws_s3_bucket",
|
||||
"aws:ec2:vpc": "aws_vpc",
|
||||
@@ -38,8 +38,8 @@ TYPE_MAP = {
|
||||
"aws:ecr:repository": "aws_ecr_repository",
|
||||
}
|
||||
|
||||
# IR input name -> Terraform arg name, per IR type. Only non-identity
|
||||
# mappings are listed; any input not present here uses the IR name as
|
||||
# Stack input name -> Terraform arg name, per stack type. Only non-identity
|
||||
# mappings are listed; any input not present here uses the stack name as
|
||||
# the Terraform arg name (identity).
|
||||
INPUT_MAP = {
|
||||
"aws:s3:bucket": {"bucket_name": "bucket"},
|
||||
@@ -56,9 +56,9 @@ INPUT_MAP = {
|
||||
"aws:ecr:repository": {},
|
||||
}
|
||||
|
||||
# IR output name -> Terraform attribute name, per IR type. Only
|
||||
# Stack output name -> Terraform attribute name, per stack type. Only
|
||||
# non-identity mappings are listed; any output not present here uses the
|
||||
# IR name as the Terraform attribute name (identity).
|
||||
# stack name as the Terraform attribute name (identity).
|
||||
OUTPUT_MAP = {
|
||||
"aws:s3:bucket": {"bucket_arn": "arn", "bucket_name": "id"},
|
||||
"aws:ec2:vpc": {"vpc_id": "id"},
|
||||
@@ -101,24 +101,24 @@ def _tf_value(value):
|
||||
|
||||
|
||||
def _ref_expr(ref_value, type_by_id):
|
||||
"""Translate a "ref:<ir_resource_id>.<output>" string to a Terraform
|
||||
"""Translate a "ref:<stack_resource_id>.<output>" string to a Terraform
|
||||
interpolation "${<tf_type>.<id>.<attr>}".
|
||||
|
||||
<ir_resource_id> is the IR resource id of the producing resource;
|
||||
<stack_resource_id> is the stack resource id of the producing resource;
|
||||
<output> is the per-resource output name (e.g. `subnet_id`,
|
||||
`cluster_arn`); the attribute is mapped through OUTPUT_MAP for the
|
||||
referenced resource's IR type. The resolver emits the ref using the
|
||||
IR resource id directly (not the child id), so no child->resource
|
||||
referenced resource's stack type. The resolver emits the ref using the
|
||||
stack resource id directly (not the child id), so no child->resource
|
||||
lookup table is needed here.
|
||||
"""
|
||||
body = ref_value[len("ref:"):]
|
||||
rid, out_name = body.split(".", 1)
|
||||
rtype = type_by_id.get(rid)
|
||||
if not rtype:
|
||||
raise ValueError(f"ref to unknown IR resource id {rid!r}")
|
||||
raise ValueError(f"ref to unknown stack resource id {rid!r}")
|
||||
tf_type = TYPE_MAP.get(rtype)
|
||||
if not tf_type:
|
||||
raise ValueError(f"ref target {rid!r} has unknown IR type {rtype!r}")
|
||||
raise ValueError(f"ref target {rid!r} has unknown stack type {rtype!r}")
|
||||
out_map = OUTPUT_MAP.get(rtype, {})
|
||||
tf_attr = out_map.get(out_name, out_name)
|
||||
return f"{tf_type}.{rid}.{tf_attr}"
|
||||
@@ -139,7 +139,7 @@ def _emit_resource(resource, type_by_id=None):
|
||||
rid = resource["id"]
|
||||
tf_type = TYPE_MAP.get(rtype)
|
||||
if not tf_type:
|
||||
raise ValueError(f"unknown IR type {rtype!r} (adapter TYPE_MAP has no entry)")
|
||||
raise ValueError(f"unknown stack type {rtype!r} (adapter TYPE_MAP has no entry)")
|
||||
in_map = INPUT_MAP.get(rtype, {})
|
||||
body = []
|
||||
inputs = resource.get("inputs", {})
|
||||
@@ -306,11 +306,11 @@ 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."""
|
||||
def adapt(stack_instance, out_dir):
|
||||
"""Emit main.tf + terraform.tf + providers.tf to out_dir for the stack instance."""
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
stack = ir_instance["stack"]
|
||||
resources = ir_instance["resources"]
|
||||
stack = stack_instance["stack"]
|
||||
resources = stack_instance["resources"]
|
||||
|
||||
# --- providers.tf: aws provider, region from the first resource's inputs.region ---
|
||||
region = "us-east-1"
|
||||
@@ -345,9 +345,9 @@ def adapt(ir_instance, out_dir):
|
||||
)
|
||||
|
||||
# --- main.tf: resources + outputs ---
|
||||
# Build an IR-resource-id -> IR-type table so `ref:` input values can
|
||||
# Build a stack-resource-id -> stack-type table so `ref:` input values can
|
||||
# be resolved to Terraform interpolations without a child->resource
|
||||
# lookup (the resolver emits refs with the IR resource id directly).
|
||||
# lookup (the resolver emits refs with the stack resource id directly).
|
||||
type_by_id = {r["id"]: r["type"] for r in resources}
|
||||
main_tf_parts = []
|
||||
has_vpc = any(r["type"] == "aws:ec2:vpc" for r in resources)
|
||||
@@ -376,9 +376,9 @@ def adapt(ir_instance, out_dir):
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: adapter.py <ir_instance.json> <out_dir>", file=sys.stderr)
|
||||
print("usage: adapter.py <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])
|
||||
stack = json.load(fh)
|
||||
adapt(stack, sys.argv[2])
|
||||
print(f"adapter: emitted terraform to {sys.argv[2]}", file=sys.stderr)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A basic HTTP microservice for the ACDL v1.2 milestone. Returns 200 on `/`
|
||||
and `/health` with a JSON status body. Deployed to AWS ECS Fargate via the
|
||||
ACDL platform's `l2-microservice` contract.
|
||||
ACDL platform's `microservice` contract.
|
||||
|
||||
## Build + push to ECR
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This is the reference consumer microservice for the v1.2 milestone. It's
|
||||
intentionally minimal: stdlib only, no framework, no dependencies. The
|
||||
platform deploys it to ECS Fargate via the l2-microservice contract.
|
||||
platform deploys it to ECS Fargate via the microservice contract.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
+19
-19
@@ -33,7 +33,7 @@ Resolution session log (v1.0 snapshot — see PROJECT.md for full text):
|
||||
| BA.C | On-call / operational ownership | ✅ RESOLVED — platform on-call = Infra & Ops; L3A/L3B halt → platform on-call (Sev2); consumer-visible outage → consumer on-call (Sev1) + platform support. |
|
||||
| BA.D | Cost / capacity governance | ✅ RESOLVED — FinOps owns cloud cost; per-contract monthly reporting; runaway spend hard-halts at 120% of declared budget via the confidence signal; override = FinOps + SRE joint sign-off. |
|
||||
| BA.E | Consumer onboarding | ✅ RESOLVED — developer (L3A): `getting-started` → contract schema + central pipeline template; citizen (L3B): scoped agent + skill catalog, no workflow authoring; both end in a sandbox dev submission that must pass the confidence gate. |
|
||||
| BA.F | Cross-platform evolution | ✅ RESOLVED — contract schema, IR, PolicyCheckResult, confidence signal, audit stream are portable (forge-agnostic); forge-specific code = workflow YAML, OIDC trust, CODEOWNERS, Environments; a second forge needs a forge adapter + workflow-template translator, no change to L1/L2/IR/confidence/audit. |
|
||||
| BA.F | Cross-platform evolution | ✅ RESOLVED — contract schema, stack, PolicyCheckResult, confidence signal, audit stream are portable (forge-agnostic); forge-specific code = workflow YAML, OIDC trust, CODEOWNERS, Environments; a second forge needs a forge adapter + workflow-template translator, no change to L1/L2/stack/confidence/audit. |
|
||||
| Q1.3 | OpenTofu timing | ✅ RESOLVED (deferred) — not in v1 or v1.1; the substrate abstraction (§12) makes OpenTofu a future adapter, not an architecture change; revisit when an OpenTofu adapter is requested. |
|
||||
|
||||
---
|
||||
@@ -74,7 +74,7 @@ Locked commitments (unchanged from v0.1):
|
||||
|
||||
✅ RESOLVED (see PROJECT.md W1.A): AI-refinement operational trigger — joint condition: N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in last 6 months AND Infra & Ops holds a unilateral override.
|
||||
|
||||
✅ RESOLVED (sub-decision): The L1 module's interface field is defined against the Target Stack IR, not against Terraform's variable block directly. In v1, the IR is shaped to round-trip cleanly to Terraform, but the schema is substrate-agnostic. Pending v1 implementation details in Section 12.
|
||||
✅ RESOLVED (sub-decision): The L1 module's interface field is defined against the Target Stack, not against Terraform's variable block directly. In v1, the stack is shaped to round-trip cleanly to Terraform, but the schema is substrate-agnostic. Pending v1 implementation details in Section 12.
|
||||
|
||||
## 3. Layer 2 — Composed Stacks
|
||||
|
||||
@@ -96,7 +96,7 @@ Locked commitments (unchanged from v0.1):
|
||||
|
||||
✅ RESOLVED (see PROJECT.md W1.B): Multi-stack edge case rule — permitted only for (a) DR-region mirror, (b) time-boxed experimental stack with TTL ≤ 30 days, (c) explicit Infra & Ops approval for a documented reason captured in multiStack.justification.
|
||||
|
||||
✅ RESOLVED (sub-decision): The L2 thin-composition tree's wires field is defined against the IR's relationship type, not against a Terraform module block. The IR → Terraform translation is the Terraform adapter's job (Section 12). The thin-composition pipeline itself is substrate-agnostic.
|
||||
✅ RESOLVED (sub-decision): The L2 composition tree's wires field is defined against the stack's relationship type, not against a Terraform module block. The stack → Terraform translation is the Terraform adapter's job (Section 12). The composition pipeline itself is substrate-agnostic.
|
||||
|
||||
## 4. Layer 3A — Developer Consumer Surface
|
||||
|
||||
@@ -297,7 +297,7 @@ Purpose. The technical execution layer for the L1/L2 substrate, including the su
|
||||
|
||||
### 12.1 Substrate abstraction (locked this revision)
|
||||
|
||||
L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack Intermediate Representation (IR) — a substrate-neutral description of:
|
||||
L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack — a substrate-neutral description of:
|
||||
|
||||
- Resources with typed input contracts, typed output contracts, and declared NFRs.
|
||||
|
||||
@@ -307,25 +307,25 @@ L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack I
|
||||
|
||||
- Policy hooks (the points in the composition where policy checks attach).
|
||||
|
||||
The L1 registry, the L2 thin-composition tree, the YML standard, and the policy check result schema are all defined against the IR. None of them is defined against any specific substrate.
|
||||
The L1 registry, the L2 thin-composition tree, the YML standard, and the policy check result schema are all defined against the stack schema. None of them is defined against any specific substrate.
|
||||
|
||||
Substrate adapters are the only substrate-specific code. An adapter compiles the IR into a substrate execution plan. v1 ships exactly one adapter: the Terraform adapter. v2+ may add additional adapters (OpenTofu, Pulumi, K8s CRDs) without architectural change.
|
||||
Substrate adapters are the only substrate-specific code. An adapter compiles the stack into a substrate execution plan. v1 ships exactly one adapter: the Terraform adapter. v2+ may add additional adapters (OpenTofu, Pulumi, K8s CRDs) without architectural change.
|
||||
|
||||
v1 implementation reality: the IR is shaped to round-trip cleanly to Terraform because there is no other adapter to differentiate from. The IR and the Terraform output are nearly isomorphic in v1. As additional adapters appear in v2+, the IR gets more expressive (e.g., substrate-specific output types) and the adapters gain translation logic, but the L1 module content, the YML standard, and the thin-composition tree do not change. This is the design that prevents the polyglot mess.
|
||||
v1 implementation reality: the stack is shaped to round-trip cleanly to Terraform because there is no other adapter to differentiate from. The stack and the Terraform output are nearly isomorphic in v1. As additional adapters appear in v2+, the stack gets more expressive (e.g., substrate-specific output types) and the adapters gain translation logic, but the L1 module content, the YML standard, and the composition tree do not change. This is the design that prevents the polyglot mess.
|
||||
|
||||
Why not build the abstraction earlier? Building a substrate-agnostic IR before there is a second adapter to test against is speculative generality. The v1 commitment is: (1) the L1 module interface is defined against the IR even though the only adapter is Terraform, and (2) the central pipeline, registry, and policy schema consume the IR-typed contracts. The adapter is the only place where substrate terminology appears in v1.
|
||||
Why not build the abstraction earlier? Building a substrate-agnostic stack before there is a second adapter to test against is speculative generality. The v1 commitment is: (1) the L1 module interface is defined against the stack schema even though the only adapter is Terraform, and (2) the central pipeline, registry, and policy schema consume the stack-typed contracts. The adapter is the only place where substrate terminology appears in v1.
|
||||
|
||||
### 12.2 Terraform adapter (v1)
|
||||
|
||||
The Terraform adapter:
|
||||
|
||||
- Translates the IR-typed L1 module interface to a Terraform variable block and a Terraform output block.
|
||||
- Translates the stack-typed L1 module interface to a Terraform variable block and a Terraform output block.
|
||||
|
||||
- Translates the IR-typed L2 thin-composition tree to a Terraform root module that calls the L1 modules.
|
||||
- Translates the stack-typed L2 composition tree to a Terraform root module that calls the L1 modules.
|
||||
|
||||
- Translates the IR-typed relationships to Terraform module references.
|
||||
- Translates the stack-typed relationships to Terraform module references.
|
||||
|
||||
- Emits a Terraform plan from the IR.
|
||||
- Emits a Terraform plan from the stack.
|
||||
|
||||
The adapter is a thin layer. It does not own L1/L2 content; it only translates.
|
||||
|
||||
@@ -372,7 +372,7 @@ Schema (canonical form, lives in the central pipeline repo):
|
||||
"result": "pass | fail | skipped | error",
|
||||
"message": "human-readable",
|
||||
"evidence": { "...engine-specific payload, opaque to the signal..." },
|
||||
"resourceRef": "IR-typed resource identifier"
|
||||
"resourceRef": "stack-typed resource identifier"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -380,11 +380,11 @@ The Checkov adapter runs in the same GitHub Actions step as Checkov itself and t
|
||||
|
||||
### 12.7 Registry maintenance
|
||||
|
||||
Locked: L1 module publication updates the L1 registry in the same PR as the module. Registry and module land together. The registry is the IR-typed contract, not a Terraform-specific variable schema. The L1 registry, the central pipeline, and the policy schema all consume the same IR-typed contract — there is one source of truth for the L1 interface, not multiple substrate-specific copies.
|
||||
Locked: L1 module publication updates the L1 registry in the same PR as the module. Registry and module land together. The registry is the stack-typed contract, not a Terraform-specific variable schema. The L1 registry, the central pipeline, and the policy schema all consume the same stack-typed contract — there is one source of truth for the L1 interface, not multiple substrate-specific copies.
|
||||
|
||||
### 12.8 Contract-schema-to-IR resolution
|
||||
### 12.8 Contract-schema-to-stack resolution
|
||||
|
||||
The contract schema declares the consumer's intent in IR-typed terms. The central pipeline resolves the contract to a target stack (a list of L1 module instances with their inputs and the relationships between them). The Terraform adapter compiles the target stack to a Terraform execution plan. This resolution is substrate-agnostic — the target stack is in the IR.
|
||||
The contract schema declares the consumer's intent in stack-typed terms. The central pipeline resolves the contract to a target stack (a list of L1 module instances with their inputs and the relationships between them). The Terraform adapter compiles the target stack to a Terraform execution plan. This resolution is substrate-agnostic — the target stack is in the stack schema.
|
||||
|
||||
## 13. Consolidated Open Design Decisions
|
||||
|
||||
@@ -420,7 +420,7 @@ by `✅ RESOLVED (see PROJECT.md)`.
|
||||
|
||||
- (BA.E) Consumer onboarding. ✅ RESOLVED (see PROJECT.md) — developer (L3A): getting-started → contract schema + central pipeline template; citizen (L3B): scoped agent + skill catalog; both end in a sandbox dev submission that must pass the confidence gate.
|
||||
|
||||
- (BA.F) Cross-platform evolution. ✅ RESOLVED (see PROJECT.md) — contract schema, IR, PolicyCheckResult, confidence signal, audit stream are portable; forge-specific code = workflow YAML, OIDC trust, CODEOWNERS, Environments; a second forge needs a forge adapter + workflow-template translator.
|
||||
- (BA.F) Cross-platform evolution. ✅ RESOLVED (see PROJECT.md) — contract schema, stack, PolicyCheckResult, confidence signal, audit stream are portable; forge-specific code = workflow YAML, OIDC trust, CODEOWNERS, Environments; a second forge needs a forge adapter + workflow-template translator.
|
||||
|
||||
- (Q1.3) OpenTofu timing. ✅ RESOLVED (deferred — see PROJECT.md) — not in v1 or v1.1; the substrate abstraction makes OpenTofu a future adapter, not an architecture change.
|
||||
|
||||
@@ -428,7 +428,7 @@ by `✅ RESOLVED (see PROJECT.md)`.
|
||||
|
||||
Status: **v1.0**. All 11 open items in §13 are resolved. The architecture is
|
||||
internally consistent; the v1.1 implementation spike (ACDL Phases 08-10)
|
||||
validates the locked substrate abstraction + contract→IR→adapter path
|
||||
validates the locked substrate abstraction + contract→stack→adapter path
|
||||
against real AWS via a per-run-rotated key (D-039; OIDC deferred to v1.2).
|
||||
The v1.2 build-out (S3 Object Lock, JWS, HITL wiring, L3B skill catalog,
|
||||
Kyverno/OPA, real OIDC federation, multi-region) is design-authored in
|
||||
@@ -447,7 +447,7 @@ Phase 07):
|
||||
|
||||
| REQ | File | Owner persona |
|
||||
|-----|------|--------------|
|
||||
| REQ-17 | `schemas/ir.schema.json` | platform-engineer |
|
||||
| REQ-17 | `schemas/stack.schema.json` | platform-engineer |
|
||||
| REQ-18 | `schemas/policy_check_result.schema.json` + `adapters/terraform/policy/checkov_adapter.py` | security-engineer |
|
||||
| REQ-19 | `platform/confidence_signal.py` | backend-engineer + security-engineer (co-authored) |
|
||||
| REQ-20 | `platform/audit_ledger_design.md` | security-engineer |
|
||||
|
||||
+18
-18
@@ -2,7 +2,7 @@
|
||||
|
||||
Three things to set up before I deliver the document, because they determine how I write the doc:
|
||||
|
||||
1. What is locked from the resolution session. Eight items: environment model (Path A — dev-only autonomous, no staging), substrate abstraction (Target Stack IR + adapter pattern, Terraform adapter in v1), policy toolchain (Checkov for Terraform plan, Kyverno for K8s, OPA last resort), separation of duties (CODEOWNERS for routing + DynamoDB outbox for identity distinctness), policy normalization PolicyCheckResult schema with engine adapters), HITL matrix (full 8-concern matrix with evidence, freshness, source), HITL timeout (1d warn, 2d freeze), HITL rollback (pre-execution model, audit chain extended, no partial deploy).
|
||||
1. What is locked from the resolution session. Eight items: environment model (Path A — dev-only autonomous, no staging), substrate abstraction (Target Stack + adapter pattern, Terraform adapter in v1), policy toolchain (Checkov for Terraform plan, Kyverno for K8s, OPA last resort), separation of duties (CODEOWNERS for routing + DynamoDB outbox for identity distinctness), policy normalization PolicyCheckResult schema with engine adapters), HITL matrix (full 8-concern matrix with evidence, freshness, source), HITL timeout (1d warn, 2d freeze), HITL rollback (pre-execution model, audit chain extended, no partial deploy).
|
||||
|
||||
2. What is still open after the session. Eleven items, listed in the updated Section 13. They are the gating items for v1.0.
|
||||
|
||||
@@ -25,7 +25,7 @@ Resolution session log (this revision):
|
||||
| ID | Question | Resolution |
|
||||
|---|---|---|
|
||||
| Q1 | Environment model | Path A locked. Dev is the only autonomous environment. QA HITL at qa. SRE HITL at prod and dr. Staging does not exist. |
|
||||
| Q1.2 | Substrate trajectory | Substrate abstraction locked. L1/L2 are defined against a Target Stack IR. Substrate adapters compile the IR to a substrate execution plan. v1 ships only the Terraform adapter. |
|
||||
| Q1.2 | Substrate trajectory | Substrate abstraction locked. L1/L2 are defined against a Target Stack. Substrate adapters compile the stack to a substrate execution plan. v1 ships only the Terraform adapter. |
|
||||
| Q1.3 | OpenTofu timing | 🟡 OPEN (W3.D-adjacent). No specific version or trigger committed. |
|
||||
| Q2.1 | Policy toolchain | Locked. Checkov for Terraform plan policy. Kyverno for K8s-native and platform-internal policy. OPA/Rego reserved for cross-resource cases; explicitly last resort due to Rego complexity. |
|
||||
| Q2.2 | Separation of duties | Locked. GitHub CODEOWNERS routes the right reviewer to the right environment. Platform-internal identity record in DynamoDB outbox enforces qaApprover ≠ prodApprover for the same contract. |
|
||||
@@ -76,7 +76,7 @@ Locked commitments (unchanged from v0.1):
|
||||
|
||||
🟡 OPEN (W1.A): AI-refinement operational trigger. The criterion for flipping aiRefinement from false to true needs a falsifiable operational signal. Recommendation: joint condition — N ≥ 50 consecutive changes with zero rollbacks AND no L1/L2 incident in the last 6 months AND Infra & Ops holds a unilateral override. Pending sign-off.
|
||||
|
||||
🟡 OPEN (sub-decision surfaced this revision): The L1 module's interface field is defined against the Target Stack IR, not against Terraform's variable block directly. In v1, the IR is shaped to round-trip cleanly to Terraform, but the schema is substrate-agnostic. Pending v1 implementation details in Section 12.
|
||||
🟡 OPEN (sub-decision surfaced this revision): The L1 module's interface field is defined against the Target Stack, not against Terraform's variable block directly. In v1, the stack is shaped to round-trip cleanly to Terraform, but the schema is substrate-agnostic. Pending v1 implementation details in Section 12.
|
||||
|
||||
## 3. Layer 2 — Composed Stacks
|
||||
|
||||
@@ -98,7 +98,7 @@ Locked commitments (unchanged from v0.1):
|
||||
|
||||
🟡 OPEN (W1.B): Multi-stack edge case rule. The multiStack: true exception needs a falsifiable rule. Recommendation: permitted only for (a) DR-region mirror of the primary stack, (b) time-boxed experimental stack with TTL ≤ 30 days, (c) explicit Infra & Ops approval for a documented reason captured in multiStack.justification. Pending sign-off.
|
||||
|
||||
🟡 OPEN (sub-decision surfaced this revision): The L2 thin-composition tree's wires field is defined against the IR's relationship type, not against a Terraform module block. The IR → Terraform translation is the Terraform adapter's job (Section 12). The thin-composition pipeline itself is substrate-agnostic.
|
||||
🟡 OPEN (sub-decision surfaced this revision): The L2 composition tree's wires field is defined against the stack's relationship type, not against a Terraform module block. The stack → Terraform translation is the Terraform adapter's job (Section 12). The composition pipeline itself is substrate-agnostic.
|
||||
|
||||
## 4. Layer 3A — Developer Consumer Surface
|
||||
|
||||
@@ -301,7 +301,7 @@ Purpose. The technical execution layer for the L1/L2 substrate, including the su
|
||||
|
||||
### 12.1 Substrate abstraction (locked this revision)
|
||||
|
||||
L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack Intermediate Representation (IR) — a substrate-neutral description of:
|
||||
L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack — a substrate-neutral description of:
|
||||
|
||||
- Resources with typed input contracts, typed output contracts, and declared NFRs.
|
||||
|
||||
@@ -311,25 +311,25 @@ L1/L2 are substrate-agnostic in shape. The architecture defines a Target Stack I
|
||||
|
||||
- Policy hooks (the points in the composition where policy checks attach).
|
||||
|
||||
The L1 registry, the L2 thin-composition tree, the YML standard, and the policy check result schema are all defined against the IR. None of them is defined against any specific substrate.
|
||||
The L1 registry, the L2 composition tree, the YML standard, and the policy check result schema are all defined against the stack schema. None of them is defined against any specific substrate.
|
||||
|
||||
Substrate adapters are the only substrate-specific code. An adapter compiles the IR into a substrate execution plan. v1 ships exactly one adapter: the Terraform adapter. v2+ may add additional adapters (OpenTofu, Pulumi, K8s CRDs) without architectural change.
|
||||
Substrate adapters are the only substrate-specific code. An adapter compiles the stack into a substrate execution plan. v1 ships exactly one adapter: the Terraform adapter. v2+ may add additional adapters (OpenTofu, Pulumi, K8s CRDs) without architectural change.
|
||||
|
||||
v1 implementation reality: the IR is shaped to round-trip cleanly to Terraform because there is no other adapter to differentiate from. The IR and the Terraform output are nearly isomorphic in v1. As additional adapters appear in v2+, the IR gets more expressive (e.g., substrate-specific output types) and the adapters gain translation logic, but the L1 module content, the YML standard, and the thin-composition tree do not change. This is the design that prevents the polyglot mess.
|
||||
v1 implementation reality: the stack is shaped to round-trip cleanly to Terraform because there is no other adapter to differentiate from. The stack and the Terraform output are nearly isomorphic in v1. As additional adapters appear in v2+, the stack gets more expressive (e.g., substrate-specific output types) and the adapters gain translation logic, but the L1 module content, the YML standard, and the composition tree do not change. This is the design that prevents the polyglot mess.
|
||||
|
||||
Why not build the abstraction earlier? Building a substrate-agnostic IR before there is a second adapter to test against is speculative generality. The v1 commitment is: (1) the L1 module interface is defined against the IR even though the only adapter is Terraform, and (2) the central pipeline, registry, and policy schema consume the IR-typed contracts. The adapter is the only place where substrate terminology appears in v1.
|
||||
Why not build the abstraction earlier? Building a substrate-agnostic stack before there is a second adapter to test against is speculative generality. The v1 commitment is: (1) the L1 module interface is defined against the stack schema even though the only adapter is Terraform, and (2) the central pipeline, registry, and policy schema consume the stack-typed contracts. The adapter is the only place where substrate terminology appears in v1.
|
||||
|
||||
### 12.2 Terraform adapter (v1)
|
||||
|
||||
The Terraform adapter:
|
||||
|
||||
- Translates the IR-typed L1 module interface to a Terraform variable block and a Terraform output block.
|
||||
- Translates the stack-typed L1 module interface to a Terraform variable block and a Terraform output block.
|
||||
|
||||
- Translates the IR-typed L2 thin-composition tree to a Terraform root module that calls the L1 modules.
|
||||
- Translates the stack-typed L2 composition tree to a Terraform root module that calls the L1 modules.
|
||||
|
||||
- Translates the IR-typed relationships to Terraform module references.
|
||||
- Translates the stack-typed relationships to Terraform module references.
|
||||
|
||||
- Emits a Terraform plan from the IR.
|
||||
- Emits a Terraform plan from the stack.
|
||||
|
||||
The adapter is a thin layer. It does not own L1/L2 content; it only translates.
|
||||
|
||||
@@ -367,7 +367,7 @@ Schema (canonical form, lives in the central pipeline repo):
|
||||
"result": "pass | fail | skipped | error",
|
||||
"message": "human-readable",
|
||||
"evidence": { "...engine-specific payload, opaque to the signal..." },
|
||||
"resourceRef": "IR-typed resource identifier"
|
||||
"resourceRef": "stack-typed resource identifier"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -375,11 +375,11 @@ The Checkov adapter runs in the same GitHub Actions step as Checkov itself and t
|
||||
|
||||
### 12.7 Registry maintenance
|
||||
|
||||
Locked: L1 module publication updates the L1 registry in the same PR as the module. Registry and module land together. The registry is the IR-typed contract, not a Terraform-specific variable schema. The L1 registry, the central pipeline, and the policy schema all consume the same IR-typed contract — there is one source of truth for the L1 interface, not multiple substrate-specific copies.
|
||||
Locked: L1 module publication updates the L1 registry in the same PR as the module. Registry and module land together. The registry is the stack-typed contract, not a Terraform-specific variable schema. The L1 registry, the central pipeline, and the policy schema all consume the same stack-typed contract — there is one source of truth for the L1 interface, not multiple substrate-specific copies.
|
||||
|
||||
### 12.8 Contract-schema-to-IR resolution
|
||||
### 12.8 Contract-schema-to-stack resolution
|
||||
|
||||
The contract schema declares the consumer's intent in IR-typed terms. The central pipeline resolves the contract to a target stack (a list of L1 module instances with their inputs and the relationships between them). The Terraform adapter compiles the target stack to a Terraform execution plan. This resolution is substrate-agnostic — the target stack is in the IR.
|
||||
The contract schema declares the consumer's intent in stack-typed terms. The central pipeline resolves the contract to a target stack (a list of L1 module instances with their inputs and the relationships between them). The Terraform adapter compiles the target stack to a Terraform execution plan. This resolution is substrate-agnostic — the target stack is in the stack schema.
|
||||
|
||||
🟡 OPEN (W3.D): L1/L2 standard versioning details, including pin model and evolution compatibility contract.
|
||||
|
||||
@@ -429,7 +429,7 @@ To finalize to v1.0:
|
||||
|
||||
1. Resolve the 11 open items in Section 13.
|
||||
|
||||
2. Validate the locked substrate abstraction against a real v1 implementation spike (one L1 module, one L2 thin-composition, one Terraform adapter, one contract submission end-to-end). The spike validates that the IR-shaped commitments do not require a polyglot mess.
|
||||
2. Validate the locked substrate abstraction against a real v1 implementation spike (one L1 module, one L2 composition, one Terraform adapter, one contract submission end-to-end). The spike validates that the stack commitments do not require a polyglot mess.
|
||||
|
||||
3. Validate the locked HITL matrix against a tabletop exercise with QA and SRE.
|
||||
|
||||
|
||||
+13
-14
@@ -13,9 +13,8 @@ There are two kinds of module:
|
||||
an `interface.json` declaring its inputs and outputs, and a `README.md`
|
||||
in plain language.
|
||||
- **L2 compositions** — a composition that references multiple L1s to
|
||||
deploy a complete stack (e.g. an ECS Fargate microservice). **The L2
|
||||
composition layer is being redesigned.** The previous implementation
|
||||
has been removed; a new mechanism will be designed in a later phase.
|
||||
deploy a complete stack (e.g. an ECS Fargate microservice). Each L2
|
||||
has a `composition.json` declaring its children and wires.
|
||||
|
||||
The Terraform adapter (`adapters/terraform/adapter.py`) compiles a
|
||||
module instance to Terraform. Each module's README documents which
|
||||
@@ -25,25 +24,25 @@ Terraform resources it creates.
|
||||
|
||||
| Module | What it creates | README |
|
||||
|--------|----------------|--------|
|
||||
| `l1-s3` | `aws_s3_bucket` — a single S3 bucket | [README](l1/l1-s3/README.md) |
|
||||
| `l1-vpc` | `aws_vpc` + `aws_subnet` + `aws_route_table` + `aws_internet_gateway` — VPC with subnets and routing | [README](l1/l1-vpc/README.md) |
|
||||
| `l1-ecs-cluster` | `aws_ecs_cluster` — ECS Fargate cluster | [README](l1/l1-ecs-cluster/README.md) |
|
||||
| `l1-ecs-service` | `aws_ecs_task_definition` + `aws_ecs_service` — Fargate service with task definition | [README](l1/l1-ecs-service/README.md) |
|
||||
| `l1-iam-role` | `aws_iam_role` — IAM role with assume-role policy | [README](l1/l1-iam-role/README.md) |
|
||||
| `l1-alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [README](l1/l1-alb/README.md) |
|
||||
| `l1-ecr` | `aws_ecr_repository` — ECR container image repository | [README](l1/l1-ecr/README.md) |
|
||||
| `s3` | `aws_s3_bucket` — a single S3 bucket | [README](l1/s3/README.md) |
|
||||
| `vpc` | `aws_vpc` + `aws_subnet` + `aws_route_table` + `aws_internet_gateway` — VPC with subnets and routing | [README](l1/vpc/README.md) |
|
||||
| `ecs-cluster` | `aws_ecs_cluster` — ECS Fargate cluster | [README](l1/ecs-cluster/README.md) |
|
||||
| `ecs-service` | `aws_ecs_task_definition` + `aws_ecs_service` — Fargate service with task definition | [README](l1/ecs-service/README.md) |
|
||||
| `iam-role` | `aws_iam_role` — IAM role with assume-role policy | [README](l1/iam-role/README.md) |
|
||||
| `alb` | `aws_lb` + `aws_lb_target_group` + `aws_lb_listener` — Application Load Balancer | [README](l1/alb/README.md) |
|
||||
| `ecr` | `aws_ecr_repository` — ECR container image repository | [README](l1/ecr/README.md) |
|
||||
|
||||
## L2 compositions
|
||||
|
||||
| Module | What it references | README |
|
||||
|--------|--------------------|--------|
|
||||
| `l2-microservice` | 6 L1s (vpc, cluster, ecr, iam-role, alb, ecs-service) — **under redesign** | [README](l2/l2-microservice/README.md) |
|
||||
| `l2-static-asset` | 1 L1 (s3) — **under redesign** | [README](l2/l2-static-asset/README.md) |
|
||||
| `microservice` | 6 L1s (vpc, cluster, ecr, iam-role, alb, ecs-service) | [README](l2/microservice/README.md) |
|
||||
| `static-asset` | 1 L1 (s3) | [README](l2/static-asset/README.md) |
|
||||
|
||||
## Registry
|
||||
|
||||
Module versions are tracked in `registry.json`. Only L1 entries are
|
||||
active; L2 entries have been pruned pending the composition redesign.
|
||||
Module versions are tracked in `registry.json`. Both L1 and L2 entries
|
||||
are registered.
|
||||
|
||||
## Template
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# l1-alb — Application Load Balancer (load balancer + target group + listener)
|
||||
# alb — Application Load Balancer (load balancer + target group + listener)
|
||||
|
||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
||||
|
||||
An Application Load Balancer with a target group and a listener. This is
|
||||
a multi-resource module: it creates a load balancer, a target group, and
|
||||
a listener that forwards traffic to the target group. The target group
|
||||
is what `l1-ecs-service` registers its tasks with.
|
||||
is what `ecs-service` registers its tasks with.
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -20,7 +20,7 @@ is what `l1-ecs-service` registers its tasks with.
|
||||
| Name | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `name` | string | yes | — | Name tag for the load balancer and child resources |
|
||||
| `subnets` | string | yes | — | Comma-separated subnet ids (from `l1-vpc`) |
|
||||
| `subnets` | string | yes | — | Comma-separated subnet ids (from `vpc`) |
|
||||
| `security_group` | string | yes | — | Security group id for the load balancer |
|
||||
| `port` | number | no | 80 | Listener port |
|
||||
| `protocol` | string | no | `HTTP` | Listener protocol |
|
||||
@@ -40,7 +40,7 @@ is what `l1-ecs-service` registers its tasks with.
|
||||
{
|
||||
"id": "alb",
|
||||
"type": "aws:elbv2:loadbalancer",
|
||||
"module": "l1-alb@1.0.0",
|
||||
"module": "alb@1.0.0",
|
||||
"inputs": {
|
||||
"name": "acdl-microservice",
|
||||
"subnets": "ref:vpc.subnet_ids",
|
||||
@@ -52,7 +52,7 @@ is what `l1-ecs-service` registers its tasks with.
|
||||
}
|
||||
```
|
||||
|
||||
The `target_group_arn` output is referenced by `l1-ecs-service` as its
|
||||
The `target_group_arn` output is referenced by `ecs-service` as its
|
||||
`lb_target_group_arn` input to wire the service to the ALB.
|
||||
|
||||
## Compliance extension points
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "l1-alb",
|
||||
"name": "alb",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:elbv2:loadbalancer",
|
||||
"description": "Application Load Balancer primitive (substrate-agnostic IR types aws:elbv2:loadbalancer + aws:elbv2:listener + aws:elbv2:targetgroup; the Terraform adapter translates to aws_lb/aws_lb_listener/aws_lb_target_group).",
|
||||
"description": "Application Load Balancer primitive (substrate-agnostic stack types aws:elbv2:loadbalancer + aws:elbv2:listener + aws:elbv2:targetgroup; the Terraform adapter translates to aws_lb/aws_lb_listener/aws_lb_target_group).",
|
||||
"inputs": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
"subnets": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated subnet ids (ref to l1-vpc).",
|
||||
"description": "Comma-separated subnet ids (ref to vpc).",
|
||||
"required": true
|
||||
},
|
||||
"security_group": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# l1-ecr — ECR repository
|
||||
# ecr — ECR repository
|
||||
|
||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
||||
|
||||
@@ -32,7 +32,7 @@ inputs, two outputs.
|
||||
{
|
||||
"id": "ecr",
|
||||
"type": "aws:ecr:repository",
|
||||
"module": "l1-ecr@1.0.0",
|
||||
"module": "ecr@1.0.0",
|
||||
"inputs": {
|
||||
"name": "acdl-microservice",
|
||||
"region": "us-east-1"
|
||||
@@ -41,7 +41,7 @@ inputs, two outputs.
|
||||
```
|
||||
|
||||
The `repository_url` output is used to build the `image` input for
|
||||
`l1-ecs-service` (e.g. `<repository_url>:latest`).
|
||||
`ecs-service` (e.g. `<repository_url>:latest`).
|
||||
|
||||
## Compliance extension points
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "l1-ecr",
|
||||
"name": "ecr",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ecr:repository",
|
||||
"description": "ECR repository primitive (substrate-agnostic IR type aws:ecr:repository; the Terraform adapter translates to aws_ecr_repository).",
|
||||
"description": "ECR repository primitive (substrate-agnostic stack type aws:ecr:repository; the Terraform adapter translates to aws_ecr_repository).",
|
||||
"inputs": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# l1-ecs-cluster — ECS Fargate cluster
|
||||
# ecs-cluster — ECS Fargate cluster
|
||||
|
||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
||||
|
||||
An ECS Fargate cluster. The simplest ECS module — one resource, two
|
||||
inputs, two outputs. The cluster is the container orchestration
|
||||
boundary that `l1-ecs-service` references for task placement.
|
||||
boundary that `ecs-service` references for task placement.
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -32,7 +32,7 @@ boundary that `l1-ecs-service` references for task placement.
|
||||
{
|
||||
"id": "cluster",
|
||||
"type": "aws:ecs:cluster",
|
||||
"module": "l1-ecs-cluster@1.0.0",
|
||||
"module": "ecs-cluster@1.0.0",
|
||||
"inputs": {
|
||||
"name": "acdl-microservice",
|
||||
"region": "us-east-1"
|
||||
@@ -40,7 +40,7 @@ boundary that `l1-ecs-service` references for task placement.
|
||||
}
|
||||
```
|
||||
|
||||
The `cluster_arn` output is referenced by `l1-ecs-service` as its
|
||||
The `cluster_arn` output is referenced by `ecs-service` as its
|
||||
`cluster_arn` input.
|
||||
|
||||
## Compliance extension points
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "l1-ecs-cluster",
|
||||
"name": "ecs-cluster",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ecs:cluster",
|
||||
"description": "ECS Fargate cluster primitive (substrate-agnostic IR type aws:ecs:cluster; the Terraform adapter translates to aws_ecs_cluster).",
|
||||
"description": "ECS Fargate cluster primitive (substrate-agnostic stack type aws:ecs:cluster; the Terraform adapter translates to aws_ecs_cluster).",
|
||||
"inputs": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# l1-ecs-service — ECS Fargate service (task definition + service)
|
||||
# ecs-service — ECS Fargate service (task definition + service)
|
||||
|
||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
||||
|
||||
@@ -23,10 +23,10 @@ runs it.
|
||||
| `cpu` | number | no | 256 | Task CPU units (Fargate) |
|
||||
| `memory` | number | no | 512 | Task memory in MiB (Fargate) |
|
||||
| `env` | string | no | — | Environment variables as a JSON map string |
|
||||
| `cluster_arn` | arn | yes | — | ECS cluster ARN (from `l1-ecs-cluster`) |
|
||||
| `subnets` | string | yes | — | Comma-separated subnet ids (from `l1-vpc`) |
|
||||
| `cluster_arn` | arn | yes | — | ECS cluster ARN (from `ecs-cluster`) |
|
||||
| `subnets` | string | yes | — | Comma-separated subnet ids (from `vpc`) |
|
||||
| `security_group` | string | yes | — | Security group id for the service ENIs |
|
||||
| `lb_target_group_arn` | arn | no | — | Optional ALB target group ARN (from `l1-alb`) |
|
||||
| `lb_target_group_arn` | arn | no | — | Optional ALB target group ARN (from `alb`) |
|
||||
| `region` | string | yes | — | AWS region the service is created in |
|
||||
|
||||
## Outputs
|
||||
@@ -42,7 +42,7 @@ runs it.
|
||||
{
|
||||
"id": "service",
|
||||
"type": "aws:ecs:task_definition",
|
||||
"module": "l1-ecs-service@1.0.0",
|
||||
"module": "ecs-service@1.0.0",
|
||||
"inputs": {
|
||||
"image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest",
|
||||
"port": 8080,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "l1-ecs-service",
|
||||
"name": "ecs-service",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ecs:task_definition",
|
||||
"description": "ECS Fargate service primitive (substrate-agnostic IR types aws:ecs:task_definition + aws:ecs:service; the Terraform adapter translates to aws_ecs_task_definition/aws_ecs_service).",
|
||||
"description": "ECS Fargate service primitive (substrate-agnostic stack types aws:ecs:task_definition + aws:ecs:service; the Terraform adapter translates to aws_ecs_task_definition/aws_ecs_service).",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"type": "string",
|
||||
@@ -34,12 +34,12 @@
|
||||
},
|
||||
"cluster_arn": {
|
||||
"type": "arn",
|
||||
"description": "ECS cluster ARN (ref to l1-ecs-cluster).",
|
||||
"description": "ECS cluster ARN (ref to ecs-cluster).",
|
||||
"required": true
|
||||
},
|
||||
"subnets": {
|
||||
"type": "string",
|
||||
"description": "Comma-separated subnet ids (ref to l1-vpc).",
|
||||
"description": "Comma-separated subnet ids (ref to vpc).",
|
||||
"required": true
|
||||
},
|
||||
"security_group": {
|
||||
@@ -49,7 +49,7 @@
|
||||
},
|
||||
"lb_target_group_arn": {
|
||||
"type": "arn",
|
||||
"description": "Optional ALB target group ARN (ref to l1-alb).",
|
||||
"description": "Optional ALB target group ARN (ref to alb).",
|
||||
"required": false
|
||||
},
|
||||
"region": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# l1-iam-role — IAM role
|
||||
# iam-role — IAM role
|
||||
|
||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
||||
|
||||
@@ -33,7 +33,7 @@ policy attachments. Used as the ECS task execution role.
|
||||
{
|
||||
"id": "roles",
|
||||
"type": "aws:iam:role",
|
||||
"module": "l1-iam-role@1.0.0",
|
||||
"module": "iam-role@1.0.0",
|
||||
"inputs": {
|
||||
"role_name": "acdl-microservice-exec",
|
||||
"assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ecs-tasks.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "l1-iam-role",
|
||||
"name": "iam-role",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:iam:role",
|
||||
"description": "IAM role primitive (substrate-agnostic IR type aws:iam:role; the Terraform adapter translates to aws_iam_role).",
|
||||
"description": "IAM role primitive (substrate-agnostic stack type aws:iam:role; the Terraform adapter translates to aws_iam_role).",
|
||||
"inputs": {
|
||||
"role_name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# l1-s3 — S3 bucket
|
||||
# s3 — S3 bucket
|
||||
|
||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
||||
|
||||
@@ -37,7 +37,7 @@ resource, two inputs, two outputs. Versioning is enabled by default.
|
||||
{
|
||||
"id": "s3",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "l1-s3@1.0.0",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "acdl-spike-bucket",
|
||||
"region": "us-east-1"
|
||||
@@ -45,7 +45,7 @@ resource, two inputs, two outputs. Versioning is enabled by default.
|
||||
}
|
||||
```
|
||||
|
||||
A concrete instance is at `spike_instance.json` (used by the platform
|
||||
A concrete instance is at `instance.json` (used by the platform
|
||||
pipeline as the regression baseline).
|
||||
|
||||
## Compliance extension points
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"stack": {
|
||||
"name": "l1-s3",
|
||||
"name": "s3",
|
||||
"kind": "l1",
|
||||
"depth": 1
|
||||
},
|
||||
@@ -9,7 +9,7 @@
|
||||
{
|
||||
"id": "s3",
|
||||
"type": "aws:s3:bucket",
|
||||
"module": "l1-s3@1.0.0",
|
||||
"module": "s3@1.0.0",
|
||||
"inputs": {
|
||||
"bucket_name": "acdl-spike-bucket",
|
||||
"region": "us-east-1"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "l1-s3",
|
||||
"name": "s3",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:s3:bucket",
|
||||
"description": "S3 bucket primitive (substrate-agnostic IR type aws:s3:bucket; the Terraform adapter translates to aws_s3_bucket).",
|
||||
"description": "S3 bucket primitive (substrate-agnostic stack type aws:s3:bucket; the Terraform adapter translates to aws_s3_bucket).",
|
||||
"inputs": {
|
||||
"bucket_name": {
|
||||
"type": "string",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# l1-vpc — VPC with subnets and routing
|
||||
# vpc — VPC with subnets and routing
|
||||
|
||||
> **Module kind:** L1 primitive | **Version:** 1.0.0
|
||||
|
||||
@@ -38,7 +38,7 @@ that other modules (ALB, ECS service) reference for subnet ids.
|
||||
{
|
||||
"id": "vpc",
|
||||
"type": "aws:ec2:vpc",
|
||||
"module": "l1-vpc@1.0.0",
|
||||
"module": "vpc@1.0.0",
|
||||
"inputs": {
|
||||
"cidr": "10.0.0.0/16",
|
||||
"azs": "us-east-1a,us-east-1b",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "l1-vpc",
|
||||
"name": "vpc",
|
||||
"version": "1.0.0",
|
||||
"kind": "l1",
|
||||
"type": "aws:ec2:vpc",
|
||||
"description": "VPC primitive (substrate-agnostic IR types aws:ec2:vpc + aws:ec2:subnet + aws:ec2:routetable; the Terraform adapter translates to aws_vpc/aws_subnet/aws_route_table).",
|
||||
"description": "VPC primitive (substrate-agnostic stack types aws:ec2:vpc + aws:ec2:subnet + aws:ec2:routetable; the Terraform adapter translates to aws_vpc/aws_subnet/aws_route_table).",
|
||||
"inputs": {
|
||||
"cidr": {
|
||||
"type": "string",
|
||||
|
||||
@@ -1,47 +1,59 @@
|
||||
# l2-microservice — ECS Fargate microservice (composition being redesigned)
|
||||
# microservice — ECS Fargate microservice
|
||||
|
||||
> **Module kind:** L2 composition | **Version:** TBD | **Status:** Under redesign
|
||||
> **Module kind:** L2 composition | **Version:** 1.0.0
|
||||
|
||||
A composition that references multiple L1 primitives to deploy an ECS
|
||||
Fargate microservice end-to-end (VPC, cluster, ECR, IAM role, ALB,
|
||||
ECS service).
|
||||
|
||||
**The composition layer is being redesigned.** The previous
|
||||
thin-composition implementation (a `composition.json` with children +
|
||||
wires) has been removed. A new composition mechanism will be designed
|
||||
in a later phase.
|
||||
|
||||
## Resources
|
||||
|
||||
TBD — the composition will reference these L1 primitives:
|
||||
The composition references these L1 primitives:
|
||||
|
||||
| L1 module | Purpose | README |
|
||||
|-----------|---------|--------|
|
||||
| `l1-vpc` | VPC, subnets, routing | [README](../l1/l1-vpc/README.md) |
|
||||
| `l1-ecs-cluster` | ECS Fargate cluster | [README](../l1/l1-ecs-cluster/README.md) |
|
||||
| `l1-ecr` | ECR image repository | [README](../l1/l1-ecr/README.md) |
|
||||
| `l1-iam-role` | IAM task execution role | [README](../l1/l1-iam-role/README.md) |
|
||||
| `l1-alb` | Application Load Balancer | [README](../l1/l1-alb/README.md) |
|
||||
| `l1-ecs-service` | ECS task definition + service | [README](../l1/l1-ecs-service/README.md) |
|
||||
| `vpc` | VPC, subnets, routing | [README](../l1/vpc/README.md) |
|
||||
| `ecs-cluster` | ECS Fargate cluster | [README](../l1/ecs-cluster/README.md) |
|
||||
| `ecr` | ECR image repository | [README](../l1/ecr/README.md) |
|
||||
| `iam-role` | IAM task execution role | [README](../l1/iam-role/README.md) |
|
||||
| `alb` | Application Load Balancer | [README](../l1/alb/README.md) |
|
||||
| `ecs-service` | ECS task definition + service | [README](../l1/ecs-service/README.md) |
|
||||
|
||||
## Inputs
|
||||
|
||||
TBD — will be defined when the composition mechanism is redesigned.
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `image` | string | yes | ECR image URL for the task container |
|
||||
| `port` | number | yes | Container port the service listens on |
|
||||
| `region` | string | yes | AWS region |
|
||||
| `cidr` | string | no | VPC CIDR block (default 10.0.0.0/16) |
|
||||
| `azs` | string | no | Comma-separated availability zones |
|
||||
|
||||
## Outputs
|
||||
|
||||
TBD — will be defined when the composition mechanism is redesigned.
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `lb_arn` | arn | The load balancer ARN |
|
||||
| `service_arn` | arn | The ECS service ARN |
|
||||
|
||||
## Usage
|
||||
|
||||
TBD — the composition mechanism is being redesigned. Until then, use
|
||||
the L1 primitives directly. See each L1 module's README for usage
|
||||
examples.
|
||||
Define a contract referencing this composition:
|
||||
|
||||
```yaml
|
||||
uses: acdl/pipelines/deploy.yaml@v1
|
||||
module: microservice
|
||||
environment: dev
|
||||
inputs:
|
||||
image: 581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest
|
||||
port: 8080
|
||||
region: us-east-1
|
||||
```
|
||||
|
||||
## Compliance extension points
|
||||
|
||||
The composition will need to wire compliance resources across L1s
|
||||
when the compliance milestone (GDPR, SOX, SOC2, HIPAA, DORA) lands:
|
||||
The composition can wire compliance resources across L1s when the
|
||||
compliance milestone (GDPR, SOX, SOC2, HIPAA, DORA) lands:
|
||||
|
||||
- **KMS key** — shared encryption key referenced by S3, ECR, CloudWatch Logs, and Secrets Manager.
|
||||
- **CloudTrail** — management-plane audit trail for the entire stack.
|
||||
@@ -53,5 +65,6 @@ See each L1 module's README for per-module compliance extension points.
|
||||
|
||||
## Versioning
|
||||
|
||||
Versioning will be defined when the composition mechanism is
|
||||
redesigned.
|
||||
`1.0.0` — interface MAJOR, behavior MINOR, lifecycle PATCH. MAJOR bumps
|
||||
require a new registry entry (immutable publication); old entries enter
|
||||
a 12-month deprecation window.
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "microservice",
|
||||
"version": "1.0.0",
|
||||
"kind": "l2",
|
||||
"depth": 1,
|
||||
"description": "A composition that references six L1 primitives to deploy an ECS Fargate microservice end-to-end.",
|
||||
"children": [
|
||||
{"id": "vpc", "module": "vpc@1.0.0"},
|
||||
{"id": "cluster", "module": "ecs-cluster@1.0.0"},
|
||||
{"id": "ecr", "module": "ecr@1.0.0"},
|
||||
{"id": "roles", "module": "iam-role@1.0.0"},
|
||||
{"id": "alb", "module": "alb@1.0.0"},
|
||||
{"id": "service", "module": "ecs-service@1.0.0"}
|
||||
],
|
||||
"wires": [
|
||||
{"from": "contract.inputs.bucket_name", "to": "vpc.inputs.cidr", "default": "10.0.0.0/16"},
|
||||
{"from": "contract.inputs.region", "to": "vpc.inputs.region"},
|
||||
{"from": "contract.inputs.region", "to": "cluster.inputs.region"},
|
||||
{"from": "contract.inputs.region", "to": "ecr.inputs.region"},
|
||||
{"from": "contract.inputs.region", "to": "roles.inputs.region"},
|
||||
{"from": "contract.inputs.region", "to": "alb.inputs.region"},
|
||||
{"from": "contract.inputs.region", "to": "service.inputs.region"},
|
||||
{"from": "vpc.outputs.subnet_ids", "to": "alb.inputs.subnets"},
|
||||
{"from": "vpc.outputs.subnet_ids", "to": "service.inputs.subnets"},
|
||||
{"from": "cluster.outputs.cluster_arn", "to": "service.inputs.cluster_arn"},
|
||||
{"from": "ecr.outputs.repository_url", "to": "service.inputs.image"},
|
||||
{"from": "roles.outputs.role_arn", "to": "service.inputs.security_group"},
|
||||
{"from": "alb.outputs.target_group_arn", "to": "service.inputs.lb_target_group_arn"}
|
||||
],
|
||||
"outputs": [
|
||||
{"from": "alb.outputs.lb_arn", "to": "stack.outputs.lb_arn"},
|
||||
{"from": "service.outputs.service_arn", "to": "stack.outputs.service_arn"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "static-asset",
|
||||
"version": "1.0.0",
|
||||
"kind": "l2",
|
||||
"depth": 1,
|
||||
"description": "A composition that references the s3 L1 primitive to deploy a single S3 bucket for static asset hosting.",
|
||||
"children": [
|
||||
{
|
||||
"id": "s3",
|
||||
"module": "s3@1.0.0"
|
||||
}
|
||||
],
|
||||
"wires": [
|
||||
{"from": "contract.inputs.bucket_name", "to": "s3.inputs.bucket_name"},
|
||||
{"from": "contract.inputs.region", "to": "s3.inputs.region"}
|
||||
],
|
||||
"outputs": [
|
||||
{"from": "s3.outputs.bucket_arn", "to": "stack.outputs.bucket_arn"},
|
||||
{"from": "s3.outputs.bucket_name", "to": "stack.outputs.bucket_name"}
|
||||
]
|
||||
}
|
||||
+28
-14
@@ -1,51 +1,65 @@
|
||||
{
|
||||
"l1-s3": {
|
||||
"s3": {
|
||||
"1.0.0": {
|
||||
"interface": "modules-ir/l1/l1-s3/interface.json",
|
||||
"interface": "modules/l1/s3/interface.json",
|
||||
"published_at": "2026-07-21T19:00:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
"l1-vpc": {
|
||||
"vpc": {
|
||||
"1.0.0": {
|
||||
"interface": "modules-ir/l1/l1-vpc/interface.json",
|
||||
"interface": "modules/l1/vpc/interface.json",
|
||||
"published_at": "2026-07-21T21:30:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
"l1-ecs-cluster": {
|
||||
"ecs-cluster": {
|
||||
"1.0.0": {
|
||||
"interface": "modules-ir/l1/l1-ecs-cluster/interface.json",
|
||||
"interface": "modules/l1/ecs-cluster/interface.json",
|
||||
"published_at": "2026-07-21T21:30:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
"l1-ecs-service": {
|
||||
"ecs-service": {
|
||||
"1.0.0": {
|
||||
"interface": "modules-ir/l1/l1-ecs-service/interface.json",
|
||||
"interface": "modules/l1/ecs-service/interface.json",
|
||||
"published_at": "2026-07-21T21:30:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
"l1-iam-role": {
|
||||
"iam-role": {
|
||||
"1.0.0": {
|
||||
"interface": "modules-ir/l1/l1-iam-role/interface.json",
|
||||
"interface": "modules/l1/iam-role/interface.json",
|
||||
"published_at": "2026-07-21T21:30:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
"l1-alb": {
|
||||
"alb": {
|
||||
"1.0.0": {
|
||||
"interface": "modules-ir/l1/l1-alb/interface.json",
|
||||
"interface": "modules/l1/alb/interface.json",
|
||||
"published_at": "2026-07-21T21:30:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
"l1-ecr": {
|
||||
"ecr": {
|
||||
"1.0.0": {
|
||||
"interface": "modules-ir/l1/l1-ecr/interface.json",
|
||||
"interface": "modules/l1/ecr/interface.json",
|
||||
"published_at": "2026-07-21T21:30:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
"static-asset": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l2/static-asset/composition.json",
|
||||
"published_at": "2026-07-22T15:00:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
},
|
||||
"microservice": {
|
||||
"1.0.0": {
|
||||
"interface": "modules/l2/microservice/composition.json",
|
||||
"published_at": "2026-07-22T15:00:00Z",
|
||||
"deprecated": false
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
# ACDL Central Pipeline Contract (v1.4)
|
||||
# ACDL Central CI Pipeline Contract (v1.5)
|
||||
#
|
||||
# This is the single source of truth for the CI/CD pipeline. Both
|
||||
# .gitea/workflows/ci.yml (Gitea Actions, dev) and
|
||||
@@ -31,6 +31,7 @@ stages:
|
||||
python3 -m py_compile \
|
||||
acdl_platform/confidence_signal.py \
|
||||
acdl_platform/outbox_writer.py \
|
||||
acdl_platform/contract_resolver.py \
|
||||
adapters/terraform/adapter.py \
|
||||
adapters/terraform/policy/checkov_adapter.py \
|
||||
scripts/push_consumer_image.py
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# ACDL Central Deployment Pipeline Contract (v1.5)
|
||||
#
|
||||
# This is the single source of truth for the deployment pipeline. It
|
||||
# declares the stages that run when a consumer submits a contract:
|
||||
# validate-contract -> resolve-stack -> terraform-plan -> checkov ->
|
||||
# confidence -> apply (dev only)
|
||||
#
|
||||
# Consumers reference this pipeline via `uses: acdl/pipelines/deploy.yaml@v1`
|
||||
# in their contract YAML. The platform (scripts/run_platform.sh) implements
|
||||
# these stages.
|
||||
#
|
||||
# Validated against schemas/deploy-pipeline.schema.json.
|
||||
|
||||
name: acdl-deploy
|
||||
environment: dev
|
||||
triggers:
|
||||
push: [main]
|
||||
pull_request: [main]
|
||||
runner: ubuntu-latest
|
||||
python_version: "3.12"
|
||||
|
||||
stages:
|
||||
- name: validate-contract
|
||||
description: Validate the consumer contract against the contract schema
|
||||
command: python3 -c "import jsonschema, yaml; jsonschema.validate(yaml.safe_load(open('contracts/static-asset.yaml')), json.load(open('schemas/contract.schema.json')))"
|
||||
required: true
|
||||
|
||||
- name: resolve-stack
|
||||
description: Resolve the contract to a Target Stack instance via the contract resolver
|
||||
command: python3 acdl_platform/contract_resolver.py contracts/static-asset.yaml /tmp/acdl-stack.json
|
||||
required: true
|
||||
|
||||
- name: terraform-plan
|
||||
description: Compile the stack to Terraform and run terraform plan
|
||||
command: bash scripts/run_platform.sh --plan-only contracts/static-asset.yaml
|
||||
required: true
|
||||
|
||||
- name: checkov
|
||||
description: Run Checkov policy checks on the emitted Terraform
|
||||
command: checkov -f terraform/spike/main.tf --framework terraform -o json --soft-fail
|
||||
required: false
|
||||
|
||||
- name: confidence
|
||||
description: Compute the confidence signal from policy + validation inputs
|
||||
command: python3 acdl_platform/confidence_signal.py /tmp/acdl-deploy-inputs.json dev
|
||||
required: true
|
||||
|
||||
- name: apply
|
||||
description: Apply the Terraform plan (dev environment only, autonomous per §10)
|
||||
command: terraform -chdir=terraform/spike apply -auto-approve -lock=false
|
||||
required: false
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://acdl.cloudinit.dev/schemas/contract.schema.json",
|
||||
"title": "ACDL Consumer Contract",
|
||||
"description": "A consumer contract declares intent: which module to deploy, in which environment, with which inputs. The contract references the central ACDL pipeline via 'uses' and declares the module name (matching a registry key), environment, and inputs. The contract resolver resolves this to a Target Stack instance.",
|
||||
"type": "object",
|
||||
"required": ["uses", "module", "environment", "inputs"],
|
||||
"properties": {
|
||||
"uses": {
|
||||
"type": "string",
|
||||
"description": "Reference to the central ACDL deployment pipeline (e.g. 'acdl/pipelines/deploy.yaml@v1')."
|
||||
},
|
||||
"module": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Module name matching a registry key (L1 primitive or L2 composition)."
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"enum": ["dev", "qa", "prod", "dr"],
|
||||
"description": "Target environment. dev = autonomous; qa/prod/dr = HITL gates."
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"description": "Module-specific inputs (bucket_name, region, image, port, etc.). Validated at resolution time against the module's interface.json or composition.json.",
|
||||
"additionalProperties": {"type": ["string", "number", "boolean"]}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://acdl.cloudinit.dev/schemas/deploy-pipeline.schema.json",
|
||||
"title": "ACDL Central Deployment Pipeline Contract",
|
||||
"description": "Declarative contract for the ACDL deployment pipeline. Declares the stages that run when a consumer submits a contract: validate-contract, resolve-stack, terraform-plan, checkov, confidence, apply. The platform (scripts/run_platform.sh) implements these stages. Consumers reference this pipeline via 'uses: acdl/pipelines/deploy.yaml@v1' in their contract YAML.",
|
||||
"type": "object",
|
||||
"required": ["name", "triggers", "runner", "stages"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Pipeline name."
|
||||
},
|
||||
"environment": {
|
||||
"type": "string",
|
||||
"enum": ["dev", "production"],
|
||||
"description": "Declared environment. dev = autonomous; production = HITL gates."
|
||||
},
|
||||
"triggers": {
|
||||
"type": "object",
|
||||
"required": ["push", "pull_request"],
|
||||
"properties": {
|
||||
"push": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Branches that trigger the pipeline on push."
|
||||
},
|
||||
"pull_request": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Branches that trigger the pipeline on PR."
|
||||
}
|
||||
}
|
||||
},
|
||||
"runner": {
|
||||
"type": "string",
|
||||
"description": "Runner image (e.g. 'ubuntu-latest')."
|
||||
},
|
||||
"python_version": {
|
||||
"type": "string",
|
||||
"description": "Python version for setup-python action."
|
||||
},
|
||||
"stages": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"$ref": "#/$defs/stage"}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"stage": {
|
||||
"type": "object",
|
||||
"required": ["name", "command", "required"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Stage name."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The shell command to run for this stage."
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean",
|
||||
"description": "If true, a non-zero exit code fails the pipeline."
|
||||
},
|
||||
"install": {
|
||||
"type": "string",
|
||||
"description": "Optional: pip install command to run before the stage command."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Optional: human-readable description of what this stage does."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -1,31 +1,31 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://acdl.cloudinit.dev/schemas/ir.schema.json",
|
||||
"title": "ACDL Target Stack IR",
|
||||
"description": "Substrate-neutral description of a target stack: resources with typed inputs/outputs/NFRs, relationships (single parent per child), composition tree (max depth 5), and policy hooks. The L1 registry, L2 thin-composition tree, contract YML, and PolicyCheckResult schema are all defined against this IR. Substrate adapters (the Terraform adapter in v1) are the only substrate-specific code.",
|
||||
"$comment": "v1 ships one adapter (Terraform). The IR is nearly isomorphic to Terraform in v1 (ARCHITECTURE.md §12.1); the adapter compiles resource.module -> module block, resource.inputs -> variable + arg, resource.outputs -> output, relationship.kind=uses_output -> interpolation, relationship.kind=parent -> composition ordering hint. As more adapters appear (v2+), the IR gains expressiveness; the L1 content + contract YML + thin-composition tree do not change. The schema body is substrate-agnostic: no Terraform block keywords (variable/output/resource as blocks) and no aws_ provider prefixes in the schema keywords; type values are IR types (aws:s3:bucket), not Terraform resource types (aws_s3_bucket).",
|
||||
"$id": "https://acdl.cloudinit.dev/schemas/stack.schema.json",
|
||||
"title": "ACDL Target Stack",
|
||||
"description": "Substrate-neutral description of a target stack: resources with typed inputs/outputs/NFRs, relationships (single parent per child), composition tree (max depth 5), and policy hooks. The L1 registry, L2 composition tree, contract YML, and PolicyCheckResult schema are all defined against this stack schema. Substrate adapters (the Terraform adapter in v1) are the only substrate-specific code.",
|
||||
"$comment": "v1 ships one adapter (Terraform). The stack is nearly isomorphic to Terraform in v1 (ARCHITECTURE.md §12.1); the adapter compiles resource.module -> module block, resource.inputs -> variable + arg, resource.outputs -> output, relationship.kind=uses_output -> interpolation, relationship.kind=parent -> composition ordering hint. As more adapters appear (v2+), the stack gains expressiveness; the L1 content + contract YML + composition tree do not change. The schema body is substrate-agnostic: no Terraform block keywords (variable/output/resource as blocks) and no aws_ provider prefixes in the schema keywords; type values are stack types (aws:s3:bucket), not Terraform resource types (aws_s3_bucket).",
|
||||
"type": "object",
|
||||
"required": ["version", "stack", "resources"],
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "IR schema version (semver).",
|
||||
"description": "Stack schema version (semver).",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$"
|
||||
},
|
||||
"stack": {
|
||||
"type": "object",
|
||||
"description": "The L1/L2 stack identity this IR represents.",
|
||||
"description": "The L1/L2 stack identity this instance represents.",
|
||||
"required": ["name", "kind", "depth"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^l[12]-[a-z][a-z0-9-]*$",
|
||||
"description": "Stack name matching the L1/L2 folder name."
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Stack name matching the module folder name."
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["l1", "l2"],
|
||||
"description": "l1 = primitive; l2 = thin-composition."
|
||||
"description": "l1 = primitive; l2 = composition."
|
||||
},
|
||||
"depth": {
|
||||
"type": "integer",
|
||||
@@ -54,16 +54,16 @@
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*$",
|
||||
"description": "Local IR resource id (unique within the stack)."
|
||||
"description": "Local stack resource id (unique within the stack)."
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "IR-typed resource identifier (substrate-agnostic), e.g. 'aws:s3:bucket'. NOT a Terraform resource type ('aws_s3_bucket'); the adapter translates IR type -> substrate type."
|
||||
"description": "Stack-typed resource identifier (substrate-agnostic), e.g. 'aws:s3:bucket'. NOT a Terraform resource type ('aws_s3_bucket'); the adapter translates stack type -> substrate type."
|
||||
},
|
||||
"module": {
|
||||
"type": "string",
|
||||
"pattern": "^l1-[a-z][a-z0-9-]*@\\d+\\.\\d+\\.\\d+$",
|
||||
"description": "L1 registry reference: name@semver (W3.D). MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window."
|
||||
"pattern": "^[a-z][a-z0-9-]*@\\d+\\.\\d+\\.\\d+$",
|
||||
"description": "Module registry reference: name@semver (W3.D). MAJOR bumps require a new registry entry (immutable publication); old entry enters a 12-month deprecation window."
|
||||
},
|
||||
"parent": {
|
||||
"type": "string",
|
||||
@@ -71,7 +71,7 @@
|
||||
},
|
||||
"inputs": {
|
||||
"type": "object",
|
||||
"description": "Input values keyed by the L1 module's declared inputs. Free-form in v1 (validated at contract->IR resolution against the L1 registry); typed per-L1 in v1.2.",
|
||||
"description": "Input values keyed by the module's declared inputs. Free-form in v1 (validated at contract->stack resolution against the module registry); typed per-module in v1.2.",
|
||||
"additionalProperties": {"type": ["string", "number", "boolean"]}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -92,7 +92,7 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "IR-typed output type: a primitive ('string', 'arn') or a reference ('ref:<resourceId>.<outputName>')."
|
||||
"description": "Stack-typed output type: a primitive ('string', 'arn') or a reference ('ref:<resourceId>.<outputName>')."
|
||||
},
|
||||
"description": {"type": "string"}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ banner "Stage 1/3: lint (py_compile)"
|
||||
python3 -m py_compile \
|
||||
acdl_platform/confidence_signal.py \
|
||||
acdl_platform/outbox_writer.py \
|
||||
acdl_platform/contract_resolver.py \
|
||||
adapters/terraform/adapter.py \
|
||||
adapters/terraform/policy/checkov_adapter.py \
|
||||
scripts/push_consumer_image.py \
|
||||
|
||||
@@ -40,13 +40,11 @@ closes D-034 by having the user manually rotate the root key afterward.
|
||||
```
|
||||
Optionally uploads to Gitea Actions secrets if `ACDL_GITEA_TOKEN` is set.
|
||||
|
||||
5. **Verify**:
|
||||
```bash
|
||||
bash scripts/verify_phase08.sh
|
||||
```
|
||||
Asserts: caller identity is `acdl-spike-runner` (not root); S3 bucket +
|
||||
DynamoDB table + IAM user + scoped policy all exist; `.env.secrets` +
|
||||
`.bootstrap_state.json` are gitignored.
|
||||
5. **Verify** (manual): confirm the caller identity is `acdl-spike-runner`
|
||||
(not root); the S3 bucket + DynamoDB table + IAM user + scoped policy
|
||||
all exist; `.env.secrets` + `.bootstrap_state.json` are gitignored.
|
||||
(`scripts/verify_phase08.sh` was the automated gate; it has been
|
||||
removed along with all other per-phase verify scripts.)
|
||||
|
||||
6. **MANUAL — D-034 closure:** rotate/deactivate the **root** key in the
|
||||
AWS IAM console (the user does this, not the script). The bootstrap
|
||||
|
||||
@@ -8,7 +8,7 @@ terraform {
|
||||
}
|
||||
backend "s3" {
|
||||
bucket = "acdl-tfstate-581513795199-us-east-1"
|
||||
key = "spike/l2-microservice/terraform.tfstate"
|
||||
key = "spike/microservice/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ terraform {
|
||||
}
|
||||
backend "s3" {
|
||||
bucket = "acdl-tfstate-581513795199-us-east-1"
|
||||
key = "spike/l1-s3/terraform.tfstate"
|
||||
key = "spike/static-asset/terraform.tfstate"
|
||||
region = "us-east-1"
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -15,18 +15,18 @@ def repo_root():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spike_ir():
|
||||
return json.load(open(ROOT / "modules-ir/l1/l1-s3/spike_instance.json"))
|
||||
def stack_instance():
|
||||
return json.load(open(ROOT / "modules/l1/s3/instance.json"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ir_schema():
|
||||
return json.load(open(ROOT / "schemas/ir.schema.json"))
|
||||
def stack_schema():
|
||||
return json.load(open(ROOT / "schemas/stack.schema.json"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registry():
|
||||
return json.load(open(ROOT / "modules-ir/registry.json"))
|
||||
return json.load(open(ROOT / "modules/registry.json"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
+44
-38
@@ -15,38 +15,44 @@ from adapters.terraform.adapter import (
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class TestSpikeInstance:
|
||||
def test_spike_instance_validates_against_ir_schema(self, spike_ir, ir_schema):
|
||||
jsonschema.validate(spike_ir, ir_schema)
|
||||
class TestInstance:
|
||||
def test_instance_validates_against_stack_schema(self, stack_instance, stack_schema):
|
||||
jsonschema.validate(stack_instance, stack_schema)
|
||||
|
||||
def test_spike_instance_has_one_resource(self, spike_ir):
|
||||
assert len(spike_ir["resources"]) == 1
|
||||
r = spike_ir["resources"][0]
|
||||
def test_instance_has_one_resource(self, stack_instance):
|
||||
assert len(stack_instance["resources"]) == 1
|
||||
r = stack_instance["resources"][0]
|
||||
assert r["id"] == "s3"
|
||||
assert r["type"] == "aws:s3:bucket"
|
||||
|
||||
def test_spike_instance_stack_is_l1_s3(self, spike_ir):
|
||||
assert spike_ir["stack"]["name"] == "l1-s3"
|
||||
assert spike_ir["stack"]["kind"] == "l1"
|
||||
def test_instance_stack_is_s3(self, stack_instance):
|
||||
assert stack_instance["stack"]["name"] == "s3"
|
||||
assert stack_instance["stack"]["kind"] == "l1"
|
||||
|
||||
|
||||
class TestRegistry:
|
||||
def test_registry_has_7_l1_entries(self, registry):
|
||||
assert len(registry) == 7
|
||||
for key in registry:
|
||||
assert key.startswith("l1-")
|
||||
EXPECTED_L1_KEYS = {"s3", "vpc", "ecs-cluster", "ecs-service", "iam-role", "alb", "ecr"}
|
||||
EXPECTED_L2_KEYS = {"static-asset", "microservice"}
|
||||
|
||||
def test_registry_has_no_l2_entries(self, registry):
|
||||
l2 = [k for k in registry if k.startswith("l2")]
|
||||
assert l2 == []
|
||||
def test_registry_has_9_entries(self, registry):
|
||||
assert len(registry) == 9
|
||||
assert set(registry.keys()) == (self.EXPECTED_L1_KEYS | self.EXPECTED_L2_KEYS)
|
||||
|
||||
def test_registry_has_7_l1_entries(self, registry):
|
||||
l1 = {k for k in registry if registry[k]["1.0.0"]["interface"].startswith("modules/l1/")}
|
||||
assert l1 == self.EXPECTED_L1_KEYS
|
||||
|
||||
def test_registry_has_2_l2_entries(self, registry):
|
||||
l2 = {k for k in registry if registry[k]["1.0.0"]["interface"].startswith("modules/l2/")}
|
||||
assert l2 == self.EXPECTED_L2_KEYS
|
||||
|
||||
def test_all_l1_interfaces_exist(self, registry, repo_root):
|
||||
for name, versions in registry.items():
|
||||
for ver, entry in versions.items():
|
||||
iface_path = os.path.join(repo_root, entry["interface"])
|
||||
assert os.path.isfile(iface_path), f"{iface_path} missing"
|
||||
iface = json.load(open(iface_path))
|
||||
assert iface["name"] == name
|
||||
for name in self.EXPECTED_L1_KEYS:
|
||||
entry = registry[name]["1.0.0"]
|
||||
iface_path = os.path.join(repo_root, entry["interface"])
|
||||
assert os.path.isfile(iface_path), f"{iface_path} missing"
|
||||
iface = json.load(open(iface_path))
|
||||
assert iface["name"] == name
|
||||
|
||||
|
||||
class TestTypeMap:
|
||||
@@ -119,56 +125,56 @@ class TestRefExpr:
|
||||
assert result == "aws_vpc.vpc.id"
|
||||
|
||||
def test_unknown_id_raises(self):
|
||||
with pytest.raises(ValueError, match="unknown IR resource id"):
|
||||
with pytest.raises(ValueError, match="unknown stack resource id"):
|
||||
_ref_expr("ref:nonexistent.output", {"s3": "aws:s3:bucket"})
|
||||
|
||||
|
||||
class TestAdapt:
|
||||
def test_adapt_emits_three_files(self, spike_ir, tmp_path):
|
||||
def test_adapt_emits_three_files(self, stack_instance, tmp_path):
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(spike_ir, out_dir)
|
||||
adapt(stack_instance, out_dir)
|
||||
assert os.path.isfile(os.path.join(out_dir, "main.tf"))
|
||||
assert os.path.isfile(os.path.join(out_dir, "terraform.tf"))
|
||||
assert os.path.isfile(os.path.join(out_dir, "providers.tf"))
|
||||
|
||||
def test_main_tf_has_s3_bucket(self, spike_ir, tmp_path):
|
||||
def test_main_tf_has_s3_bucket(self, stack_instance, tmp_path):
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(spike_ir, out_dir)
|
||||
adapt(stack_instance, out_dir)
|
||||
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||
assert 'resource "aws_s3_bucket" "s3"' in main_tf
|
||||
assert 'bucket = "acdl-spike-bucket"' in main_tf
|
||||
|
||||
def test_main_tf_has_versioning(self, spike_ir, tmp_path):
|
||||
def test_main_tf_has_versioning(self, stack_instance, tmp_path):
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(spike_ir, out_dir)
|
||||
adapt(stack_instance, out_dir)
|
||||
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||
assert "versioning" in main_tf
|
||||
assert "enabled = true" in main_tf
|
||||
|
||||
def test_main_tf_has_outputs(self, spike_ir, tmp_path):
|
||||
def test_main_tf_has_outputs(self, stack_instance, tmp_path):
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(spike_ir, out_dir)
|
||||
adapt(stack_instance, out_dir)
|
||||
main_tf = open(os.path.join(out_dir, "main.tf")).read()
|
||||
assert 'output "bucket_arn"' in main_tf
|
||||
assert 'output "bucket_name"' in main_tf
|
||||
|
||||
def test_terraform_tf_has_backend(self, spike_ir, tmp_path):
|
||||
def test_terraform_tf_has_backend(self, stack_instance, tmp_path):
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(spike_ir, out_dir)
|
||||
adapt(stack_instance, out_dir)
|
||||
terraform_tf = open(os.path.join(out_dir, "terraform.tf")).read()
|
||||
assert 'backend "s3"' in terraform_tf
|
||||
assert 'required_version' in terraform_tf
|
||||
assert ">= 1.9" in terraform_tf
|
||||
|
||||
def test_providers_tf_has_aws(self, spike_ir, tmp_path):
|
||||
def test_providers_tf_has_aws(self, stack_instance, tmp_path):
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(spike_ir, out_dir)
|
||||
adapt(stack_instance, out_dir)
|
||||
providers_tf = open(os.path.join(out_dir, "providers.tf")).read()
|
||||
assert 'provider "aws"' in providers_tf
|
||||
assert "us-east-1" in providers_tf
|
||||
|
||||
def test_backend_key_uses_stack_name(self, spike_ir, tmp_path):
|
||||
def test_backend_key_uses_stack_name(self, stack_instance, tmp_path):
|
||||
out_dir = str(tmp_path / "tf_out")
|
||||
adapt(spike_ir, out_dir)
|
||||
adapt(stack_instance, out_dir)
|
||||
terraform_tf = open(os.path.join(out_dir, "terraform.tf")).read()
|
||||
assert "spike/l1-s3/terraform.tfstate" in terraform_tf
|
||||
assert "spike/s3/terraform.tfstate" in terraform_tf
|
||||
@@ -82,14 +82,14 @@ class TestPerInputScore:
|
||||
|
||||
def test_validation_all_true(self):
|
||||
score, reasons = _per_input_score("validation", {
|
||||
"schema": True, "ir_resolved": True,
|
||||
"schema": True, "stack_resolved": True,
|
||||
"tf_validated": True, "tf_planned": True
|
||||
})
|
||||
assert score == 1.0
|
||||
|
||||
def test_validation_partial(self):
|
||||
score, reasons = _per_input_score("validation", {
|
||||
"schema": True, "ir_resolved": True,
|
||||
"schema": True, "stack_resolved": True,
|
||||
"tf_validated": False, "tf_planned": False
|
||||
})
|
||||
assert score == 0.5
|
||||
@@ -131,7 +131,7 @@ class TestCompute:
|
||||
def _base_inputs(self):
|
||||
return {
|
||||
"policy": [{"result": "pass"}],
|
||||
"validation": {"schema": True, "ir_resolved": True,
|
||||
"validation": {"schema": True, "stack_resolved": True,
|
||||
"tf_validated": True, "tf_planned": True},
|
||||
"freshness": {"age_days": 0, "max_age_days": 7},
|
||||
"source": {"submitter": "dev", "commit_sha": "abc"},
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import jsonschema
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class TestContractSchema:
|
||||
def test_schema_is_valid_json_schema(self):
|
||||
schema = json.load(open(ROOT / "schemas/contract.schema.json"))
|
||||
jsonschema.Draft202012Validator.check_schema(schema)
|
||||
|
||||
def test_schema_requires_uses_module_environment_inputs(self):
|
||||
schema = json.load(open(ROOT / "schemas/contract.schema.json"))
|
||||
for field in ["uses", "module", "environment", "inputs"]:
|
||||
assert field in schema["required"]
|
||||
|
||||
|
||||
class TestResolveStaticAsset:
|
||||
def test_resolve_static_asset_contract(self, tmp_path):
|
||||
from acdl_platform.contract_resolver import resolve
|
||||
stack = resolve(str(ROOT / "contracts/static-asset.yaml"), str(ROOT))
|
||||
assert stack["stack"]["name"] == "static-asset"
|
||||
assert stack["stack"]["kind"] == "l2"
|
||||
assert stack["stack"]["depth"] == 1
|
||||
assert len(stack["resources"]) >= 1
|
||||
|
||||
def test_resolve_static_asset_has_s3_resource(self):
|
||||
from acdl_platform.contract_resolver import resolve
|
||||
stack = resolve(str(ROOT / "contracts/static-asset.yaml"), str(ROOT))
|
||||
s3_res = [r for r in stack["resources"] if r["type"] == "aws:s3:bucket"]
|
||||
assert len(s3_res) == 1
|
||||
assert s3_res[0]["inputs"]["bucket_name"] == "acdl-spike-bucket"
|
||||
assert s3_res[0]["inputs"]["region"] == "us-east-1"
|
||||
|
||||
def test_resolve_static_asset_validates_against_stack_schema(self):
|
||||
from acdl_platform.contract_resolver import resolve
|
||||
stack = resolve(str(ROOT / "contracts/static-asset.yaml"), str(ROOT))
|
||||
schema = json.load(open(ROOT / "schemas/stack.schema.json"))
|
||||
jsonschema.validate(stack, schema)
|
||||
|
||||
|
||||
class TestResolveMicroservice:
|
||||
def test_resolve_microservice_contract(self):
|
||||
contract = {
|
||||
"uses": "acdl/pipelines/deploy.yaml@v1",
|
||||
"module": "microservice",
|
||||
"environment": "dev",
|
||||
"inputs": {
|
||||
"image": "581513795199.dkr.ecr.us-east-1.amazonaws.com/acdl-microservice:latest",
|
||||
"port": 8080,
|
||||
"region": "us-east-1",
|
||||
},
|
||||
}
|
||||
contract_path = ROOT / "contracts" / "test-microservice.yaml"
|
||||
with open(contract_path, "w") as fh:
|
||||
yaml.dump(contract, fh)
|
||||
try:
|
||||
from acdl_platform.contract_resolver import resolve
|
||||
stack = resolve(str(contract_path), str(ROOT))
|
||||
assert stack["stack"]["name"] == "microservice"
|
||||
assert stack["stack"]["kind"] == "l2"
|
||||
assert len(stack["resources"]) >= 6
|
||||
finally:
|
||||
os.remove(contract_path)
|
||||
|
||||
|
||||
class TestResolveL1Direct:
|
||||
def test_resolve_s3_direct(self, tmp_path):
|
||||
contract = {
|
||||
"uses": "acdl/pipelines/deploy.yaml@v1",
|
||||
"module": "s3",
|
||||
"environment": "dev",
|
||||
"inputs": {
|
||||
"bucket_name": "my-test-bucket",
|
||||
"region": "us-east-1",
|
||||
},
|
||||
}
|
||||
contract_path = tmp_path / "test-s3.yaml"
|
||||
with open(contract_path, "w") as fh:
|
||||
yaml.dump(contract, fh)
|
||||
from acdl_platform.contract_resolver import resolve
|
||||
stack = resolve(str(contract_path), str(ROOT))
|
||||
assert stack["stack"]["name"] == "s3"
|
||||
assert stack["stack"]["kind"] == "l1"
|
||||
assert len(stack["resources"]) == 1
|
||||
assert stack["resources"][0]["type"] == "aws:s3:bucket"
|
||||
assert stack["resources"][0]["inputs"]["bucket_name"] == "my-test-bucket"
|
||||
|
||||
def test_resolve_s3_validates_against_stack_schema(self, tmp_path):
|
||||
contract = {
|
||||
"uses": "acdl/pipelines/deploy.yaml@v1",
|
||||
"module": "s3",
|
||||
"environment": "dev",
|
||||
"inputs": {"bucket_name": "test", "region": "us-east-1"},
|
||||
}
|
||||
contract_path = tmp_path / "test-s3-schema.yaml"
|
||||
with open(contract_path, "w") as fh:
|
||||
yaml.dump(contract, fh)
|
||||
from acdl_platform.contract_resolver import resolve
|
||||
stack = resolve(str(contract_path), str(ROOT))
|
||||
schema = json.load(open(ROOT / "schemas/stack.schema.json"))
|
||||
jsonschema.validate(stack, schema)
|
||||
|
||||
|
||||
class TestResolveErrors:
|
||||
def test_unknown_module_raises(self, tmp_path):
|
||||
contract = {
|
||||
"uses": "acdl/pipelines/deploy.yaml@v1",
|
||||
"module": "nonexistent",
|
||||
"environment": "dev",
|
||||
"inputs": {},
|
||||
}
|
||||
contract_path = tmp_path / "bad.yaml"
|
||||
with open(contract_path, "w") as fh:
|
||||
yaml.dump(contract, fh)
|
||||
from acdl_platform.contract_resolver import resolve
|
||||
with pytest.raises(ValueError, match="not found in registry"):
|
||||
resolve(str(contract_path), str(ROOT))
|
||||
|
||||
def test_missing_required_field_fails_validation(self, tmp_path):
|
||||
contract = {"uses": "acdl/pipelines/deploy.yaml@v1", "module": "s3"}
|
||||
contract_path = tmp_path / "incomplete.yaml"
|
||||
with open(contract_path, "w") as fh:
|
||||
yaml.dump(contract, fh)
|
||||
from acdl_platform.contract_resolver import resolve
|
||||
with pytest.raises(jsonschema.ValidationError):
|
||||
resolve(str(contract_path), str(ROOT))
|
||||
|
||||
|
||||
class TestDeployPipelineContract:
|
||||
def test_deploy_pipeline_validates_against_schema(self):
|
||||
schema = json.load(open(ROOT / "schemas/deploy-pipeline.schema.json"))
|
||||
with open(ROOT / "pipelines/deploy.yaml") as fh:
|
||||
contract = yaml.safe_load(fh)
|
||||
jsonschema.validate(contract, schema)
|
||||
|
||||
def test_deploy_pipeline_has_six_stages(self):
|
||||
with open(ROOT / "pipelines/deploy.yaml") as fh:
|
||||
contract = yaml.safe_load(fh)
|
||||
stage_names = [s["name"] for s in contract["stages"]]
|
||||
assert "validate-contract" in stage_names
|
||||
assert "resolve-stack" in stage_names
|
||||
assert "terraform-plan" in stage_names
|
||||
assert "checkov" in stage_names
|
||||
assert "confidence" in stage_names
|
||||
assert "apply" in stage_names
|
||||
@@ -41,7 +41,7 @@ class TestWriteEvent:
|
||||
"eventType": "CONFIDENCE_COMPUTED",
|
||||
"ts": "2026-07-22T00:00:00Z",
|
||||
"environment": "dev",
|
||||
"stack": "l1-s3",
|
||||
"stack": "s3",
|
||||
"score": 0.85,
|
||||
"band": "pass",
|
||||
"prev_event_hash": "GENESIS",
|
||||
|
||||
@@ -10,14 +10,14 @@ ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class TestPipelineIntegration:
|
||||
def test_load_ir_and_adapt_offline(self, tmp_path):
|
||||
ir = json.load(open(ROOT / "modules-ir/l1/l1-s3/spike_instance.json"))
|
||||
assert ir["stack"]["name"] == "l1-s3"
|
||||
def test_load_stack_and_adapt_offline(self, tmp_path):
|
||||
stack = json.load(open(ROOT / "modules/l1/s3/instance.json"))
|
||||
assert stack["stack"]["name"] == "s3"
|
||||
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from adapters.terraform.adapter import adapt
|
||||
out_dir = str(tmp_path / "tf")
|
||||
adapt(ir, out_dir)
|
||||
adapt(stack, out_dir)
|
||||
|
||||
assert os.path.isfile(os.path.join(out_dir, "main.tf"))
|
||||
assert os.path.isfile(os.path.join(out_dir, "terraform.tf"))
|
||||
@@ -33,7 +33,7 @@ class TestPipelineIntegration:
|
||||
|
||||
inputs = {
|
||||
"policy": [{"result": "pass"}],
|
||||
"validation": {"schema": True, "ir_resolved": True,
|
||||
"validation": {"schema": True, "stack_resolved": True,
|
||||
"tf_validated": True, "tf_planned": True},
|
||||
"freshness": {"age_days": 0, "max_age_days": 7},
|
||||
"source": {"submitter": "test", "commit_sha": "test-sha"},
|
||||
|
||||
Reference in New Issue
Block a user