diff --git a/adapters/terraform/adapter.py b/adapters/terraform/adapter.py index 5397e38..abba509 100644 --- a/adapters/terraform/adapter.py +++ b/adapters/terraform/adapter.py @@ -68,7 +68,7 @@ INPUT_MAP = { OUTPUT_MAP = { "aws:s3:bucket": {"bucket_arn": "arn", "bucket_name": "id"}, "aws:ec2:vpc": {"vpc_id": "id"}, - "aws:ec2:subnet": {"subnet_id": "id"}, + "aws:ec2:subnet": {"subnet_ids": "id", "subnet_id": "id"}, "aws:ec2:routetable": {}, "aws:ecs:cluster": {"cluster_arn": "arn", "cluster_id": "id"}, "aws:ecs:task_definition": {"task_def_arn": "arn"}, diff --git a/core/contract_resolver.py b/core/contract_resolver.py index c8a0995..c29c43d 100644 --- a/core/contract_resolver.py +++ b/core/contract_resolver.py @@ -44,7 +44,13 @@ def _resolve_wire_value(wire, contract_inputs, child_outputs): - ".outputs." — a reference to another child's output Returns either a concrete value (string/number/boolean) or a - "ref:." string for cross-child references. + "ref:." string for cross-child references. + + For multi-resource L1s (e.g. vpc which expands to vpc-vpc, vpc-subnet, + vpc-routetable), the ref must point to the sub-resource that actually + produces the output, not the child id. The child_outputs table maps + childId -> {outputName -> resourceId} so the ref uses the correct + resource id. """ from_expr = wire["from"] to_expr = wire["to"] @@ -66,7 +72,13 @@ def _resolve_wire_value(wire, contract_inputs, child_outputs): if len(parts) >= 3 and parts[1] == "outputs": child_id = parts[0] output_name = parts[2] - return f"ref:{child_id}.{output_name}" + # Look up the sub-resource that produces this output. + # child_outputs[child_id] is a dict {outputName -> resourceId}. + # If the child is a single-resource L1, the resourceId == child_id. + # If multi-resource, the resourceId is the expanded sub-resource id. + child_out_map = child_outputs.get(child_id, {}) + resource_id = child_out_map.get(output_name, child_id) + return f"ref:{resource_id}.{output_name}" return None @@ -125,6 +137,9 @@ def resolve_l2(contract, registry, repo_root): composition = _load_json(comp_path) # Track child outputs for wire resolution + # child_outputs[childId] = {outputName: resourceId} + # For single-resource L1s, resourceId == childId + # For multi-resource L1s, resourceId is the expanded sub-resource id child_outputs = {} resources = [] @@ -139,15 +154,18 @@ def resolve_l2(contract, registry, repo_root): child_iface_path = os.path.join(repo_root, child_entry["interface"]) child_iface = _load_json(child_iface_path) + # Build the output->resourceId map for this child + child_out_map = {} + # 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"]: + res_id = f"{child_id}-{sub_res['type'].split(':')[-1].replace('_', '-')}" if len(child_iface["resources"]) > 1 else child_id resource = { - "id": f"{child_id}-{sub_res['type'].split(':')[-1].replace('_', '-')}" - if len(child_iface["resources"]) > 1 else child_id, + "id": res_id, "type": sub_res["type"], "module": child_module, "inputs": {}, @@ -157,6 +175,9 @@ def resolve_l2(contract, registry, repo_root): }, } resources.append(resource) + # Map each output to this sub-resource's id + for out_name in sub_res.get("outputs", []): + child_out_map[out_name] = res_id else: # Single-resource L1 resource = { @@ -170,9 +191,17 @@ def resolve_l2(contract, registry, repo_root): }, } resources.append(resource) + # Map each output to the child id + for out_name in child_iface.get("outputs", {}): + child_out_map[out_name] = child_id - # Track outputs for this child - child_outputs[child_id] = child_iface.get("outputs", {}) + # Also map interface-level outputs (for L1s that declare outputs at the + # interface level rather than per-resource) + for out_name in child_iface.get("outputs", {}): + if out_name not in child_out_map: + child_out_map[out_name] = child_id + + child_outputs[child_id] = child_out_map # Resolve wires to populate inputs for wire in composition.get("wires", []): diff --git a/modules/l1/vpc/interface.json b/modules/l1/vpc/interface.json index 15b9d7c..8e79c97 100644 --- a/modules/l1/vpc/interface.json +++ b/modules/l1/vpc/interface.json @@ -48,7 +48,7 @@ "type": "aws:ec2:subnet", "description": "One subnet per availability zone (azs split on comma).", "inputs": ["cidr", "az", "vpc_id", "name"], - "outputs": ["subnet_id"] + "outputs": ["subnet_ids"] }, { "type": "aws:ec2:routetable",