Skip to content

Commit e0c6232

Browse files
committed
subnets: auto-assign IPv6 CIDR blocks to subnets when not specified
When creating subnets in a managed VPC with IPv6 enabled, automatically assign IPv6 CIDR blocks to subnets that have isIPv6=true but no explicit IPv6CidrBlock specified. This simplifies subnet configuration by allowing users to enable IPv6 without manually calculating and assigning individual subnet IPv6 CIDRs, for example, in case where VPC IPv6 CIDR is unknown pre-provisioning and AWS will assign one during VPC creation. Note: This logic only applies when spec.network.vpc.ipv6 is non-nil, subnets are managed and non-existing.
1 parent 1cca7b9 commit e0c6232

File tree

2 files changed

+512
-1
lines changed

2 files changed

+512
-1
lines changed

pkg/cloud/services/network/subnets.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,60 @@ func (s *Service) reconcileSubnets() error {
176176
return errors.Wrapf(err, "expected the zone attributes to be populated to subnet")
177177
}
178178

179+
// Auto-assign IPv6 CIDRs to subnets (new subnets not yet created) with isIPv6=true but no IPv6CidrBlock.
180+
// This only applies to managed VPCs with IPv6 enabled.
181+
if !unmanagedVPC && s.scope.VPC().IsIPv6Enabled() {
182+
// Collect subnets needing IPv6 assignment and track already-used IPv6 CIDRs.
183+
var subnetsRequiringIPv6Assignment []*infrav1.SubnetSpec
184+
usedIPv6CIDRs := make(map[string]bool)
185+
186+
for i := range subnets {
187+
subnet := &subnets[i]
188+
if subnet.IPv6CidrBlock != "" {
189+
usedIPv6CIDRs[subnet.IPv6CidrBlock] = true
190+
}
191+
// Only assign to subnets that don't exist yet (no ResourceID) and have isIPv6 but no IPv6CidrBlock.
192+
// This includes both dual-stack subnets and IPv6-only subnets.
193+
if subnet.ResourceID == "" && subnet.IsIPv6 && subnet.IPv6CidrBlock == "" {
194+
subnetsRequiringIPv6Assignment = append(subnetsRequiringIPv6Assignment, subnet)
195+
}
196+
}
197+
198+
if len(subnetsRequiringIPv6Assignment) > 0 {
199+
// Calculate total number of subnets needed including already assigned ones.
200+
totalSubnetsNeeded := len(usedIPv6CIDRs) + len(subnetsRequiringIPv6Assignment)
201+
202+
// Generate IPv6 subnet CIDRs from the VPC's IPv6 block.
203+
ipv6SubnetCIDRs, err := cidr.SplitIntoSubnetsIPv6(s.scope.VPC().IPv6.CidrBlock, totalSubnetsNeeded)
204+
if err != nil {
205+
return fmt.Errorf("failed splitting IPv6 VPC CIDR %q into subnets: %w", s.scope.VPC().IPv6.CidrBlock, err)
206+
}
207+
208+
// Assign available IPv6 CIDRs to subnets that need them.
209+
assignedCount := 0
210+
for _, subnetCIDR := range ipv6SubnetCIDRs {
211+
if assignedCount >= len(subnetsRequiringIPv6Assignment) {
212+
break
213+
}
214+
215+
cidrBlock := subnetCIDR.String()
216+
if !usedIPv6CIDRs[cidrBlock] {
217+
subnet := subnetsRequiringIPv6Assignment[assignedCount]
218+
subnet.IPv6CidrBlock = cidrBlock
219+
usedIPv6CIDRs[cidrBlock] = true
220+
221+
s.scope.Info("Auto-assigned IPv6 CIDR to subnet", "subnet-id", subnet.ID, "ipv6-cidr-block", cidrBlock)
222+
assignedCount++
223+
}
224+
}
225+
226+
// Verify all subnets were assigned.
227+
if assignedCount < len(subnetsRequiringIPv6Assignment) {
228+
return fmt.Errorf("failed to assign IPv6 CIDRs to all subnets: assigned %d out of %d", assignedCount, len(subnetsRequiringIPv6Assignment))
229+
}
230+
}
231+
}
232+
179233
// When the VPC is managed by CAPA, we need to create the subnets.
180234
if !unmanagedVPC {
181235
// Check that we need at least 1 public subnet after we have updated the metadata
@@ -498,7 +552,6 @@ func (s *Service) createSubnet(sn *infrav1.SubnetSpec) (*infrav1.SubnetSpec, err
498552
// Build the subnet creation request.
499553
input := &ec2.CreateSubnetInput{
500554
VpcId: aws.String(s.scope.VPC().ID),
501-
CidrBlock: aws.String(sn.CidrBlock),
502555
AvailabilityZone: aws.String(sn.AvailabilityZone),
503556
TagSpecifications: []types.TagSpecification{
504557
tags.BuildParamsToTagSpecification(
@@ -507,8 +560,23 @@ func (s *Service) createSubnet(sn *infrav1.SubnetSpec) (*infrav1.SubnetSpec, err
507560
),
508561
},
509562
}
563+
// Set IPv4 CIDR if provided (dual-stack or IPv4-only subnet).
564+
if sn.CidrBlock != "" {
565+
input.CidrBlock = aws.String(sn.CidrBlock)
566+
}
567+
// Set IPv6 CIDR if this is an IPv6 subnet (dual-stack or IPv6-only).
510568
if sn.IsIPv6 {
569+
if sn.IPv6CidrBlock == "" {
570+
err := fmt.Errorf("IPv6 CIDR block is required when isIpv6 is set to true")
571+
record.Warnf(s.scope.InfraCluster(), "FailedCreateSubnet", "Failed to create managed subnet: %v", err)
572+
return nil, err
573+
}
574+
511575
input.Ipv6CidrBlock = aws.String(sn.IPv6CidrBlock)
576+
// For IPv6-only subnets, we need to specify Ipv6Native.
577+
if sn.CidrBlock == "" {
578+
input.Ipv6Native = aws.Bool(true)
579+
}
512580
}
513581
out, err := s.EC2Client.CreateSubnet(context.TODO(), input)
514582
if err != nil {

0 commit comments

Comments
 (0)