From 4c368ea495bdfe03e29a56df07b54e4e399de5b4 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Tue, 25 Aug 2026 03:40:29 -0500 Subject: [PATCH 1/4] feat: support GUA and subnet IPAM allocation --- README.md | 19 ++- apis/networks/definition.yaml | 52 ++++++++- examples/networks/with-ipam.yaml | 15 +-- functions/render/000-state-init.yaml.gotmpl | 9 ++ .../render/005-state-network.yaml.gotmpl | 19 +-- .../007-state-network-subnets.yaml.gotmpl | 108 +++++++++++++++++- functions/render/009-state-status.yaml.gotmpl | 13 ++- functions/render/10-vpc.yaml.gotmpl | 11 +- functions/render/50-subnets.yaml.gotmpl | 25 +++- functions/render/999-status.yaml.gotmpl | 8 +- tests/test-render/main.k | 31 ++++- 11 files changed, 270 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index ee975ff..54f441d 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,15 @@ spec: enabled: true poolId: ipam-pool-0123456789abcdef0 netmaskLength: 16 - ipv6Ula: + ipv6Gua: enabled: true poolId: ipam-pool-0fedcba9876543210 netmaskLength: 56 subnetLayout: + # VPC resource-planning pools, not the regional VPC allocation pools above + ipv4PoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 + ipv6PoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 + ipv6NetmaskLength: 64 availabilityZones: [a, b, c] public: enabled: true @@ -93,6 +97,10 @@ This saves ~$32/mo. Add NAT later if you need IPv4 egress to external services. - **No conflicts** - IPAM prevents overlapping ranges across VPCs - **Multi-account ready** - Share pools via RAM when you scale +For Amazon-provided IPv6 GUA, use a Regional IPAM pool (for example `/52`) to +allocate a `/56` to each VPC. Subnets then allocate `/64`s from a VPC +resource-planning pool whose source resource is that VPC. + ### IPv6 Benefits - **EKS Auto Mode** - IPv6 prevents IP exhaustion when scaling - **Future-proof** - Native dual-stack from day one @@ -445,11 +453,17 @@ spec: | `ipv6Ula.enabled` | boolean | Enable IPv6 ULA CIDR allocation from IPAM | | `ipv6Ula.poolId` | string | IPAM pool ID for IPv6 | | `ipv6Ula.netmaskLength` | int | VPC IPv6 netmask (default: 56) | +| `ipv6Gua.enabled` | boolean | Enable IPv6 GUA allocation from a regional IPAM pool | +| `ipv6Gua.poolId` | string | Regional Amazon-provided or BYOIP IPv6 IPAM pool ID | +| `ipv6Gua.netmaskLength` | int | VPC IPv6 netmask (default: 56) | ### spec.subnetLayout | Field | Type | Description | |-------|------|-------------| +| `ipv4PoolId` | string | IPv4 VPC resource-planning IPAM pool ID | +| `ipv6PoolId` | string | IPv6 VPC resource-planning IPAM pool ID | +| `ipv6NetmaskLength` | int | IPv6 subnet netmask (default: 64) | | `availabilityZones` | []string | AZs for subnet creation (default: [a, b, c]) | | `public.enabled` | boolean | Create public subnets (default: true) | | `public.netmaskLength` | int | Public subnet netmask (default: 24) | @@ -493,6 +507,8 @@ status: cidr: "10.100.0.0/16" ipv6Ula: cidr: "fd00:dead:beef::/56" + ipv6Gua: + cidr: "2600:1f18:abc::/56" network: name: my-network region: us-east-1 @@ -500,6 +516,7 @@ status: cidr: ipv4: "10.100.0.0/16" ipv6Ula: "fd00:dead:beef::/56" + ipv6Gua: "2600:1f18:abc::/56" ipv6AmazonProvided: "2600:1f18:abc::/56" # If using Amazon-provided availabilityZones: - us-east-1a diff --git a/apis/networks/definition.yaml b/apis/networks/definition.yaml index a5efd6e..3ceb3e1 100644 --- a/apis/networks/definition.yaml +++ b/apis/networks/definition.yaml @@ -63,12 +63,12 @@ spec: description: Kind of the provider config reference. default: ProviderConfig - # IPAM - allocate VPC CIDR from IPAM pool, subnets calculated via cidrmath + # IPAM - allocate VPC CIDRs from regional IPAM pools ipam: type: object description: | - Enable automatic VPC CIDR allocation from an IPAM pool. - Subnet CIDRs are calculated from the VPC CIDR using cidrmath. + Enable automatic VPC CIDR allocation from regional IPAM pools. + Subnet allocation is configured separately through subnetLayout. properties: ipv4: type: object @@ -100,14 +100,47 @@ spec: type: integer description: Netmask length for VPC IPv6 CIDR. AWS requires /56. default: 56 + ipv6Gua: + type: object + description: IPv6 global unicast address (GUA) VPC allocation from IPAM. + properties: + enabled: + type: boolean + description: Enable IPv6 GUA CIDR allocation from a regional IPAM pool. + default: false + poolId: + type: string + description: Regional Amazon-provided or BYOIP IPv6 IPAM pool ID. + netmaskLength: + type: integer + description: Netmask length allocated to the VPC (typically /56). + default: 56 # Subnet layout - used with both IPAM and manual VPC CIDR subnetLayout: type: object description: | - Subnet layout configuration. When using IPAM, subnets are - automatically calculated from the VPC CIDR using cidrmath. + Subnet layout configuration. When ipv4PoolId is set, subnets + allocate IPv4 CIDRs from a VPC resource-planning IPAM pool. + Otherwise, IPv4 CIDRs are calculated from the VPC CIDR. When + ipv6PoolId is set, each subnet allocates an IPv6 CIDR from its + VPC resource-planning pool. properties: + ipv4PoolId: + type: string + description: | + IPv4 VPC resource-planning IPAM pool ID whose source resource + is this VPC. Do not use the regional pool that allocates VPC CIDRs. + ipv6PoolId: + type: string + description: | + IPv6 VPC resource-planning IPAM pool ID whose source resource + is this VPC. Do not use the regional /52 pool that allocates + VPC /56 CIDRs. + ipv6NetmaskLength: + type: integer + description: IPv6 netmask length allocated to each subnet. + default: 64 availabilityZones: type: array description: AZ suffixes for subnet creation (e.g., ["a", "b", "c"]). @@ -492,6 +525,13 @@ spec: cidr: type: string description: Allocated VPC IPv6 ULA CIDR from IPAM. + ipv6Gua: + type: object + description: IPv6 GUA IPAM allocation status. + properties: + cidr: + type: string + description: Allocated VPC IPv6 GUA CIDR from IPAM. network: type: object properties: @@ -508,6 +548,8 @@ spec: type: string ipv6Ula: type: string + ipv6Gua: + type: string ipv6AmazonProvided: type: string availabilityZones: diff --git a/examples/networks/with-ipam.yaml b/examples/networks/with-ipam.yaml index 11e9942..eef10b8 100644 --- a/examples/networks/with-ipam.yaml +++ b/examples/networks/with-ipam.yaml @@ -19,17 +19,18 @@ spec: # Pool ID from aws-ipam status poolId: ipam-pool-0123456789abcdef0 netmaskLength: 16 - ipv6Ula: + ipv6Gua: enabled: true - # ULA pool ID from aws-ipam status + # Regional Amazon-provided GUA pool; allocate one /56 to this VPC poolId: ipam-pool-0fedcba9876543210 - netmaskLength: 56 # AWS requires /56 for VPC IPv6 + netmaskLength: 56 - # Subnet layout - CIDRs calculated from VPC CIDR via cidrmath - # This is meant to be temporary until Terraform/Crossplane support for IPAM - # subnet allocations is available - # https://github.com/hops-ops/aws-network/pull/7 + # These are VPC resource-planning pools whose source resource is this VPC. + # They are not the regional pools above. subnetLayout: + ipv4PoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 + ipv6PoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 + ipv6NetmaskLength: 64 availabilityZones: - a - b diff --git a/functions/render/000-state-init.yaml.gotmpl b/functions/render/000-state-init.yaml.gotmpl index f1830ce..ef6adbe 100644 --- a/functions/render/000-state-init.yaml.gotmpl +++ b/functions/render/000-state-init.yaml.gotmpl @@ -59,6 +59,7 @@ {{- $ipamSpec := $spec.ipam | default dict }} {{- $ipv4IpamSpec := $ipamSpec.ipv4 | default dict }} {{- $ipv6UlaIpamSpec := $ipamSpec.ipv6Ula | default dict }} +{{- $ipv6GuaIpamSpec := $ipamSpec.ipv6Gua | default dict }} {{- $ipam := dict "ipv4" (dict "enabled" ($ipv4IpamSpec.enabled | default false) @@ -70,6 +71,11 @@ "poolId" ($ipv6UlaIpamSpec.poolId | default "") "netmaskLength" ($ipv6UlaIpamSpec.netmaskLength | default 56) ) + "ipv6Gua" (dict + "enabled" ($ipv6GuaIpamSpec.enabled | default false) + "poolId" ($ipv6GuaIpamSpec.poolId | default "") + "netmaskLength" ($ipv6GuaIpamSpec.netmaskLength | default 56) + ) }} # Subnet layout configuration @@ -77,6 +83,9 @@ {{- $layoutPublicSpec := $layoutSpec.public | default dict }} {{- $layoutPrivateSpec := $layoutSpec.private | default dict }} {{- $subnetLayout := dict + "ipv4PoolId" ($layoutSpec.ipv4PoolId | default "") + "ipv6PoolId" ($layoutSpec.ipv6PoolId | default "") + "ipv6NetmaskLength" ($layoutSpec.ipv6NetmaskLength | default 64) "availabilityZones" ($layoutSpec.availabilityZones | default (list "a" "b" "c")) "public" (dict "enabled" ($layoutPublicSpec.enabled | default true) diff --git a/functions/render/005-state-network.yaml.gotmpl b/functions/render/005-state-network.yaml.gotmpl index b90ef0a..906be04 100644 --- a/functions/render/005-state-network.yaml.gotmpl +++ b/functions/render/005-state-network.yaml.gotmpl @@ -17,12 +17,13 @@ # Layout mode: using subnetLayout to auto-calculate subnets (works with both IPAM and manual) {{- $layoutMode := or $ipamMode (and $manualMode (gt (len $eff.subnetLayout.availabilityZones) 0) (eq (len $eff.subnets) 0)) }} -# IPv6 enabled flag (from ULA, Amazon-provided, or IPAM allocation) -{{- $ulaEnabled := $eff.ipv6.ula.enabled }} -{{- if $eff.ipam.ipv6Ula.enabled }} - {{- $ulaEnabled = true }} -{{- end }} -{{- $hasIpv6 := or $ulaEnabled $eff.ipv6.amazonProvided.enabled }} +# IPv6 source precedence: GUA IPAM, ULA, then direct Amazon-provided allocation. +# Normalizing here prevents conflicting provider fields if callers set more than one. +{{- $guaEnabled := $eff.ipam.ipv6Gua.enabled }} +{{- $ulaRequested := or $eff.ipv6.ula.enabled $eff.ipam.ipv6Ula.enabled }} +{{- $ulaEnabled := and (not $guaEnabled) $ulaRequested }} +{{- $amazonEnabled := and (not $guaEnabled) (not $ulaEnabled) $eff.ipv6.amazonProvided.enabled }} +{{- $hasIpv6 := or $ulaEnabled $guaEnabled $amazonEnabled }} # ============================================================================== # Set $state.network core slice @@ -47,11 +48,13 @@ "ipamPoolId" $eff.ipv6.ula.ipamPoolId ) "amazon" (dict - "enabled" $eff.ipv6.amazonProvided.enabled + "enabled" $amazonEnabled + ) + "gua" (dict + "enabled" $guaEnabled ) ) "ipam" $eff.ipam }} {{- $state = set $state "network" $network }} - diff --git a/functions/render/007-state-network-subnets.yaml.gotmpl b/functions/render/007-state-network-subnets.yaml.gotmpl index a6b2128..504b4c4 100644 --- a/functions/render/007-state-network-subnets.yaml.gotmpl +++ b/functions/render/007-state-network-subnets.yaml.gotmpl @@ -96,6 +96,98 @@ {{- $numAzs := len $availabilityZones }} {{- $vpcCidr := $state.network.vpc.cidr }} {{- $vpcIpv6Cidr := $state.network.vpc.ipv6Cidr }} + {{- $subnetIpv4IpamPoolId := $eff.subnetLayout.ipv4PoolId }} + {{- $subnetIpv6IpamPoolId := $eff.subnetLayout.ipv6PoolId }} + + # -------------------------------------------------------------------------- + # IPAM Subnet Pool Path: allocate CIDRs from IPAM pool + # No VPC CIDR dependency — subnets render immediately + # -------------------------------------------------------------------------- + {{- if ne $subnetIpv4IpamPoolId "" }} + + {{- $azIndex := 0 }} + {{- range $az := $availabilityZones }} + {{- $fullAz := printf "%s%s" $state.network.region $az }} + + # Public subnet via IPAM + {{- if $eff.subnetLayout.public.enabled }} + {{- $subnetName := printf "%s-public-%s" $state.network.name $az }} + {{- $subnetKey := printf "public-%s" $az }} + {{- $subnetExternalName := get $eff.subnetLayout.externalNames $subnetKey | default "" }} + + {{- $subnetTags := merge (dict) $state.network.tags (dict + "Name" $subnetName + "hops.ops.com.ai/tier" "public" + "hops.ops.com.ai/az" $az + "kubernetes.io/role/elb" "1" + ) }} + + {{- $config := dict + "name" $subnetName + "externalName" $subnetExternalName + "managementPolicies" $eff.subnetLayout.managementPolicies + "resourceName" (printf "subnet-public-%s" $az) + "cidr" "" + "ipv4IpamPoolId" $subnetIpv4IpamPoolId + "ipv4NetmaskLength" $eff.subnetLayout.public.netmaskLength + "ipv6IpamPoolId" $subnetIpv6IpamPoolId + "ipv6NetmaskLength" $eff.subnetLayout.ipv6NetmaskLength + "az" $fullAz + "azSuffix" $az + "public" true + "mapPublicIpOnLaunch" true + "tags" $subnetTags + "labels" (merge (dict) $state.network.labels (dict "hops.ops.com.ai/tier" "public" "hops.ops.com.ai/az" $az)) + "ipv6CidrBlock" "" + "assignIpv6AddressOnCreation" (ne $subnetIpv6IpamPoolId "") + }} + + {{- $publicSubnets = append $publicSubnets $config }} + {{- end }} + + # Private subnet via IPAM + {{- if $eff.subnetLayout.private.enabled }} + {{- $subnetName := printf "%s-private-%s" $state.network.name $az }} + {{- $subnetKey := printf "private-%s" $az }} + {{- $subnetExternalName := get $eff.subnetLayout.externalNames $subnetKey | default "" }} + + {{- $subnetTags := merge (dict) $state.network.tags (dict + "Name" $subnetName + "hops.ops.com.ai/tier" "private" + "hops.ops.com.ai/az" $az + "kubernetes.io/role/internal-elb" "1" + ) }} + + {{- $config := dict + "name" $subnetName + "externalName" $subnetExternalName + "managementPolicies" $eff.subnetLayout.managementPolicies + "resourceName" (printf "subnet-private-%s" $az) + "cidr" "" + "ipv4IpamPoolId" $subnetIpv4IpamPoolId + "ipv4NetmaskLength" $eff.subnetLayout.private.netmaskLength + "ipv6IpamPoolId" $subnetIpv6IpamPoolId + "ipv6NetmaskLength" $eff.subnetLayout.ipv6NetmaskLength + "az" $fullAz + "azSuffix" $az + "public" false + "mapPublicIpOnLaunch" false + "tags" $subnetTags + "labels" (merge (dict) $state.network.labels (dict "hops.ops.com.ai/tier" "private" "hops.ops.com.ai/az" $az)) + "ipv6CidrBlock" "" + "assignIpv6AddressOnCreation" (ne $subnetIpv6IpamPoolId "") + }} + + {{- $privateSubnets = append $privateSubnets $config }} + {{- end }} + + {{- $azIndex = add $azIndex 1 }} + {{- end }} + + # -------------------------------------------------------------------------- + # cidrmath Path: calculate subnet CIDRs from VPC CIDR (existing behavior) + # -------------------------------------------------------------------------- + {{- else }} # Only calculate subnets if we have a VPC CIDR {{- if ne $vpcCidr "" }} @@ -131,7 +223,7 @@ # Calculate IPv6 subnet for public - only Amazon-provided (not ULA) {{- $ipv6AmazonCidr := "" }} - {{- if and $state.network.ipv6.amazon.enabled (ne $vpcIpv6Cidr "") }} + {{- if and (eq $subnetIpv6IpamPoolId "") (or $state.network.ipv6.amazon.enabled $state.network.ipv6.gua.enabled) (ne $vpcIpv6Cidr "") }} {{- $ipv6Parts := splitList "/" $vpcIpv6Cidr }} {{- $ipv6Base := index $ipv6Parts 0 }} {{- $ipv6Groups := splitList ":" (trimSuffix "::" $ipv6Base) }} @@ -163,7 +255,7 @@ ) }} {{- $ipv6CidrBlock := $ipv6AmazonCidr }} - {{- $assignIpv6 := ne $ipv6CidrBlock "" }} + {{- $assignIpv6 := or (ne $subnetIpv6IpamPoolId "") (ne $ipv6CidrBlock "") }} # Get externalName from layout map (key = "public-{az}") {{- $subnetKey := printf "public-%s" $az }} @@ -181,6 +273,8 @@ "mapPublicIpOnLaunch" true "tags" $subnetTags "labels" (merge (dict) $state.network.labels (dict "hops.ops.com.ai/tier" "public" "hops.ops.com.ai/az" $az)) + "ipv6IpamPoolId" $subnetIpv6IpamPoolId + "ipv6NetmaskLength" $eff.subnetLayout.ipv6NetmaskLength "ipv6CidrBlock" $ipv6CidrBlock "assignIpv6AddressOnCreation" $assignIpv6 }} @@ -198,7 +292,7 @@ # Calculate IPv6 subnets for private - supports both ULA and Amazon-provided {{- $ipv6UlaCidr := "" }} {{- $ipv6AmazonCidr := "" }} - {{- if ne $vpcIpv6Cidr "" }} + {{- if and (eq $subnetIpv6IpamPoolId "") (ne $vpcIpv6Cidr "") }} {{- $ipv6Parts := splitList "/" $vpcIpv6Cidr }} {{- $ipv6Base := index $ipv6Parts 0 }} {{- $ipv6Groups := splitList ":" (trimSuffix "::" $ipv6Base) }} @@ -224,7 +318,7 @@ {{- $ipv6UlaCidr = printf "%s:%s:%s:%s::/64" $g1 $g2 $g3 $newG4 }} {{- end }} # Amazon-provided IPv6 for private subnets (offset after public subnets) - {{- if $state.network.ipv6.amazon.enabled }} + {{- if or $state.network.ipv6.amazon.enabled $state.network.ipv6.gua.enabled }} {{- $ipv6PrivateIndex := add $azIndex $numAzs }} {{- $subnetHex := "" }} {{- if ne $g4Base "" }} @@ -246,7 +340,7 @@ # Compute final IPv6 values for this subnet (ULA preferred over Amazon-provided) {{- $ipv6CidrBlock := $ipv6UlaCidr | default $ipv6AmazonCidr }} - {{- $assignIpv6 := ne $ipv6CidrBlock "" }} + {{- $assignIpv6 := or (ne $subnetIpv6IpamPoolId "") (ne $ipv6CidrBlock "") }} # Get externalName from layout map (key = "private-{az}") {{- $subnetKey := printf "private-%s" $az }} @@ -264,6 +358,8 @@ "mapPublicIpOnLaunch" false "tags" $subnetTags "labels" (merge (dict) $state.network.labels (dict "hops.ops.com.ai/tier" "private" "hops.ops.com.ai/az" $az)) + "ipv6IpamPoolId" $subnetIpv6IpamPoolId + "ipv6NetmaskLength" $eff.subnetLayout.ipv6NetmaskLength "ipv6CidrBlock" $ipv6CidrBlock "assignIpv6AddressOnCreation" $assignIpv6 }} @@ -274,6 +370,8 @@ {{- $azIndex = add $azIndex 1 }} {{- end }} {{- end }} + + {{- end }} {{- end }} # ============================================================================== diff --git a/functions/render/009-state-status.yaml.gotmpl b/functions/render/009-state-status.yaml.gotmpl index 2d3b363..bf132bb 100644 --- a/functions/render/009-state-status.yaml.gotmpl +++ b/functions/render/009-state-status.yaml.gotmpl @@ -12,13 +12,16 @@ # Compute IPAM Status (only if in IPAM mode) # ============================================================================== {{- $ipamStatus := dict }} -{{- if $net.mode.ipam }} +{{- if or $net.ipam.ipv4.enabled (and $net.ipam.ipv6Ula.enabled $net.ipv6.ula.enabled) $net.ipv6.gua.enabled }} {{- if $net.ipam.ipv4.enabled }} {{- $ipamStatus = set $ipamStatus "ipv4" (dict "cidr" ($obs.vpc.cidr | default "Pending")) }} {{- end }} - {{- if $net.ipam.ipv6Ula.enabled }} + {{- if and $net.ipam.ipv6Ula.enabled $net.ipv6.ula.enabled }} {{- $ipamStatus = set $ipamStatus "ipv6Ula" (dict "cidr" ($obs.vpc.ipv6Cidr | default "Pending")) }} {{- end }} + {{- if $net.ipv6.gua.enabled }} + {{- $ipamStatus = set $ipamStatus "ipv6Gua" (dict "cidr" ($obs.vpc.ipv6Cidr | default "Pending")) }} + {{- end }} {{- end }} # ============================================================================== @@ -28,6 +31,9 @@ {{- if $net.ipv6.ula.enabled }} {{- $cidrStatus = set $cidrStatus "ipv6Ula" ($obs.vpc.ipv6Cidr | default "Pending") }} {{- end }} +{{- if $net.ipv6.gua.enabled }} + {{- $cidrStatus = set $cidrStatus "ipv6Gua" ($obs.vpc.ipv6Cidr | default "Pending") }} +{{- end }} {{- if $net.ipv6.amazon.enabled }} {{- $cidrStatus = set $cidrStatus "ipv6AmazonProvided" ($obs.vpc.ipv6Cidr | default "Pending") }} {{- end }} @@ -173,9 +179,8 @@ ) }} -{{- if $net.mode.ipam }} +{{- if or $net.ipam.ipv4.enabled (and $net.ipam.ipv6Ula.enabled $net.ipv6.ula.enabled) $net.ipv6.gua.enabled }} {{- $status = set $status "ipam" $ipamStatus }} {{- end }} {{- $state = set $state "status" $status }} - diff --git a/functions/render/10-vpc.yaml.gotmpl b/functions/render/10-vpc.yaml.gotmpl index 2de7fd2..4aeb1f3 100644 --- a/functions/render/10-vpc.yaml.gotmpl +++ b/functions/render/10-vpc.yaml.gotmpl @@ -31,10 +31,11 @@ spec: # Use explicit CIDR cidrBlock: {{ $state.network.vpc.cidr }} {{- end }} - {{- if $state.network.ipv6.amazon.enabled }} - assignGeneratedIpv6CidrBlock: true - {{- end }} - {{- if $state.network.ipam.ipv6Ula.enabled }} + {{- if $state.network.ipam.ipv6Gua.enabled }} + # Allocate an IPv6 GUA /56 from a regional IPAM pool + ipv6IpamPoolId: {{ $state.network.ipam.ipv6Gua.poolId }} + ipv6NetmaskLength: {{ $state.network.ipam.ipv6Gua.netmaskLength }} + {{- else if $state.network.ipam.ipv6Ula.enabled }} # Allocate IPv6 ULA CIDR from IPAM pool ipv6IpamPoolId: {{ $state.network.ipam.ipv6Ula.poolId }} ipv6NetmaskLength: {{ $state.network.ipam.ipv6Ula.netmaskLength }} @@ -44,6 +45,8 @@ spec: {{- if $state.network.ipv6.ula.cidr }} ipv6CidrBlock: {{ $state.network.ipv6.ula.cidr }} {{- end }} + {{- else if $state.network.ipv6.amazon.enabled }} + assignGeneratedIpv6CidrBlock: true {{- end }} tags: Name: {{ $state.network.name }} diff --git a/functions/render/50-subnets.yaml.gotmpl b/functions/render/50-subnets.yaml.gotmpl index 5858db0..2bf72a4 100644 --- a/functions/render/50-subnets.yaml.gotmpl +++ b/functions/render/50-subnets.yaml.gotmpl @@ -11,7 +11,7 @@ # Public Subnets # ============================================================================== {{- range $subnet := $state.network.subnets.public }} -{{- if ne $subnet.cidr "" }} +{{- if or (ne $subnet.cidr "") (ne ($subnet.ipv4IpamPoolId | default "") "") }} {{- $subnetExternalName := $subnet.externalName | default "" }} {{- $subnetMgmtPolicies := $subnet.managementPolicies | default $state.network.managementPolicies }} --- @@ -34,11 +34,21 @@ spec: vpcIdRef: name: {{ $state.network.name }} mapPublicIpOnLaunch: {{ $subnet.mapPublicIpOnLaunch }} + {{- if ne ($subnet.ipv4IpamPoolId | default "") "" }} + ipv4IpamPoolId: {{ $subnet.ipv4IpamPoolId }} + ipv4NetmaskLength: {{ $subnet.ipv4NetmaskLength }} + {{- else }} cidrBlock: {{ $subnet.cidr }} + {{- end }} {{- if $subnet.assignIpv6AddressOnCreation }} assignIpv6AddressOnCreation: true + {{- if ne ($subnet.ipv6IpamPoolId | default "") "" }} + ipv6IpamPoolId: {{ $subnet.ipv6IpamPoolId }} + ipv6NetmaskLength: {{ $subnet.ipv6NetmaskLength }} + {{- else }} ipv6CidrBlock: {{ $subnet.ipv6CidrBlock }} {{- end }} + {{- end }} tags: {{ $subnet.tags | toYaml | nindent 6 }} providerConfigRef: @@ -75,7 +85,7 @@ spec: # Private Subnets # ============================================================================== {{- range $subnet := $state.network.subnets.private }} -{{- if ne $subnet.cidr "" }} +{{- if or (ne $subnet.cidr "") (ne ($subnet.ipv4IpamPoolId | default "") "") }} {{- $subnetExternalName := $subnet.externalName | default "" }} {{- $subnetMgmtPolicies := $subnet.managementPolicies | default $state.network.managementPolicies }} --- @@ -98,11 +108,21 @@ spec: vpcIdRef: name: {{ $state.network.name }} mapPublicIpOnLaunch: {{ $subnet.mapPublicIpOnLaunch }} + {{- if ne ($subnet.ipv4IpamPoolId | default "") "" }} + ipv4IpamPoolId: {{ $subnet.ipv4IpamPoolId }} + ipv4NetmaskLength: {{ $subnet.ipv4NetmaskLength }} + {{- else }} cidrBlock: {{ $subnet.cidr }} + {{- end }} {{- if $subnet.assignIpv6AddressOnCreation }} assignIpv6AddressOnCreation: true + {{- if ne ($subnet.ipv6IpamPoolId | default "") "" }} + ipv6IpamPoolId: {{ $subnet.ipv6IpamPoolId }} + ipv6NetmaskLength: {{ $subnet.ipv6NetmaskLength }} + {{- else }} ipv6CidrBlock: {{ $subnet.ipv6CidrBlock }} {{- end }} + {{- end }} tags: {{ $subnet.tags | toYaml | nindent 6 }} providerConfigRef: @@ -136,4 +156,3 @@ spec: {{- end }} {{- end }} - diff --git a/functions/render/999-status.yaml.gotmpl b/functions/render/999-status.yaml.gotmpl index a20580a..0f93321 100644 --- a/functions/render/999-status.yaml.gotmpl +++ b/functions/render/999-status.yaml.gotmpl @@ -24,6 +24,10 @@ status: ipv6Ula: cidr: {{ $s.ipam.ipv6Ula.cidr }} {{- end }} + {{- if $s.ipam.ipv6Gua }} + ipv6Gua: + cidr: {{ $s.ipam.ipv6Gua.cidr }} + {{- end }} {{- end }} network: name: {{ $net.name }} @@ -34,6 +38,9 @@ status: {{- if $net.cidr.ipv6Ula }} ipv6Ula: {{ $net.cidr.ipv6Ula }} {{- end }} + {{- if $net.cidr.ipv6Gua }} + ipv6Gua: {{ $net.cidr.ipv6Gua }} + {{- end }} {{- if $net.cidr.ipv6AmazonProvided }} ipv6AmazonProvided: {{ $net.cidr.ipv6AmazonProvided }} {{- end }} @@ -109,4 +116,3 @@ status: id: {{ $net.transitGatewayAttachment.id }} ready: {{ $net.transitGatewayAttachment.ready }} {{- end }} - diff --git a/tests/test-render/main.k b/tests/test-render/main.k index a369bcb..fff7e3f 100644 --- a/tests/test-render/main.k +++ b/tests/test-render/main.k @@ -282,7 +282,7 @@ _items = [ } }, - # Test: ipam.ipv4.enabled=true uses IPAM pool allocation + # Test: VPC and subnets use their respective IPAM pools metav1alpha1.CompositionTest { metadata.name = "ipam-enabled-uses-pool-id" spec = { @@ -300,8 +300,21 @@ _items = [ ipv4 = { enabled = True poolId = "ipam-pool-0123456789abcdef0" - vpc = {netmaskLength = 16} + netmaskLength = 16 } + ipv6Gua = { + enabled = True + poolId = "ipam-pool-0fedcba9876543210" + netmaskLength = 56 + } + } + subnetLayout = { + ipv4PoolId = "ipam-pool-vpc-ipv4-0123456789abcdef0" + ipv6PoolId = "ipam-pool-vpc-ipv6-0123456789abcdef0" + ipv6NetmaskLength = 64 + availabilityZones = ["a"] + public = {enabled = True, netmaskLength = 24} + private = {enabled = False} } } } @@ -313,6 +326,20 @@ _items = [ spec.forProvider = { ipv4IpamPoolId = "ipam-pool-0123456789abcdef0" ipv4NetmaskLength = 16 + ipv6IpamPoolId = "ipam-pool-0fedcba9876543210" + ipv6NetmaskLength = 56 + } + } + { + apiVersion = "ec2.aws.m.upbound.io/v1beta1" + kind = "Subnet" + metadata.name = "test-ipam-public-a" + spec.forProvider = { + ipv4IpamPoolId = "ipam-pool-vpc-ipv4-0123456789abcdef0" + ipv4NetmaskLength = 24 + ipv6IpamPoolId = "ipam-pool-vpc-ipv6-0123456789abcdef0" + ipv6NetmaskLength = 64 + assignIpv6AddressOnCreation = True } } ] From d587145ad5b4bba942250f9b8b29d68669c8dced Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Tue, 25 Aug 2026 04:14:34 -0500 Subject: [PATCH 2/4] feat: create VPC IPAM resource planning pools --- Makefile | 8 +- README.md | 22 +++- apis/networks/definition.yaml | 61 +++++++++ examples/networks/with-ipam.yaml | 15 ++- .../with-ipam/steps/1/vpc.yaml | 3 +- .../with-ipam/steps/2/ipam-planning-ipv4.yaml | 25 ++++ .../with-ipam/steps/2/ipam-planning-ipv6.yaml | 25 ++++ .../with-ipam/steps/2/vpc.yaml | 3 +- .../with-ipam/steps/3/ipam-planning-ipv4.yaml | 25 ++++ .../with-ipam/steps/3/ipam-planning-ipv6.yaml | 25 ++++ .../with-ipam/steps/3/vpc.yaml | 3 +- .../with-ipam/steps/4/egress-only-igw.yaml | 1 + .../with-ipam/steps/4/eip-a.yaml | 1 + .../with-ipam/steps/4/internet-gateway.yaml | 1 + .../with-ipam/steps/4/ipam-planning-ipv4.yaml | 25 ++++ .../with-ipam/steps/4/ipam-planning-ipv6.yaml | 25 ++++ .../with-ipam/steps/4/nat-a.yaml | 1 + .../with-ipam/steps/4/rt-private-a.yaml | 1 + .../with-ipam/steps/4/rt-private-b.yaml | 1 + .../with-ipam/steps/4/rt-private-c.yaml | 1 + .../with-ipam/steps/4/rt-public.yaml | 1 + .../with-ipam/steps/4/subnet-private-a.yaml | 3 +- .../with-ipam/steps/4/subnet-private-b.yaml | 3 +- .../with-ipam/steps/4/subnet-private-c.yaml | 3 +- .../with-ipam/steps/4/subnet-public-a.yaml | 3 +- .../with-ipam/steps/4/subnet-public-b.yaml | 3 +- .../with-ipam/steps/4/subnet-public-c.yaml | 3 +- .../with-ipam/steps/4/vpc.yaml | 3 +- functions/render/000-state-init.yaml.gotmpl | 19 +++ .../render/001-state-observed-vpc.yaml.gotmpl | 3 +- ...5-state-observed-ipam-planning.yaml.gotmpl | 23 ++++ .../007-state-network-subnets.yaml.gotmpl | 18 +++ functions/render/009-state-status.yaml.gotmpl | 20 ++- .../render/20-ipam-planning-pools.yaml.gotmpl | 123 ++++++++++++++++++ functions/render/50-subnets.yaml.gotmpl | 88 +++++++++++++ functions/render/999-status.yaml.gotmpl | 9 ++ 36 files changed, 575 insertions(+), 22 deletions(-) create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6.yaml create mode 100644 functions/render/0025-state-observed-ipam-planning.yaml.gotmpl create mode 100644 functions/render/20-ipam-planning-pools.yaml.gotmpl diff --git a/Makefile b/Makefile index 8190d60..912536a 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,7 @@ render\:all: observed=$${entry#*::}; \ outfile="$$tmpdir/$$(echo $$entry | tr '/:' '__')"; \ ( \ + set -o pipefail; \ if [ -n "$$observed" ]; then \ echo "=== Rendering $$example with observed-resources $$observed ==="; \ up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example --observed-resources=$$observed; \ @@ -75,16 +76,17 @@ validate\:all: generate-configuration observed=$${entry#*::}; \ outfile="$$tmpdir/$$(echo $$entry | tr '/:' '__')"; \ ( \ + set -o pipefail; \ if [ -n "$$observed" ]; then \ echo "=== Validating $$example with observed-resources $$observed ==="; \ up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \ --observed-resources=$$observed --include-full-xr --quiet | \ - crossplane beta validate $(XRD_DIR) --error-on-missing-schemas -; \ + crossplane resource validate $(XRD_DIR) --error-on-missing-schemas -; \ else \ echo "=== Validating $$example ==="; \ up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \ --include-full-xr --quiet | \ - crossplane beta validate $(XRD_DIR) --error-on-missing-schemas -; \ + crossplane resource validate $(XRD_DIR) --error-on-missing-schemas -; \ fi; \ echo "" \ ) > "$$outfile" 2>&1 & \ @@ -123,7 +125,7 @@ validate\:%: generate-configuration echo "=== Validating $$example ==="; \ up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \ --include-full-xr --quiet | \ - crossplane beta validate $(XRD_DIR) --error-on-missing-schemas -; \ + crossplane resource validate $(XRD_DIR) --error-on-missing-schemas -; \ else \ echo "Example $$example not found"; \ exit 1; \ diff --git a/README.md b/README.md index 54f441d..7ab7399 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,17 @@ spec: enabled: true poolId: ipam-pool-0fedcba9876543210 netmaskLength: 56 + resourcePlanning: + # The IPAM administrator creates pools sourced from this workload VPC. + providerConfigRef: + name: network + ipv4: + enabled: true + scopeId: ipam-scope-private0123456789 + ipv6: + enabled: true + scopeId: ipam-scope-public0123456789 subnetLayout: - # VPC resource-planning pools, not the regional VPC allocation pools above - ipv4PoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 - ipv6PoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 ipv6NetmaskLength: 64 availabilityZones: [a, b, c] public: @@ -456,6 +463,12 @@ spec: | `ipv6Gua.enabled` | boolean | Enable IPv6 GUA allocation from a regional IPAM pool | | `ipv6Gua.poolId` | string | Regional Amazon-provided or BYOIP IPv6 IPAM pool ID | | `ipv6Gua.netmaskLength` | int | VPC IPv6 netmask (default: 56) | +| `resourcePlanning.providerConfigRef` | object | ProviderConfig for the IPAM administrator account | +| `resourcePlanning.managementPolicies` | []string | Management operations for planning pools | +| `resourcePlanning.ipv4.enabled` | boolean | Create an IPv4 planning pool sourced from the VPC | +| `resourcePlanning.ipv4.scopeId` | string | Private IPAM scope containing the IPv4 regional pool | +| `resourcePlanning.ipv6.enabled` | boolean | Create an IPv6 planning pool sourced from the VPC | +| `resourcePlanning.ipv6.scopeId` | string | IPAM scope containing the IPv6 regional pool | ### spec.subnetLayout @@ -509,6 +522,9 @@ status: cidr: "fd00:dead:beef::/56" ipv6Gua: cidr: "2600:1f18:abc::/56" + resourcePlanning: + ipv4PoolId: ipam-pool-vpc-ipv4-abc123 + ipv6PoolId: ipam-pool-vpc-ipv6-def456 network: name: my-network region: us-east-1 diff --git a/apis/networks/definition.yaml b/apis/networks/definition.yaml index 3ceb3e1..7a41c7e 100644 --- a/apis/networks/definition.yaml +++ b/apis/networks/definition.yaml @@ -115,6 +115,59 @@ spec: type: integer description: Netmask length allocated to the VPC (typically /56). default: 56 + resourcePlanning: + type: object + description: | + Create VPC resource-planning pools in the IPAM administrator + account and allocate layout subnets from them. The VPC must + receive its address space from the corresponding regional pool. + properties: + providerConfigRef: + type: object + description: AWS ProviderConfig for the IPAM administrator account. + properties: + name: + type: string + description: Name of the IPAM administrator ProviderConfig. + default: default + kind: + type: string + description: Kind of the provider config reference. + default: ProviderConfig + managementPolicies: + type: array + description: Management operations for resource-planning pools. + items: + type: string + enum: + - "*" + - Create + - Observe + - Update + - Delete + - LateInitialize + ipv4: + type: object + description: IPv4 VPC resource-planning pool configuration. + properties: + enabled: + type: boolean + description: Create an IPv4 planning pool sourced from this VPC. + default: false + scopeId: + type: string + description: Private IPAM scope ID in the IPAM administrator account. + ipv6: + type: object + description: IPv6 VPC resource-planning pool configuration. + properties: + enabled: + type: boolean + description: Create an IPv6 planning pool sourced from this VPC. + default: false + scopeId: + type: string + description: IPAM scope ID containing the regional IPv6 source pool. # Subnet layout - used with both IPAM and manual VPC CIDR subnetLayout: @@ -532,6 +585,14 @@ spec: cidr: type: string description: Allocated VPC IPv6 GUA CIDR from IPAM. + resourcePlanning: + type: object + description: Observed VPC resource-planning pool IDs. + properties: + ipv4PoolId: + type: string + ipv6PoolId: + type: string network: type: object properties: diff --git a/examples/networks/with-ipam.yaml b/examples/networks/with-ipam.yaml index eef10b8..5241221 100644 --- a/examples/networks/with-ipam.yaml +++ b/examples/networks/with-ipam.yaml @@ -24,12 +24,19 @@ spec: # Regional Amazon-provided GUA pool; allocate one /56 to this VPC poolId: ipam-pool-0fedcba9876543210 netmaskLength: 56 + resourcePlanning: + # Planning pools are created in the central IPAM administrator account. + providerConfigRef: + name: network + ipv4: + enabled: true + scopeId: ipam-scope-0123456789abcdef0 + ipv6: + enabled: true + scopeId: ipam-scope-0fedcba9876543210 - # These are VPC resource-planning pools whose source resource is this VPC. - # They are not the regional pools above. + # Subnets allocate directly from the automatically created VPC planning pools. subnetLayout: - ipv4PoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 - ipv6PoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 ipv6NetmaskLength: 64 availabilityZones: - a diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/1/vpc.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/1/vpc.yaml index ba00707..89928c2 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/1/vpc.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/1/vpc.yaml @@ -2,6 +2,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: VPC metadata: name: with-ipam + namespace: infra annotations: crossplane.io/composition-resource-name: vpc gotemplating.fn.crossplane.io/composition-resource-name: vpc @@ -15,7 +16,7 @@ status: id: vpc-0123456789abcdef0 arn: arn:aws:ec2:us-east-1:123456789012:vpc/vpc-0123456789abcdef0 cidrBlock: "10.100.0.0/16" - ipv6CidrBlock: "fd00:100::/56" + ipv6CidrBlock: "2600:1f26:45:8100::/56" ipv6AssociationId: vpc-cidr-assoc-0123456789abcdef0 defaultNetworkAclId: acl-0123456789abcdef0 defaultRouteTableId: rtb-0123456789abcdef0 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4.yaml new file mode 100644 index 0000000..f8d769e --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4.yaml @@ -0,0 +1,25 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPool +metadata: + namespace: infra + name: with-ipam-ipv4-planning + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv4 + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv4 +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv4-0123456789abcdef0 + addressFamily: ipv4 + ipamScopeId: ipam-scope-0123456789abcdef0 + locale: us-east-1 + sourceIpamPoolId: ipam-pool-0123456789abcdef0 + sourceResource: + resourceId: vpc-0123456789abcdef0 + resourceOwner: "123456789012" + resourceRegion: us-east-1 + resourceType: vpc diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6.yaml new file mode 100644 index 0000000..150a25f --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6.yaml @@ -0,0 +1,25 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPool +metadata: + namespace: infra + name: with-ipam-ipv6-planning + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv6 + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv6 +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv6-0123456789abcdef0 + addressFamily: ipv6 + ipamScopeId: ipam-scope-0fedcba9876543210 + locale: us-east-1 + sourceIpamPoolId: ipam-pool-0fedcba9876543210 + sourceResource: + resourceId: vpc-0123456789abcdef0 + resourceOwner: "123456789012" + resourceRegion: us-east-1 + resourceType: vpc diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/2/vpc.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/2/vpc.yaml index ca76993..6ae4508 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/2/vpc.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/2/vpc.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: VPC metadata: + namespace: infra name: with-ipam annotations: crossplane.io/composition-resource-name: vpc @@ -16,7 +17,7 @@ status: arn: arn:aws:ec2:us-east-1:123456789012:vpc/vpc-0123456789abcdef0 cidrBlock: "10.100.0.0/16" ipv4IpamPoolId: ipam-pool-0123456789abcdef0 - ipv6CidrBlock: "fd00:100::/56" + ipv6CidrBlock: "2600:1f26:45:8100::/56" ipv6IpamPoolId: ipam-pool-0fedcba9876543210 ipv6AssociationId: vpc-cidr-assoc-0123456789abcdef0 defaultNetworkAclId: acl-0123456789abcdef0 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4.yaml new file mode 100644 index 0000000..f8d769e --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4.yaml @@ -0,0 +1,25 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPool +metadata: + namespace: infra + name: with-ipam-ipv4-planning + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv4 + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv4 +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv4-0123456789abcdef0 + addressFamily: ipv4 + ipamScopeId: ipam-scope-0123456789abcdef0 + locale: us-east-1 + sourceIpamPoolId: ipam-pool-0123456789abcdef0 + sourceResource: + resourceId: vpc-0123456789abcdef0 + resourceOwner: "123456789012" + resourceRegion: us-east-1 + resourceType: vpc diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6.yaml new file mode 100644 index 0000000..150a25f --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6.yaml @@ -0,0 +1,25 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPool +metadata: + namespace: infra + name: with-ipam-ipv6-planning + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv6 + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv6 +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv6-0123456789abcdef0 + addressFamily: ipv6 + ipamScopeId: ipam-scope-0fedcba9876543210 + locale: us-east-1 + sourceIpamPoolId: ipam-pool-0fedcba9876543210 + sourceResource: + resourceId: vpc-0123456789abcdef0 + resourceOwner: "123456789012" + resourceRegion: us-east-1 + resourceType: vpc diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/3/vpc.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/3/vpc.yaml index ca76993..6ae4508 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/3/vpc.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/3/vpc.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: VPC metadata: + namespace: infra name: with-ipam annotations: crossplane.io/composition-resource-name: vpc @@ -16,7 +17,7 @@ status: arn: arn:aws:ec2:us-east-1:123456789012:vpc/vpc-0123456789abcdef0 cidrBlock: "10.100.0.0/16" ipv4IpamPoolId: ipam-pool-0123456789abcdef0 - ipv6CidrBlock: "fd00:100::/56" + ipv6CidrBlock: "2600:1f26:45:8100::/56" ipv6IpamPoolId: ipam-pool-0fedcba9876543210 ipv6AssociationId: vpc-cidr-assoc-0123456789abcdef0 defaultNetworkAclId: acl-0123456789abcdef0 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/egress-only-igw.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/egress-only-igw.yaml index 74c8ce8..2b77f76 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/egress-only-igw.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/egress-only-igw.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: EgressOnlyInternetGateway metadata: + namespace: infra name: with-ipam annotations: crossplane.io/composition-resource-name: egress-only-igw diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/eip-a.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/eip-a.yaml index 9ef199a..3beb7a8 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/eip-a.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/eip-a.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: EIP metadata: + namespace: infra name: with-ipam-nat-a annotations: crossplane.io/composition-resource-name: eip-nat-a diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/internet-gateway.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/internet-gateway.yaml index 2411423..3cde618 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/internet-gateway.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/internet-gateway.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: InternetGateway metadata: + namespace: infra name: with-ipam annotations: crossplane.io/composition-resource-name: internet-gateway diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4.yaml new file mode 100644 index 0000000..f8d769e --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4.yaml @@ -0,0 +1,25 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPool +metadata: + namespace: infra + name: with-ipam-ipv4-planning + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv4 + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv4 +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv4-0123456789abcdef0 + addressFamily: ipv4 + ipamScopeId: ipam-scope-0123456789abcdef0 + locale: us-east-1 + sourceIpamPoolId: ipam-pool-0123456789abcdef0 + sourceResource: + resourceId: vpc-0123456789abcdef0 + resourceOwner: "123456789012" + resourceRegion: us-east-1 + resourceType: vpc diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6.yaml new file mode 100644 index 0000000..150a25f --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6.yaml @@ -0,0 +1,25 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPool +metadata: + namespace: infra + name: with-ipam-ipv6-planning + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv6 + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv6 +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv6-0123456789abcdef0 + addressFamily: ipv6 + ipamScopeId: ipam-scope-0fedcba9876543210 + locale: us-east-1 + sourceIpamPoolId: ipam-pool-0fedcba9876543210 + sourceResource: + resourceId: vpc-0123456789abcdef0 + resourceOwner: "123456789012" + resourceRegion: us-east-1 + resourceType: vpc diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/nat-a.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/nat-a.yaml index 40d829e..e7c65fe 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/nat-a.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/nat-a.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: NATGateway metadata: + namespace: infra name: with-ipam-nat-a annotations: crossplane.io/composition-resource-name: nat-a diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-a.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-a.yaml index fd0480b..ea7bc19 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-a.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-a.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: RouteTable metadata: + namespace: infra name: with-ipam-private-a annotations: crossplane.io/composition-resource-name: rt-private-a diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-b.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-b.yaml index c14eb46..d9cacd2 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-b.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-b.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: RouteTable metadata: + namespace: infra name: with-ipam-private-b annotations: crossplane.io/composition-resource-name: rt-private-b diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-c.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-c.yaml index e7c280d..1083fc7 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-c.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-private-c.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: RouteTable metadata: + namespace: infra name: with-ipam-private-c annotations: crossplane.io/composition-resource-name: rt-private-c diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-public.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-public.yaml index d405e2c..4ec081d 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-public.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/rt-public.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: RouteTable metadata: + namespace: infra name: with-ipam-public annotations: crossplane.io/composition-resource-name: rt-public diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-a.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-a.yaml index e39b4cc..8698217 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-a.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-a.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: Subnet metadata: + namespace: infra name: with-ipam-private-a annotations: crossplane.io/composition-resource-name: subnet-private-a @@ -16,7 +17,7 @@ status: arn: arn:aws:ec2:us-east-1:123456789012:subnet/subnet-priv-a-0123456789 vpcId: vpc-0123456789abcdef0 cidrBlock: "10.100.0.0/20" - ipv6CidrBlock: "fd00:100:0:1::/64" + ipv6CidrBlock: "2600:1f26:45:8101::/64" availabilityZone: us-east-1a availabilityZoneId: use1-az1 mapPublicIpOnLaunch: false diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-b.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-b.yaml index d1b2e40..12d34ad 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-b.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-b.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: Subnet metadata: + namespace: infra name: with-ipam-private-b annotations: crossplane.io/composition-resource-name: subnet-private-b @@ -16,7 +17,7 @@ status: arn: arn:aws:ec2:us-east-1:123456789012:subnet/subnet-priv-b-0123456789 vpcId: vpc-0123456789abcdef0 cidrBlock: "10.100.16.0/20" - ipv6CidrBlock: "fd00:100:0:2::/64" + ipv6CidrBlock: "2600:1f26:45:8102::/64" availabilityZone: us-east-1b availabilityZoneId: use1-az2 mapPublicIpOnLaunch: false diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-c.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-c.yaml index fdbb3bd..a8c94c2 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-c.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-private-c.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: Subnet metadata: + namespace: infra name: with-ipam-private-c annotations: crossplane.io/composition-resource-name: subnet-private-c @@ -16,7 +17,7 @@ status: arn: arn:aws:ec2:us-east-1:123456789012:subnet/subnet-priv-c-0123456789 vpcId: vpc-0123456789abcdef0 cidrBlock: "10.100.32.0/20" - ipv6CidrBlock: "fd00:100:0:3::/64" + ipv6CidrBlock: "2600:1f26:45:8103::/64" availabilityZone: us-east-1c availabilityZoneId: use1-az3 mapPublicIpOnLaunch: false diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-a.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-a.yaml index fbb81e0..93669dd 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-a.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-a.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: Subnet metadata: + namespace: infra name: with-ipam-public-a annotations: crossplane.io/composition-resource-name: subnet-public-a @@ -16,7 +17,7 @@ status: arn: arn:aws:ec2:us-east-1:123456789012:subnet/subnet-pub-a-0123456789 vpcId: vpc-0123456789abcdef0 cidrBlock: "10.100.200.0/24" - ipv6CidrBlock: "fd00:100:0:100::/64" + ipv6CidrBlock: "2600:1f26:45:8110::/64" availabilityZone: us-east-1a availabilityZoneId: use1-az1 mapPublicIpOnLaunch: true diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-b.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-b.yaml index e07f32a..18d87df 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-b.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-b.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: Subnet metadata: + namespace: infra name: with-ipam-public-b annotations: crossplane.io/composition-resource-name: subnet-public-b @@ -16,7 +17,7 @@ status: arn: arn:aws:ec2:us-east-1:123456789012:subnet/subnet-pub-b-0123456789 vpcId: vpc-0123456789abcdef0 cidrBlock: "10.100.201.0/24" - ipv6CidrBlock: "fd00:100:0:101::/64" + ipv6CidrBlock: "2600:1f26:45:8111::/64" availabilityZone: us-east-1b availabilityZoneId: use1-az2 mapPublicIpOnLaunch: true diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-c.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-c.yaml index 338700d..f8b8121 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-c.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/subnet-public-c.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: Subnet metadata: + namespace: infra name: with-ipam-public-c annotations: crossplane.io/composition-resource-name: subnet-public-c @@ -16,7 +17,7 @@ status: arn: arn:aws:ec2:us-east-1:123456789012:subnet/subnet-pub-c-0123456789 vpcId: vpc-0123456789abcdef0 cidrBlock: "10.100.202.0/24" - ipv6CidrBlock: "fd00:100:0:102::/64" + ipv6CidrBlock: "2600:1f26:45:8112::/64" availabilityZone: us-east-1c availabilityZoneId: use1-az3 mapPublicIpOnLaunch: true diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/vpc.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/vpc.yaml index ca76993..6ae4508 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/vpc.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/vpc.yaml @@ -1,6 +1,7 @@ apiVersion: ec2.aws.m.upbound.io/v1beta1 kind: VPC metadata: + namespace: infra name: with-ipam annotations: crossplane.io/composition-resource-name: vpc @@ -16,7 +17,7 @@ status: arn: arn:aws:ec2:us-east-1:123456789012:vpc/vpc-0123456789abcdef0 cidrBlock: "10.100.0.0/16" ipv4IpamPoolId: ipam-pool-0123456789abcdef0 - ipv6CidrBlock: "fd00:100::/56" + ipv6CidrBlock: "2600:1f26:45:8100::/56" ipv6IpamPoolId: ipam-pool-0fedcba9876543210 ipv6AssociationId: vpc-cidr-assoc-0123456789abcdef0 defaultNetworkAclId: acl-0123456789abcdef0 diff --git a/functions/render/000-state-init.yaml.gotmpl b/functions/render/000-state-init.yaml.gotmpl index ef6adbe..0a54aad 100644 --- a/functions/render/000-state-init.yaml.gotmpl +++ b/functions/render/000-state-init.yaml.gotmpl @@ -60,6 +60,10 @@ {{- $ipv4IpamSpec := $ipamSpec.ipv4 | default dict }} {{- $ipv6UlaIpamSpec := $ipamSpec.ipv6Ula | default dict }} {{- $ipv6GuaIpamSpec := $ipamSpec.ipv6Gua | default dict }} +{{- $resourcePlanningSpec := $ipamSpec.resourcePlanning | default dict }} +{{- $resourcePlanningIpv4Spec := $resourcePlanningSpec.ipv4 | default dict }} +{{- $resourcePlanningIpv6Spec := $resourcePlanningSpec.ipv6 | default dict }} +{{- $resourcePlanningProviderConfigRef := $resourcePlanningSpec.providerConfigRef | default dict }} {{- $ipam := dict "ipv4" (dict "enabled" ($ipv4IpamSpec.enabled | default false) @@ -76,6 +80,21 @@ "poolId" ($ipv6GuaIpamSpec.poolId | default "") "netmaskLength" ($ipv6GuaIpamSpec.netmaskLength | default 56) ) + "resourcePlanning" (dict + "providerConfigRef" (dict + "name" ($resourcePlanningProviderConfigRef.name | default "default") + "kind" ($resourcePlanningProviderConfigRef.kind | default "ProviderConfig") + ) + "managementPolicies" ($resourcePlanningSpec.managementPolicies | default $managementPolicies) + "ipv4" (dict + "enabled" ($resourcePlanningIpv4Spec.enabled | default false) + "scopeId" ($resourcePlanningIpv4Spec.scopeId | default "") + ) + "ipv6" (dict + "enabled" ($resourcePlanningIpv6Spec.enabled | default false) + "scopeId" ($resourcePlanningIpv6Spec.scopeId | default "") + ) + ) }} # Subnet layout configuration diff --git a/functions/render/001-state-observed-vpc.yaml.gotmpl b/functions/render/001-state-observed-vpc.yaml.gotmpl index e0dc8b1..711023e 100644 --- a/functions/render/001-state-observed-vpc.yaml.gotmpl +++ b/functions/render/001-state-observed-vpc.yaml.gotmpl @@ -40,6 +40,7 @@ {{- $vpcIpv6IpamPoolId := $vpcAtProvider.ipv6IpamPoolId | default "" }} {{- $vpcIpv6Cidr := $vpcAtProvider.ipv6CidrBlock | default "" }} {{- $vpcIpv6AssociationId := $vpcAtProvider.ipv6AssociationId | default "" }} +{{- $vpcOwnerId := $vpcAtProvider.ownerId | default "" }} {{- $vpcReady := get $readiness "vpc" | default false }} # ============================================================================== @@ -54,8 +55,8 @@ "ipv6IpamPoolId" $vpcIpv6IpamPoolId "ipv6Cidr" $vpcIpv6Cidr "ipv6AssociationId" $vpcIpv6AssociationId + "ownerId" $vpcOwnerId ) "_readiness" $readiness "_raw" $raw )) }} - diff --git a/functions/render/0025-state-observed-ipam-planning.yaml.gotmpl b/functions/render/0025-state-observed-ipam-planning.yaml.gotmpl new file mode 100644 index 0000000..e93a2e9 --- /dev/null +++ b/functions/render/0025-state-observed-ipam-planning.yaml.gotmpl @@ -0,0 +1,23 @@ +# code: language=yaml +# +# Observe VPC resource-planning pools managed by the IPAM administrator account. +# Depends on: 001-state-observed-vpc.yaml.gotmpl +# + +{{- $raw := $state.observed._raw }} +{{- $readiness := $state.observed._readiness }} +{{- $planning := dict }} + +{{- range $family := list "ipv4" "ipv6" }} + {{- $resourceName := printf "ipam-planning-%s" $family }} + {{- $entry := get $raw $resourceName | default dict }} + {{- $resource := $entry.resource | default dict }} + {{- $status := $resource.status | default dict }} + {{- $atProvider := $status.atProvider | default dict }} + {{- $planning = set $planning $family (dict + "ready" (get $readiness $resourceName | default false) + "id" ($atProvider.id | default "") + ) }} +{{- end }} + +{{- $state = set $state "observed" (merge $state.observed (dict "ipamPlanning" $planning)) }} diff --git a/functions/render/007-state-network-subnets.yaml.gotmpl b/functions/render/007-state-network-subnets.yaml.gotmpl index 504b4c4..3cdf253 100644 --- a/functions/render/007-state-network-subnets.yaml.gotmpl +++ b/functions/render/007-state-network-subnets.yaml.gotmpl @@ -98,6 +98,23 @@ {{- $vpcIpv6Cidr := $state.network.vpc.ipv6Cidr }} {{- $subnetIpv4IpamPoolId := $eff.subnetLayout.ipv4PoolId }} {{- $subnetIpv6IpamPoolId := $eff.subnetLayout.ipv6PoolId }} + {{- $planning := $state.network.ipam.resourcePlanning }} + {{- $ipv4PlanningRequested := $planning.ipv4.enabled }} + {{- $ipv6PlanningRequested := $planning.ipv6.enabled }} + {{- if $ipv4PlanningRequested }} + {{- $subnetIpv4IpamPoolId = $state.observed.ipamPlanning.ipv4.id }} + {{- end }} + {{- if $ipv6PlanningRequested }} + {{- $subnetIpv6IpamPoolId = $state.observed.ipamPlanning.ipv6.id }} + {{- end }} + {{- $planningPending := or + (and $ipv4PlanningRequested (eq $subnetIpv4IpamPoolId "")) + (and $ipv6PlanningRequested (eq $subnetIpv6IpamPoolId "")) + }} + + # Hold subnets until every requested planning pool exists. Rendering CIDR-based + # subnets while a planning pool is pending would create the wrong immutable shape. + {{- if not $planningPending }} # -------------------------------------------------------------------------- # IPAM Subnet Pool Path: allocate CIDRs from IPAM pool @@ -372,6 +389,7 @@ {{- end }} {{- end }} + {{- end }} {{- end }} # ============================================================================== diff --git a/functions/render/009-state-status.yaml.gotmpl b/functions/render/009-state-status.yaml.gotmpl index bf132bb..f21780e 100644 --- a/functions/render/009-state-status.yaml.gotmpl +++ b/functions/render/009-state-status.yaml.gotmpl @@ -22,6 +22,12 @@ {{- if $net.ipv6.gua.enabled }} {{- $ipamStatus = set $ipamStatus "ipv6Gua" (dict "cidr" ($obs.vpc.ipv6Cidr | default "Pending")) }} {{- end }} + {{- if or $net.ipam.resourcePlanning.ipv4.enabled $net.ipam.resourcePlanning.ipv6.enabled }} + {{- $ipamStatus = set $ipamStatus "resourcePlanning" (dict + "ipv4PoolId" ($obs.ipamPlanning.ipv4.id | default "Pending") + "ipv6PoolId" ($obs.ipamPlanning.ipv6.id | default "Pending") + ) }} + {{- end }} {{- end }} # ============================================================================== @@ -170,8 +176,20 @@ # ============================================================================== # Set $state.status slice # ============================================================================== +{{- $ready := $obs.vpc.ready }} +{{- if $net.ipam.resourcePlanning.ipv4.enabled }} + {{- $ready = and $ready $obs.ipamPlanning.ipv4.ready }} +{{- end }} +{{- if $net.ipam.resourcePlanning.ipv6.enabled }} + {{- $ready = and $ready $obs.ipamPlanning.ipv6.ready }} +{{- end }} +{{- range $subnet := concat $net.subnets.public $net.subnets.private }} + {{- $subnetObs := get $obs.subnets $subnet.resourceName | default dict }} + {{- $ready = and $ready ($subnetObs.ready | default false) }} +{{- end }} + {{- $status := dict - "ready" $obs.vpc.ready + "ready" $ready "network" $networkStatus "spec" (dict "raw" $state.spec.raw diff --git a/functions/render/20-ipam-planning-pools.yaml.gotmpl b/functions/render/20-ipam-planning-pools.yaml.gotmpl new file mode 100644 index 0000000..94e4bca --- /dev/null +++ b/functions/render/20-ipam-planning-pools.yaml.gotmpl @@ -0,0 +1,123 @@ +# code: language=yaml +# +# VPC resource-planning IPAM pools. +# These are managed from the IPAM administrator account after the workload VPC exists. +# Depends on: $state.network, $state.observed.vpc +# + +{{- $planning := $state.network.ipam.resourcePlanning }} +{{- $vpc := $state.observed.vpc }} +{{- $canSourceVpc := and (ne $vpc.id "") (ne $vpc.ownerId "") }} + +{{- if and $canSourceVpc $planning.ipv4.enabled (ne $planning.ipv4.scopeId "") $state.network.ipam.ipv4.enabled (ne $state.network.ipam.ipv4.poolId "") }} +--- +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPool +metadata: + name: {{ $state.network.name }}-ipv4-planning + annotations: + {{ setResourceNameAnnotation "ipam-planning-ipv4" }} + labels: {{ $state.network.labels | toJson }} +spec: + managementPolicies: {{ $planning.managementPolicies | toJson }} + forProvider: + region: {{ $state.network.region }} + addressFamily: ipv4 + ipamScopeId: {{ $planning.ipv4.scopeId }} + locale: {{ $state.network.region }} + sourceIpamPoolId: {{ $state.network.ipam.ipv4.poolId }} + sourceResource: + resourceId: {{ $vpc.id }} + resourceOwner: {{ $vpc.ownerId | quote }} + resourceRegion: {{ $state.network.region }} + resourceType: vpc + tags: + Name: {{ $state.network.name }}-ipv4-planning + {{ $state.network.tags | toYaml | nindent 6 }} + providerConfigRef: + name: {{ $planning.providerConfigRef.name }} + kind: {{ $planning.providerConfigRef.kind }} + +{{- $planningObs := $state.observed.ipamPlanning.ipv4 }} +{{- if and $vpc.ready $planningObs.ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.network.name }}-delete-ipv4-planning-before-vpc + annotations: + {{ setResourceNameAnnotation "delete-ipv4-planning-before-vpc" }} +spec: + replayDeletion: true + of: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPC + resourceRef: + name: {{ $state.network.name }} + by: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPool + resourceRef: + name: {{ $state.network.name }}-ipv4-planning +{{- end }} +{{- end }} + +{{- $ipv6SourcePoolId := "" }} +{{- if $state.network.ipam.ipv6Gua.enabled }} + {{- $ipv6SourcePoolId = $state.network.ipam.ipv6Gua.poolId }} +{{- else if $state.network.ipam.ipv6Ula.enabled }} + {{- $ipv6SourcePoolId = $state.network.ipam.ipv6Ula.poolId }} +{{- end }} + +{{- if and $canSourceVpc $planning.ipv6.enabled (ne $planning.ipv6.scopeId "") (ne $ipv6SourcePoolId "") }} +--- +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPool +metadata: + name: {{ $state.network.name }}-ipv6-planning + annotations: + {{ setResourceNameAnnotation "ipam-planning-ipv6" }} + labels: {{ $state.network.labels | toJson }} +spec: + managementPolicies: {{ $planning.managementPolicies | toJson }} + forProvider: + region: {{ $state.network.region }} + addressFamily: ipv6 + ipamScopeId: {{ $planning.ipv6.scopeId }} + locale: {{ $state.network.region }} + sourceIpamPoolId: {{ $ipv6SourcePoolId }} + sourceResource: + resourceId: {{ $vpc.id }} + resourceOwner: {{ $vpc.ownerId | quote }} + resourceRegion: {{ $state.network.region }} + resourceType: vpc + tags: + Name: {{ $state.network.name }}-ipv6-planning + {{ $state.network.tags | toYaml | nindent 6 }} + providerConfigRef: + name: {{ $planning.providerConfigRef.name }} + kind: {{ $planning.providerConfigRef.kind }} + +{{- $planningObs := $state.observed.ipamPlanning.ipv6 }} +{{- if and $vpc.ready $planningObs.ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.network.name }}-delete-ipv6-planning-before-vpc + annotations: + {{ setResourceNameAnnotation "delete-ipv6-planning-before-vpc" }} +spec: + replayDeletion: true + of: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPC + resourceRef: + name: {{ $state.network.name }} + by: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPool + resourceRef: + name: {{ $state.network.name }}-ipv6-planning +{{- end }} +{{- end }} diff --git a/functions/render/50-subnets.yaml.gotmpl b/functions/render/50-subnets.yaml.gotmpl index 2bf72a4..3571aff 100644 --- a/functions/render/50-subnets.yaml.gotmpl +++ b/functions/render/50-subnets.yaml.gotmpl @@ -78,6 +78,50 @@ spec: resourceRef: name: {{ $subnet.name }} {{- end }} + +# Usages: Delete public subnet before automatic IPAM planning pools +{{- if and $state.network.ipam.resourcePlanning.ipv4.enabled $state.observed.ipamPlanning.ipv4.ready $subnetObs.ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.network.name }}-delete-{{ $subnet.resourceName }}-before-ipv4-planning + annotations: + {{ setResourceNameAnnotation (printf "delete-%s-before-ipv4-planning" $subnet.resourceName) }} +spec: + replayDeletion: true + of: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPool + resourceRef: + name: {{ $state.network.name }}-ipv4-planning + by: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: Subnet + resourceRef: + name: {{ $subnet.name }} +{{- end }} +{{- if and $state.network.ipam.resourcePlanning.ipv6.enabled $state.observed.ipamPlanning.ipv6.ready $subnetObs.ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.network.name }}-delete-{{ $subnet.resourceName }}-before-ipv6-planning + annotations: + {{ setResourceNameAnnotation (printf "delete-%s-before-ipv6-planning" $subnet.resourceName) }} +spec: + replayDeletion: true + of: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPool + resourceRef: + name: {{ $state.network.name }}-ipv6-planning + by: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: Subnet + resourceRef: + name: {{ $subnet.name }} +{{- end }} {{- end }} {{- end }} @@ -152,6 +196,50 @@ spec: resourceRef: name: {{ $subnet.name }} {{- end }} + +# Usages: Delete private subnet before automatic IPAM planning pools +{{- if and $state.network.ipam.resourcePlanning.ipv4.enabled $state.observed.ipamPlanning.ipv4.ready $subnetObs.ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.network.name }}-delete-{{ $subnet.resourceName }}-before-ipv4-planning + annotations: + {{ setResourceNameAnnotation (printf "delete-%s-before-ipv4-planning" $subnet.resourceName) }} +spec: + replayDeletion: true + of: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPool + resourceRef: + name: {{ $state.network.name }}-ipv4-planning + by: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: Subnet + resourceRef: + name: {{ $subnet.name }} +{{- end }} +{{- if and $state.network.ipam.resourcePlanning.ipv6.enabled $state.observed.ipamPlanning.ipv6.ready $subnetObs.ready }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.network.name }}-delete-{{ $subnet.resourceName }}-before-ipv6-planning + annotations: + {{ setResourceNameAnnotation (printf "delete-%s-before-ipv6-planning" $subnet.resourceName) }} +spec: + replayDeletion: true + of: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPool + resourceRef: + name: {{ $state.network.name }}-ipv6-planning + by: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: Subnet + resourceRef: + name: {{ $subnet.name }} +{{- end }} {{- end }} {{- end }} diff --git a/functions/render/999-status.yaml.gotmpl b/functions/render/999-status.yaml.gotmpl index 0f93321..c709936 100644 --- a/functions/render/999-status.yaml.gotmpl +++ b/functions/render/999-status.yaml.gotmpl @@ -28,6 +28,15 @@ status: ipv6Gua: cidr: {{ $s.ipam.ipv6Gua.cidr }} {{- end }} + {{- if $s.ipam.resourcePlanning }} + resourcePlanning: + {{- if $state.network.ipam.resourcePlanning.ipv4.enabled }} + ipv4PoolId: {{ $s.ipam.resourcePlanning.ipv4PoolId }} + {{- end }} + {{- if $state.network.ipam.resourcePlanning.ipv6.enabled }} + ipv6PoolId: {{ $s.ipam.resourcePlanning.ipv6PoolId }} + {{- end }} + {{- end }} {{- end }} network: name: {{ $net.name }} From 3f6d49d84857708c333bfe7557ce02060aa51ce3 Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Tue, 25 Aug 2026 04:48:58 -0500 Subject: [PATCH 3/4] feat: provision VPC CIDRs for IPAM subnet planning --- README.md | 7 ++ apis/networks/definition.yaml | 13 +++ .../steps/2/ipam-planning-ipv4-cidr.yaml | 18 ++++ .../steps/2/ipam-planning-ipv6-cidr.yaml | 18 ++++ .../steps/3/ipam-planning-ipv4-cidr.yaml | 18 ++++ .../steps/3/ipam-planning-ipv6-cidr.yaml | 18 ++++ .../steps/4/ipam-planning-ipv4-cidr.yaml | 18 ++++ .../steps/4/ipam-planning-ipv6-cidr.yaml | 18 ++++ functions/render/000-state-init.yaml.gotmpl | 3 + ...5-state-observed-ipam-planning.yaml.gotmpl | 8 ++ .../007-state-network-subnets.yaml.gotmpl | 12 +-- functions/render/009-state-status.yaml.gotmpl | 4 +- .../render/20-ipam-planning-pools.yaml.gotmpl | 10 ++ .../render/21-ipam-planning-cidrs.yaml.gotmpl | 97 +++++++++++++++++++ 14 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4-cidr.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6-cidr.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4-cidr.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6-cidr.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4-cidr.yaml create mode 100644 examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6-cidr.yaml create mode 100644 functions/render/21-ipam-planning-cidrs.yaml.gotmpl diff --git a/README.md b/README.md index 7ab7399..1e11d88 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,11 @@ spec: ipv4: enabled: true scopeId: ipam-scope-private0123456789 + # externalName: ipam-pool-existing-ipv4-planning ipv6: enabled: true scopeId: ipam-scope-public0123456789 + # externalName: ipam-pool-existing-ipv6-planning subnetLayout: ipv6NetmaskLength: 64 availabilityZones: [a, b, c] @@ -108,6 +110,11 @@ For Amazon-provided IPv6 GUA, use a Regional IPAM pool (for example `/52`) to allocate a `/56` to each VPC. Subnets then allocate `/64`s from a VPC resource-planning pool whose source resource is that VPC. +When the planning pools and workload VPC are in different AWS accounts, share +the planning pools with the workload account or its OU through AWS RAM. The +Foundation `ramShares` API can manage this after the Network status exposes the +planning pool IDs. + ### IPv6 Benefits - **EKS Auto Mode** - IPv6 prevents IP exhaustion when scaling - **Future-proof** - Native dual-stack from day one diff --git a/apis/networks/definition.yaml b/apis/networks/definition.yaml index 7a41c7e..74eaedf 100644 --- a/apis/networks/definition.yaml +++ b/apis/networks/definition.yaml @@ -157,6 +157,9 @@ spec: scopeId: type: string description: Private IPAM scope ID in the IPAM administrator account. + externalName: + type: string + description: Existing IPv4 VPC resource-planning pool ID to import. ipv6: type: object description: IPv6 VPC resource-planning pool configuration. @@ -168,6 +171,16 @@ spec: scopeId: type: string description: IPAM scope ID containing the regional IPv6 source pool. + externalName: + type: string + description: Existing IPv6 VPC resource-planning pool ID to import. + publicIpSource: + type: string + description: Public IPv6 source inherited from the regional GUA pool. + enum: + - amazon + - byoip + default: amazon # Subnet layout - used with both IPAM and manual VPC CIDR subnetLayout: diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4-cidr.yaml new file mode 100644 index 0000000..429cd33 --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4-cidr.yaml @@ -0,0 +1,18 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPoolCidr +metadata: + namespace: infra + name: with-ipam-ipv4-planning-cidr + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv4-cidr + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv4-cidr +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv4-0123456789abcdef0_10.0.0.0/16 + ipamPoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 + cidr: 10.0.0.0/16 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6-cidr.yaml new file mode 100644 index 0000000..3db9c39 --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6-cidr.yaml @@ -0,0 +1,18 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPoolCidr +metadata: + namespace: infra + name: with-ipam-ipv6-planning-cidr + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv6-cidr + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv6-cidr +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv6-0123456789abcdef0_2600:1f00::/56 + ipamPoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 + cidr: 2600:1f00::/56 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4-cidr.yaml new file mode 100644 index 0000000..429cd33 --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4-cidr.yaml @@ -0,0 +1,18 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPoolCidr +metadata: + namespace: infra + name: with-ipam-ipv4-planning-cidr + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv4-cidr + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv4-cidr +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv4-0123456789abcdef0_10.0.0.0/16 + ipamPoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 + cidr: 10.0.0.0/16 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6-cidr.yaml new file mode 100644 index 0000000..3db9c39 --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6-cidr.yaml @@ -0,0 +1,18 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPoolCidr +metadata: + namespace: infra + name: with-ipam-ipv6-planning-cidr + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv6-cidr + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv6-cidr +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv6-0123456789abcdef0_2600:1f00::/56 + ipamPoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 + cidr: 2600:1f00::/56 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4-cidr.yaml new file mode 100644 index 0000000..429cd33 --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4-cidr.yaml @@ -0,0 +1,18 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPoolCidr +metadata: + namespace: infra + name: with-ipam-ipv4-planning-cidr + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv4-cidr + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv4-cidr +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv4-0123456789abcdef0_10.0.0.0/16 + ipamPoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 + cidr: 10.0.0.0/16 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6-cidr.yaml new file mode 100644 index 0000000..3db9c39 --- /dev/null +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6-cidr.yaml @@ -0,0 +1,18 @@ +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPoolCidr +metadata: + namespace: infra + name: with-ipam-ipv6-planning-cidr + annotations: + crossplane.io/composition-resource-name: ipam-planning-ipv6-cidr + gotemplating.fn.crossplane.io/composition-resource-name: ipam-planning-ipv6-cidr +status: + conditions: + - type: Ready + status: "True" + - type: Synced + status: "True" + atProvider: + id: ipam-pool-vpc-ipv6-0123456789abcdef0_2600:1f00::/56 + ipamPoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 + cidr: 2600:1f00::/56 diff --git a/functions/render/000-state-init.yaml.gotmpl b/functions/render/000-state-init.yaml.gotmpl index 0a54aad..e67d142 100644 --- a/functions/render/000-state-init.yaml.gotmpl +++ b/functions/render/000-state-init.yaml.gotmpl @@ -89,10 +89,13 @@ "ipv4" (dict "enabled" ($resourcePlanningIpv4Spec.enabled | default false) "scopeId" ($resourcePlanningIpv4Spec.scopeId | default "") + "externalName" ($resourcePlanningIpv4Spec.externalName | default "") ) "ipv6" (dict "enabled" ($resourcePlanningIpv6Spec.enabled | default false) "scopeId" ($resourcePlanningIpv6Spec.scopeId | default "") + "externalName" ($resourcePlanningIpv6Spec.externalName | default "") + "publicIpSource" ($resourcePlanningIpv6Spec.publicIpSource | default "amazon") ) ) }} diff --git a/functions/render/0025-state-observed-ipam-planning.yaml.gotmpl b/functions/render/0025-state-observed-ipam-planning.yaml.gotmpl index e93a2e9..a1dda39 100644 --- a/functions/render/0025-state-observed-ipam-planning.yaml.gotmpl +++ b/functions/render/0025-state-observed-ipam-planning.yaml.gotmpl @@ -14,9 +14,17 @@ {{- $resource := $entry.resource | default dict }} {{- $status := $resource.status | default dict }} {{- $atProvider := $status.atProvider | default dict }} + {{- $cidrResourceName := printf "ipam-planning-%s-cidr" $family }} + {{- $cidrEntry := get $raw $cidrResourceName | default dict }} + {{- $cidrResource := $cidrEntry.resource | default dict }} + {{- $cidrStatus := $cidrResource.status | default dict }} + {{- $cidrAtProvider := $cidrStatus.atProvider | default dict }} {{- $planning = set $planning $family (dict "ready" (get $readiness $resourceName | default false) "id" ($atProvider.id | default "") + "cidrReady" (get $readiness $cidrResourceName | default false) + "cidrId" ($cidrAtProvider.id | default "") + "cidr" ($cidrAtProvider.cidr | default "") ) }} {{- end }} diff --git a/functions/render/007-state-network-subnets.yaml.gotmpl b/functions/render/007-state-network-subnets.yaml.gotmpl index 3cdf253..55a65fb 100644 --- a/functions/render/007-state-network-subnets.yaml.gotmpl +++ b/functions/render/007-state-network-subnets.yaml.gotmpl @@ -108,17 +108,17 @@ {{- $subnetIpv6IpamPoolId = $state.observed.ipamPlanning.ipv6.id }} {{- end }} {{- $planningPending := or - (and $ipv4PlanningRequested (eq $subnetIpv4IpamPoolId "")) - (and $ipv6PlanningRequested (eq $subnetIpv6IpamPoolId "")) + (and $ipv4PlanningRequested (or (eq $subnetIpv4IpamPoolId "") (eq $state.observed.ipamPlanning.ipv4.cidrId ""))) + (and $ipv6PlanningRequested (or (eq $subnetIpv6IpamPoolId "") (eq $state.observed.ipamPlanning.ipv6.cidrId ""))) }} - # Hold subnets until every requested planning pool exists. Rendering CIDR-based - # subnets while a planning pool is pending would create the wrong immutable shape. + # Hold subnets until every requested planning pool exists and its VPC CIDR has + # been provisioned. Gate on observed IDs, not transient readiness, so a later + # Ready=False cannot un-render and delete live subnets. {{- if not $planningPending }} # -------------------------------------------------------------------------- - # IPAM Subnet Pool Path: allocate CIDRs from IPAM pool - # No VPC CIDR dependency — subnets render immediately + # IPAM Subnet Pool Path: allocate CIDRs from provisioned planning pools # -------------------------------------------------------------------------- {{- if ne $subnetIpv4IpamPoolId "" }} diff --git a/functions/render/009-state-status.yaml.gotmpl b/functions/render/009-state-status.yaml.gotmpl index f21780e..89f548c 100644 --- a/functions/render/009-state-status.yaml.gotmpl +++ b/functions/render/009-state-status.yaml.gotmpl @@ -178,10 +178,10 @@ # ============================================================================== {{- $ready := $obs.vpc.ready }} {{- if $net.ipam.resourcePlanning.ipv4.enabled }} - {{- $ready = and $ready $obs.ipamPlanning.ipv4.ready }} + {{- $ready = and $ready $obs.ipamPlanning.ipv4.ready $obs.ipamPlanning.ipv4.cidrReady }} {{- end }} {{- if $net.ipam.resourcePlanning.ipv6.enabled }} - {{- $ready = and $ready $obs.ipamPlanning.ipv6.ready }} + {{- $ready = and $ready $obs.ipamPlanning.ipv6.ready $obs.ipamPlanning.ipv6.cidrReady }} {{- end }} {{- range $subnet := concat $net.subnets.public $net.subnets.private }} {{- $subnetObs := get $obs.subnets $subnet.resourceName | default dict }} diff --git a/functions/render/20-ipam-planning-pools.yaml.gotmpl b/functions/render/20-ipam-planning-pools.yaml.gotmpl index 94e4bca..366a947 100644 --- a/functions/render/20-ipam-planning-pools.yaml.gotmpl +++ b/functions/render/20-ipam-planning-pools.yaml.gotmpl @@ -17,6 +17,9 @@ metadata: name: {{ $state.network.name }}-ipv4-planning annotations: {{ setResourceNameAnnotation "ipam-planning-ipv4" }} + {{- if $planning.ipv4.externalName }} + crossplane.io/external-name: {{ $planning.ipv4.externalName | quote }} + {{- end }} labels: {{ $state.network.labels | toJson }} spec: managementPolicies: {{ $planning.managementPolicies | toJson }} @@ -77,12 +80,19 @@ metadata: name: {{ $state.network.name }}-ipv6-planning annotations: {{ setResourceNameAnnotation "ipam-planning-ipv6" }} + {{- if $planning.ipv6.externalName }} + crossplane.io/external-name: {{ $planning.ipv6.externalName | quote }} + {{- end }} labels: {{ $state.network.labels | toJson }} spec: managementPolicies: {{ $planning.managementPolicies | toJson }} forProvider: region: {{ $state.network.region }} addressFamily: ipv6 + {{- if $state.network.ipam.ipv6Gua.enabled }} + awsService: ec2 + publicIpSource: {{ $planning.ipv6.publicIpSource }} + {{- end }} ipamScopeId: {{ $planning.ipv6.scopeId }} locale: {{ $state.network.region }} sourceIpamPoolId: {{ $ipv6SourcePoolId }} diff --git a/functions/render/21-ipam-planning-cidrs.yaml.gotmpl b/functions/render/21-ipam-planning-cidrs.yaml.gotmpl new file mode 100644 index 0000000..48b93d1 --- /dev/null +++ b/functions/render/21-ipam-planning-cidrs.yaml.gotmpl @@ -0,0 +1,97 @@ +# code: language=yaml +# +# Provision the workload VPC CIDRs into its resource-planning pools. +# AWS requires this explicit step before the pools can allocate subnet CIDRs. +# Depends on: $state.network, $state.observed.vpc, $state.observed.ipamPlanning +# + +{{- $planning := $state.network.ipam.resourcePlanning }} +{{- $vpc := $state.observed.vpc }} + +{{- if and $planning.ipv4.enabled (ne $state.observed.ipamPlanning.ipv4.id "") (ne $vpc.cidr "") }} +--- +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPoolCidr +metadata: + name: {{ $state.network.name }}-ipv4-planning-cidr + annotations: + {{ setResourceNameAnnotation "ipam-planning-ipv4-cidr" }} + labels: {{ $state.network.labels | toJson }} +spec: + managementPolicies: {{ $planning.managementPolicies | toJson }} + forProvider: + region: {{ $state.network.region }} + ipamPoolIdRef: + name: {{ $state.network.name }}-ipv4-planning + cidr: {{ $vpc.cidr }} + providerConfigRef: + name: {{ $planning.providerConfigRef.name }} + kind: {{ $planning.providerConfigRef.kind }} + +{{- $cidrObs := $state.observed.ipamPlanning.ipv4 }} +{{- if and $cidrObs.ready $cidrObs.cidrReady }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.network.name }}-delete-ipv4-planning-cidr-before-ipv4-planning + annotations: + {{ setResourceNameAnnotation "delete-ipv4-planning-cidr-before-ipv4-planning" }} +spec: + replayDeletion: true + of: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPool + resourceRef: + name: {{ $state.network.name }}-ipv4-planning + by: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPoolCidr + resourceRef: + name: {{ $state.network.name }}-ipv4-planning-cidr +{{- end }} +{{- end }} + +{{- if and $planning.ipv6.enabled (ne $state.observed.ipamPlanning.ipv6.id "") (ne $vpc.ipv6Cidr "") }} +--- +apiVersion: ec2.aws.m.upbound.io/v1beta1 +kind: VPCIpamPoolCidr +metadata: + name: {{ $state.network.name }}-ipv6-planning-cidr + annotations: + {{ setResourceNameAnnotation "ipam-planning-ipv6-cidr" }} + labels: {{ $state.network.labels | toJson }} +spec: + managementPolicies: {{ $planning.managementPolicies | toJson }} + forProvider: + region: {{ $state.network.region }} + ipamPoolIdRef: + name: {{ $state.network.name }}-ipv6-planning + cidr: {{ $vpc.ipv6Cidr }} + providerConfigRef: + name: {{ $planning.providerConfigRef.name }} + kind: {{ $planning.providerConfigRef.kind }} + +{{- $cidrObs := $state.observed.ipamPlanning.ipv6 }} +{{- if and $cidrObs.ready $cidrObs.cidrReady }} +--- +apiVersion: protection.crossplane.io/v1beta1 +kind: Usage +metadata: + name: {{ $state.network.name }}-delete-ipv6-planning-cidr-before-ipv6-planning + annotations: + {{ setResourceNameAnnotation "delete-ipv6-planning-cidr-before-ipv6-planning" }} +spec: + replayDeletion: true + of: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPool + resourceRef: + name: {{ $state.network.name }}-ipv6-planning + by: + apiVersion: ec2.aws.m.upbound.io/v1beta1 + kind: VPCIpamPoolCidr + resourceRef: + name: {{ $state.network.name }}-ipv6-planning-cidr +{{- end }} +{{- end }} From 34a2e5237cb8e0982775b2814f33e28a5c67390a Mon Sep 17 00:00:00 2001 From: Patrick Lee Scott Date: Tue, 25 Aug 2026 16:36:08 -0500 Subject: [PATCH 4/4] fix: validate and document IPAM planning --- Makefile | 1 + README.md | 3 +++ apis/networks/definition.yaml | 3 +++ .../with-ipam/steps/2/ipam-planning-ipv4-cidr.yaml | 4 ++-- .../with-ipam/steps/2/ipam-planning-ipv6-cidr.yaml | 4 ++-- .../with-ipam/steps/3/ipam-planning-ipv4-cidr.yaml | 4 ++-- .../with-ipam/steps/3/ipam-planning-ipv6-cidr.yaml | 4 ++-- .../with-ipam/steps/4/ipam-planning-ipv4-cidr.yaml | 4 ++-- .../with-ipam/steps/4/ipam-planning-ipv6-cidr.yaml | 4 ++-- 9 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 912536a..68b11d1 100644 --- a/Makefile +++ b/Makefile @@ -123,6 +123,7 @@ validate\:%: generate-configuration @example="examples/networks/$*.yaml"; \ if [ -f "$$example" ]; then \ echo "=== Validating $$example ==="; \ + set -o pipefail; \ up composition render --xrd=$(DEFINITION) $(COMPOSITION) $$example \ --include-full-xr --quiet | \ crossplane resource validate $(XRD_DIR) --error-on-missing-schemas -; \ diff --git a/README.md b/README.md index 1e11d88..68adcb2 100644 --- a/README.md +++ b/README.md @@ -473,8 +473,11 @@ spec: | `resourcePlanning.providerConfigRef` | object | ProviderConfig for the IPAM administrator account | | `resourcePlanning.managementPolicies` | []string | Management operations for planning pools | | `resourcePlanning.ipv4.enabled` | boolean | Create an IPv4 planning pool sourced from the VPC | +| `resourcePlanning.ipv4.externalName` | string | Existing IPv4 planning pool ID to import | | `resourcePlanning.ipv4.scopeId` | string | Private IPAM scope containing the IPv4 regional pool | | `resourcePlanning.ipv6.enabled` | boolean | Create an IPv6 planning pool sourced from the VPC | +| `resourcePlanning.ipv6.externalName` | string | Existing IPv6 planning pool ID to import | +| `resourcePlanning.ipv6.publicIpSource` | string | IPv6 public address source (`amazon` or `byoip`) | | `resourcePlanning.ipv6.scopeId` | string | IPAM scope containing the IPv6 regional pool | ### spec.subnetLayout diff --git a/apis/networks/definition.yaml b/apis/networks/definition.yaml index 74eaedf..78a679f 100644 --- a/apis/networks/definition.yaml +++ b/apis/networks/definition.yaml @@ -103,6 +103,9 @@ spec: ipv6Gua: type: object description: IPv6 global unicast address (GUA) VPC allocation from IPAM. + x-kubernetes-validations: + - rule: "!has(self.enabled) || !self.enabled || (has(self.poolId) && self.poolId != '')" + message: poolId is required when ipv6Gua.enabled is true. properties: enabled: type: boolean diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4-cidr.yaml index 429cd33..e68a41d 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4-cidr.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv4-cidr.yaml @@ -13,6 +13,6 @@ status: - type: Synced status: "True" atProvider: - id: ipam-pool-vpc-ipv4-0123456789abcdef0_10.0.0.0/16 + id: ipam-pool-vpc-ipv4-0123456789abcdef0_10.100.0.0/16 ipamPoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 - cidr: 10.0.0.0/16 + cidr: 10.100.0.0/16 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6-cidr.yaml index 3db9c39..ecd546c 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6-cidr.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/2/ipam-planning-ipv6-cidr.yaml @@ -13,6 +13,6 @@ status: - type: Synced status: "True" atProvider: - id: ipam-pool-vpc-ipv6-0123456789abcdef0_2600:1f00::/56 + id: ipam-pool-vpc-ipv6-0123456789abcdef0_2600:1f26:45:8100::/56 ipamPoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 - cidr: 2600:1f00::/56 + cidr: 2600:1f26:45:8100::/56 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4-cidr.yaml index 429cd33..e68a41d 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4-cidr.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv4-cidr.yaml @@ -13,6 +13,6 @@ status: - type: Synced status: "True" atProvider: - id: ipam-pool-vpc-ipv4-0123456789abcdef0_10.0.0.0/16 + id: ipam-pool-vpc-ipv4-0123456789abcdef0_10.100.0.0/16 ipamPoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 - cidr: 10.0.0.0/16 + cidr: 10.100.0.0/16 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6-cidr.yaml index 3db9c39..ecd546c 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6-cidr.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/3/ipam-planning-ipv6-cidr.yaml @@ -13,6 +13,6 @@ status: - type: Synced status: "True" atProvider: - id: ipam-pool-vpc-ipv6-0123456789abcdef0_2600:1f00::/56 + id: ipam-pool-vpc-ipv6-0123456789abcdef0_2600:1f26:45:8100::/56 ipamPoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 - cidr: 2600:1f00::/56 + cidr: 2600:1f26:45:8100::/56 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4-cidr.yaml index 429cd33..e68a41d 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4-cidr.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv4-cidr.yaml @@ -13,6 +13,6 @@ status: - type: Synced status: "True" atProvider: - id: ipam-pool-vpc-ipv4-0123456789abcdef0_10.0.0.0/16 + id: ipam-pool-vpc-ipv4-0123456789abcdef0_10.100.0.0/16 ipamPoolId: ipam-pool-vpc-ipv4-0123456789abcdef0 - cidr: 10.0.0.0/16 + cidr: 10.100.0.0/16 diff --git a/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6-cidr.yaml b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6-cidr.yaml index 3db9c39..ecd546c 100644 --- a/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6-cidr.yaml +++ b/examples/test/mocks/observed-resources/with-ipam/steps/4/ipam-planning-ipv6-cidr.yaml @@ -13,6 +13,6 @@ status: - type: Synced status: "True" atProvider: - id: ipam-pool-vpc-ipv6-0123456789abcdef0_2600:1f00::/56 + id: ipam-pool-vpc-ipv6-0123456789abcdef0_2600:1f26:45:8100::/56 ipamPoolId: ipam-pool-vpc-ipv6-0123456789abcdef0 - cidr: 2600:1f00::/56 + cidr: 2600:1f26:45:8100::/56