Parent-Child Resources
Some resources can only exist within the context of a parent resource. For example, an IAM RolePolicy belongs to an IAM Role, or an ECS TaskSet belongs to a Service. formae models these relationships to enable tiered discovery and correct ordering of create/destroy operations.
Why Parent-Child Matters
Parent-child relationships exist primarily for discovery. To list child resources, you need the parent's identifier. formae discovers parents first, then uses their properties to discover children.
When formae discovers a child resource, it automatically injects a resolvable reference from the child to its parent. This resolvable reference feeds into formae's dependency DAG, which ensures correct ordering during apply and destroy operations. See Resolvables for details on how the DAG determines update ordering.
formae handles discovery ordering and resolvable injection automatically when you declare the relationship correctly.
Declaring a Parent-Child Relationship
Use the parent and listParam fields in your @ResourceHint:
import "@formae/formae.pkl"
import "../myplugin.pkl"
const type = "MyPlugin::IAM::RolePolicy"
@myplugin.ResourceHint {
type = module.type
identifier = "PolicyName"
// This resource's parent
parent = "MyPlugin::IAM::Role"
// How to query children from the parent
listParam = new formae.ListProperty {
parentProperty = "RoleName" // Property in the parent resource
listParameter = "RoleName" // Parameter name your List API expects
}
}
open class RolePolicy extends formae.Resource {
@myplugin.FieldHint {}
policyName: String
// Accept either a string or a reference to the parent
@myplugin.FieldHint { createOnly = true }
roleName: String|formae.Resolvable
}
The ListProperty Class
formae.ListProperty maps parent properties to List API parameters:
open class ListProperty {
// Name of the property in the parent resource that contains the value
hidden parentProperty: String
// Name of the parameter your plugin's List operation expects
hidden listParameter: String
}
Example: For IAM RolePolicies:
- parentProperty = "RoleName" → Get the RoleName from the discovered Role
- listParameter = "RoleName" → Pass it to the List API as the RoleName parameter
How Tiered Discovery Works
When formae runs discovery:
-
Discover root resources: Resources without a
parentare discovered first (VPCs, Roles, S3 Buckets) -
Extract parent properties: For each discovered parent, formae extracts the
parentPropertyvalue -
Discover children: formae calls your plugin's
List()operation with the parent's property value inAdditionalProperties -
Recurse: If children have their own children, the process continues
VPC (discovered)
└─ VpcId: "vpc-123"
│
▼ List(AdditionalProperties: {"VpcId": "vpc-123"})
│
Subnet (discovered)
└─ SubnetId: "subnet-456"
│
▼ List(AdditionalProperties: {"SubnetId": "subnet-456"})
│
NetworkInterface (discovered)
Automatic Resolvable Injection
When formae discovers a child resource, it automatically creates a resolvable reference to the parent:
{
"SubnetId": "subnet-456",
"VpcId": {
"$ref": "formae://2ABC123#/VpcId",
"$value": "vpc-123"
}
}
This resolvable reference is added to formae's dependency DAG, which determines update ordering: - Apply: VPC is created before Subnet (dependency first) - Destroy: Subnet is destroyed before VPC (dependent first)
The ordering mechanism is the same as any other resolvable—parent-child just automates the injection during discovery.
Multi-Level Hierarchies
Parent-child relationships can be nested to any depth:
// VPC - root resource (no parent)
@aws.ResourceHint {
type = "AWS::EC2::VPC"
identifier = "VpcId"
}
// Subnet - child of VPC
@aws.ResourceHint {
type = "AWS::EC2::Subnet"
identifier = "SubnetId"
parent = "AWS::EC2::VPC"
listParam = new formae.ListProperty {
parentProperty = "VpcId"
listParameter = "VpcId"
}
}
// NetworkInterface - child of Subnet
@aws.ResourceHint {
type = "AWS::EC2::NetworkInterface"
identifier = "NetworkInterfaceId"
parent = "AWS::EC2::Subnet"
listParam = new formae.ListProperty {
parentProperty = "SubnetId"
listParameter = "SubnetId"
}
}
Discovery order: VPC → Subnet → NetworkInterface (parents before children)
Update ordering (via injected resolvables): - Apply: VPC → Subnet → NetworkInterface - Destroy: NetworkInterface → Subnet → VPC
Multiple List Parameters
Some resources require multiple parent properties to list. Use a list of ListProperty:
@myplugin.ResourceHint {
type = "MyPlugin::ECS::TaskSet"
identifier = "Id"
parent = "MyPlugin::ECS::Service"
listParam = new Listing {
new formae.ListProperty {
parentProperty = "ServiceName"
listParameter = "Service"
}
new formae.ListProperty {
parentProperty = "Cluster"
listParameter = "Cluster"
}
}
}
Your plugin's List() receives both parameters in AdditionalProperties.
Plugin Implementation
Your plugin's List() operation receives parent properties in request.AdditionalProperties:
func (p *Plugin) List(ctx context.Context, req *resource.ListRequest) (*resource.ListResult, error) {
// For child resources, AdditionalProperties contains parent values
// e.g., {"RoleName": "my-role"} for listing RolePolicies
if len(req.AdditionalProperties) > 0 {
// Use these to filter your API call
roleName := req.AdditionalProperties["RoleName"]
// Call your backend API with this filter
}
// Return discovered resources
}
When to Use Parent-Child
Use parent-child relationships when:
| Scenario | Example |
|---|---|
| API requires parent ID to list | IAM RolePolicies need RoleName |
| Resources are scoped to a parent | Subnets exist within a VPC |
| Destroying parent should destroy children | Deleting a Role deletes its policies |
Don't use parent-child for: - Simple references (Subnet references VPC but isn't "inside" it conceptually) - Resources that can exist independently - Resources where you don't need tiered discovery
Common Patterns
AWS CloudControl Resources
Many AWS resources follow the parent-child pattern:
// EFS Mount Target (child of FileSystem)
@aws.ResourceHint {
type = "AWS::EFS::MountTarget"
identifier = "MountTargetId"
parent = "AWS::EFS::FileSystem"
listParam = new formae.ListProperty {
parentProperty = "FileSystemId"
listParameter = "FileSystemId"
}
}
Resources Without Discovery
Some child resources can't be discovered (API doesn't support listing):
@aws.ResourceHint {
type = "AWS::EC2::Route"
identifier = "Ref"
discoverable = false // No List API available
}
These resources must be defined in Forma files; they won't be auto-discovered.
Summary
| Field | Purpose |
|---|---|
parent |
Parent resource type (e.g., "AWS::IAM::Role") |
listParam.parentProperty |
Which property to read from parent |
listParam.listParameter |
What to call it in List request |
Declare parent-child relationships to enable tiered discovery and automatic ordering. formae handles the complexity of discovering in the right order and injecting references automatically.