From 15b74ced2857f9da1d4f789329d6d0f069c74a14 Mon Sep 17 00:00:00 2001 From: Thuan Vo Date: Wed, 10 Jun 2026 14:23:43 -0700 Subject: [PATCH] aws: set IPv6 block on AWSCluster NetworkSpec for dual-stack ipFamily When the infrastructure ipFamily is DualStackIPv4Primary or DualStackIPv6Primary, set the VPC IPv6 block on the AWSCluster so CAPA can create IPv6-capable resources. Co-Authored-By: Claude Opus 4.6 --- pkg/controllers/infracluster/aws.go | 12 +++- .../infracluster_controller_test.go | 67 +++++++++++++++++-- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/pkg/controllers/infracluster/aws.go b/pkg/controllers/infracluster/aws.go index b50e24834..fb3d5a700 100644 --- a/pkg/controllers/infracluster/aws.go +++ b/pkg/controllers/infracluster/aws.go @@ -32,6 +32,7 @@ import ( clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" "sigs.k8s.io/controller-runtime/pkg/client" + configv1 "github.com/openshift/api/config/v1" mapiv1beta1 "github.com/openshift/api/machine/v1beta1" ) @@ -82,7 +83,7 @@ func (r *InfraClusterController) ensureAWSCluster(ctx context.Context, log logr. return nil, fmt.Errorf("unable to obtain MAPI ProviderSpec: %w", err) } - awsCluster, err = r.newAWSCluster(providerSpec, apiURL, int32(port)) + awsCluster, err = r.newAWSCluster(providerSpec, apiURL, int32(port), r.Infra.Status.PlatformStatus.AWS.IPFamily) if err != nil { return nil, fmt.Errorf("failed to get AWSCluster: %w", err) } @@ -96,7 +97,7 @@ func (r *InfraClusterController) ensureAWSCluster(ctx context.Context, log logr. return awsCluster, nil } -func (r *InfraClusterController) newAWSCluster(providerSpec *mapiv1beta1.AWSMachineProviderConfig, apiURL *url.URL, port int32) (*awsv1.AWSCluster, error) { +func (r *InfraClusterController) newAWSCluster(providerSpec *mapiv1beta1.AWSMachineProviderConfig, apiURL *url.URL, port int32, ipFamily configv1.IPFamilyType) (*awsv1.AWSCluster, error) { controlPlaneLoadBalancer, secondaryControlPlaneLoadBalancer, err := extractLoadBalancerConfigFromMAPIAWSProviderSpec(providerSpec) if err != nil { return nil, fmt.Errorf("failed to extract control plane load balancer configuration: %w", err) @@ -132,6 +133,13 @@ func (r *InfraClusterController) newAWSCluster(providerSpec *mapiv1beta1.AWSMach }, } + // CAPA requires the VPC IPv6 block to be set to assign primary IPv6 addresses to instances. + // This is only applicable to IPv6 primary, but we set it for both dual-stack families + // to indicate that the VPC has IPv6 addressing enabled. + if ipFamily == configv1.DualStackIPv4Primary || ipFamily == configv1.DualStackIPv6Primary { + target.Spec.NetworkSpec.VPC.IPv6 = &awsv1.IPv6{} + } + return target, nil } diff --git a/pkg/controllers/infracluster/infracluster_controller_test.go b/pkg/controllers/infracluster/infracluster_controller_test.go index bfca1f5dc..635036543 100644 --- a/pkg/controllers/infracluster/infracluster_controller_test.go +++ b/pkg/controllers/infracluster/infracluster_controller_test.go @@ -54,10 +54,10 @@ var _ = Describe("InfraCluster", func() { bareInfraCluster *awsv1.AWSCluster capiNamespace *corev1.Namespace mapiNamespace *corev1.Namespace + ocpInfraAWS *configv1.Infrastructure ) ocpInfraClusterName := "test-infra-cluster-name" - ocpInfraAWS := configv1resourcebuilder.Infrastructure().AsAWS(ocpInfraClusterName, awsTestRegion).Build() infraClusterWithExternallyManagedByAnnotation := &awsv1.AWSCluster{ ObjectMeta: metav1.ObjectMeta{ @@ -88,6 +88,8 @@ var _ = Describe("InfraCluster", func() { } BeforeEach(func() { + ocpInfraAWS = configv1resourcebuilder.Infrastructure().AsAWS(ocpInfraClusterName, awsTestRegion).Build() + // Create ClusterOperator. Expect(cl.Create(ctx, configv1resourcebuilder.ClusterOperator().WithName(controllers.ClusterOperatorName).Build())).To(Succeed()) @@ -110,8 +112,9 @@ var _ = Describe("InfraCluster", func() { infraClusterWithExternallyManagedByAnnotation.Namespace = capiNamespace.Name infraClusterWithExternallyManagedByAnnotationWithValue.Namespace = capiNamespace.Name infraClusterWithoutExternallyManagedByAnnotation.Namespace = capiNamespace.Name + }) - // Setup and Start Manager. + JustBeforeEach(func() { mgrCtx, mgrCancel = context.WithCancel(context.Background()) mgrDone = make(chan struct{}) startManager(mgrCtx, mgrDone, ocpInfraAWS, capiNamespace.Name, mapiNamespace.Name) @@ -160,17 +163,21 @@ var _ = Describe("InfraCluster", func() { }) Context("When there are Control Plane Machines but no ControlPlaneMachineSet", func() { + var machine1, machine2, machine3 *mapiv1beta1.Machine + youngLoadBalancers := []mapiv1beta1.LoadBalancerReference{{Name: "young-int", Type: mapiv1beta1.NetworkLoadBalancerType}} oldLoadBalancers := []mapiv1beta1.LoadBalancerReference{{Name: "old-int", Type: mapiv1beta1.NetworkLoadBalancerType}} BeforeEach(func() { - machine1 := mapiv1beta1resourcebuilder.Machine().AsMaster().WithNamespace(mapiNamespace.Name).WithName("master-1").WithProviderSpecBuilder(mapiv1beta1resourcebuilder.AWSProviderSpec().WithLoadBalancers(oldLoadBalancers)).Build() - machine2 := mapiv1beta1resourcebuilder.Machine().AsMaster().WithNamespace(mapiNamespace.Name).WithName("master-2").WithProviderSpecBuilder(mapiv1beta1resourcebuilder.AWSProviderSpec().WithLoadBalancers(youngLoadBalancers)).Build() - machine3 := mapiv1beta1resourcebuilder.Machine().AsMaster().WithNamespace(mapiNamespace.Name).WithName("master-3").WithProviderSpecBuilder(mapiv1beta1resourcebuilder.AWSProviderSpec().WithLoadBalancers(oldLoadBalancers)).Build() + machine1 = mapiv1beta1resourcebuilder.Machine().AsMaster().WithNamespace(mapiNamespace.Name).WithName("master-1").WithProviderSpecBuilder(mapiv1beta1resourcebuilder.AWSProviderSpec().WithLoadBalancers(oldLoadBalancers)).Build() + machine2 = mapiv1beta1resourcebuilder.Machine().AsMaster().WithNamespace(mapiNamespace.Name).WithName("master-2").WithProviderSpecBuilder(mapiv1beta1resourcebuilder.AWSProviderSpec().WithLoadBalancers(youngLoadBalancers)).Build() + machine3 = mapiv1beta1resourcebuilder.Machine().AsMaster().WithNamespace(mapiNamespace.Name).WithName("master-3").WithProviderSpecBuilder(mapiv1beta1resourcebuilder.AWSProviderSpec().WithLoadBalancers(oldLoadBalancers)).Build() Expect(cl.Create(ctx, machine1)).To(Succeed()) Expect(cl.Create(ctx, machine3)).To(Succeed()) + }) + JustBeforeEach(func() { // Wait for the InfraCluster to be created. Eventually(komega.Get(bareInfraCluster)).Should(Succeed()) @@ -206,6 +213,56 @@ var _ = Describe("InfraCluster", func() { )) }) }) + + Context("When the infrastructure ipFamily is configured", func() { + BeforeEach(func() { + internalLB := []mapiv1beta1.LoadBalancerReference{{Name: ocpInfraClusterName + "-int", Type: mapiv1beta1.NetworkLoadBalancerType}} + machineTemplateBuilder := mapiv1resourcebuilder.OpenShiftMachineV1Beta1Template().WithProviderSpecBuilder( + mapiv1beta1resourcebuilder.AWSProviderSpec().WithLoadBalancers(internalLB), + ) + cpms := mapiv1resourcebuilder.ControlPlaneMachineSet().WithNamespace(mapiNamespace.Name).WithName("cluster").WithMachineTemplateBuilder(machineTemplateBuilder).Build() + Expect(cl.Create(ctx, cpms)).To(Succeed()) + }) + + Context("When ipFamily is DualStackIPv4Primary", func() { + BeforeEach(func() { + ocpInfraAWS.Status.PlatformStatus.AWS.IPFamily = configv1.DualStackIPv4Primary + }) + + It("should create an AWSCluster with IPv6 enabled in NetworkSpec", func() { + Eventually(komega.Object(bareInfraCluster)).Should(SatisfyAll( + HaveField("Status.Ready", BeTrue()), + HaveField("Spec.NetworkSpec.VPC.IPv6", Not(BeNil())), + )) + }) + }) + + Context("When ipFamily is DualStackIPv6Primary", func() { + BeforeEach(func() { + ocpInfraAWS.Status.PlatformStatus.AWS.IPFamily = configv1.DualStackIPv6Primary + }) + + It("should create an AWSCluster with IPv6 enabled in NetworkSpec", func() { + Eventually(komega.Object(bareInfraCluster)).Should(SatisfyAll( + HaveField("Status.Ready", BeTrue()), + HaveField("Spec.NetworkSpec.VPC.IPv6", Not(BeNil())), + )) + }) + }) + + Context("When ipFamily is IPv4", func() { + BeforeEach(func() { + ocpInfraAWS.Status.PlatformStatus.AWS.IPFamily = configv1.IPv4 + }) + + It("should create an AWSCluster without IPv6 in NetworkSpec", func() { + Eventually(komega.Object(bareInfraCluster)).Should(SatisfyAll( + HaveField("Status.Ready", BeTrue()), + HaveField("Spec.NetworkSpec.VPC.IPv6", BeNil()), + )) + }) + }) + }) }) Context("When there is an InfraCluster with no externally ManagedBy Annotation", func() {