Skip to content

Commit 70c78c2

Browse files
committed
OCPBUGS-38411: Not allow eipAllocations in CLB while changing the LBType of the Ingress Controller from NLB to Classic.
After changing the LBType of the IngressController from `NLB` to `Classic`, the eipAllocations still were attached with controller and the LB service. Howver, eipAllocations are not supported in Classic. This commit fixes service annotation `service.beta.kubernetes.io/aws-load-balancer-eip-allocations` value to a blank value. The CCM then removes this annotation from the service. It also sets the IngressController in Progressing Status to True with a status message to delete the load balancer service so that a new lb service is recreated for Classic LB.
1 parent 26f0181 commit 70c78c2

File tree

4 files changed

+191
-2
lines changed

4 files changed

+191
-2
lines changed

pkg/operator/controller/ingress/load_balancer_service.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,13 @@ func desiredLoadBalancerService(ci *operatorv1.IngressController, deploymentRef
441441
service.Annotations[awsLBSubnetsAnnotation] = JoinAWSSubnets(clbParams.Subnets, ",")
442442
}
443443
}
444+
445+
if eipAllocationsAWSEnabled {
446+
nlbParams := getAWSNetworkLoadBalancerParametersInSpec(ci)
447+
if nlbParams != nil && awsEIPAllocationsExist(nlbParams.EIPAllocations) {
448+
service.Annotations[awsEIPAllocationsAnnotation] = ""
449+
}
450+
}
444451
}
445452
}
446453
}
@@ -675,7 +682,16 @@ func loadBalancerServiceChanged(current, expected *corev1.Service) (bool, *corev
675682
// avoid problems, make sure the previous release blocks upgrades when
676683
// the user has modified an annotation or spec field that the new
677684
// release manages.
678-
changed, updated := loadBalancerServiceAnnotationsChanged(current, expected, managedLoadBalancerServiceAnnotations)
685+
686+
// Make eipAllocation annotation managed ONLY when LB type annotation changes.
687+
// Ordinarily, the eipAllocation annotation is not managed, so they are not immediately effectuated.
688+
// However, since eipAllocations are specific to NLB type, when LB type is changed from NLB to Classic, we MUST not use
689+
// eipAllocations associated with the NLB for Classic.
690+
managedAnnotationsUpdated := managedLoadBalancerServiceAnnotations
691+
if current.Annotations[AWSLBTypeAnnotation] != expected.Annotations[AWSLBTypeAnnotation] {
692+
managedAnnotationsUpdated = managedLoadBalancerServiceAnnotations.Union(sets.NewString(awsEIPAllocationsAnnotation))
693+
}
694+
changed, updated := loadBalancerServiceAnnotationsChanged(current, expected, managedAnnotationsUpdated)
679695

680696
// If spec.loadBalancerSourceRanges is nonempty on the service, that
681697
// means that allowedSourceRanges is nonempty on the ingresscontroller,
@@ -846,6 +862,7 @@ func loadBalancerServiceIsProgressing(ic *operatorv1.IngressController, service
846862
if nlbParams := getAWSNetworkLoadBalancerParametersInStatus(ic); nlbParams != nil {
847863
haveEIPAllocations = nlbParams.EIPAllocations
848864
}
865+
849866
if !awsEIPAllocationsEqual(wantEIPAllocations, haveEIPAllocations) {
850867
// Generate JSON for the oc patch command as well as "pretty" json
851868
// that will be used for a more human-readable error message.
@@ -856,6 +873,7 @@ func loadBalancerServiceIsProgressing(ic *operatorv1.IngressController, service
856873
ocPatchRevertCmd := fmt.Sprintf("oc -n openshift-ingress-operator patch ingresscontrollers/%[1]s --type=merge --patch='{\"spec\":{\"endpointPublishingStrategy\":{\"type\":\"LoadBalancerService\",\"loadBalancer\":{\"providerParameters\":{\"type\":\"AWS\",\"aws\":{\"type\":\"%[2]s\",\"%[3]s\":{\"eipAllocations\":%[4]s}}}}}}}'", ic.Name, getAWSLoadBalancerTypeInStatus(ic), "networkLoadBalancer", haveEIPAllocationsPatchJson)
857874
err := fmt.Errorf("%[1]s To effectuate this change, you must delete the service: `oc -n %[2]s delete svc/%[3]s`; the service load-balancer will then be deprovisioned and a new one created. This will most likely cause the new load-balancer to have a different host name and IP address and cause disruption. To return to the previous state, you can revert the change to the IngressController: `%[4]s`", changedMsg, service.Namespace, service.Name, ocPatchRevertCmd)
858875
errs = append(errs, err)
876+
859877
}
860878
}
861879

pkg/operator/controller/ingress/load_balancer_service_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,34 @@ func Test_desiredLoadBalancerService(t *testing.T) {
6161
}
6262
return eps
6363
}
64+
65+
clb = func(scope operatorv1.LoadBalancerScope) *operatorv1.EndpointPublishingStrategy {
66+
eps := lbs(scope)
67+
eps.LoadBalancer.ProviderParameters = &operatorv1.ProviderLoadBalancerParameters{
68+
Type: operatorv1.AWSLoadBalancerProvider,
69+
AWS: &operatorv1.AWSLoadBalancerParameters{
70+
Type: operatorv1.AWSClassicLoadBalancer,
71+
ClassicLoadBalancerParameters: &operatorv1.AWSClassicLoadBalancerParameters{},
72+
},
73+
}
74+
return eps
75+
}
76+
77+
clbWithEIPAllocations = func(scope operatorv1.LoadBalancerScope, eipAllocations []operatorv1.EIPAllocation) *operatorv1.EndpointPublishingStrategy {
78+
eps := lbs(scope)
79+
eps.LoadBalancer.ProviderParameters = &operatorv1.ProviderLoadBalancerParameters{
80+
Type: operatorv1.AWSLoadBalancerProvider,
81+
AWS: &operatorv1.AWSLoadBalancerParameters{
82+
Type: operatorv1.AWSClassicLoadBalancer,
83+
NetworkLoadBalancerParameters: &operatorv1.AWSNetworkLoadBalancerParameters{
84+
EIPAllocations: eipAllocations,
85+
},
86+
},
87+
}
88+
eps.LoadBalancer.ProviderParameters.AWS.NetworkLoadBalancerParameters.EIPAllocations = eipAllocations
89+
return eps
90+
}
91+
6492
awsLbWithSubnets = func(lbType operatorv1.AWSLoadBalancerType, subnets *operatorv1.AWSSubnets) *operatorv1.EndpointPublishingStrategy {
6593
eps := lbs(operatorv1.ExternalLoadBalancer)
6694
eps.LoadBalancer.ProviderParameters = &operatorv1.ProviderLoadBalancerParameters{
@@ -454,6 +482,33 @@ func Test_desiredLoadBalancerService(t *testing.T) {
454482
awsLBSubnetsAnnotation: {true, "subnet-00000000000000001,subnet-00000000000000002,subnetA,subnetB"},
455483
},
456484
},
485+
{
486+
description: "change from network load balancer with eipAllocations to classic load balancer type for aws platform when feature gate is enabled",
487+
platformStatus: platformStatus(configv1.AWSPlatformType),
488+
strategySpec: clbWithEIPAllocations(operatorv1.ExternalLoadBalancer,
489+
[]operatorv1.EIPAllocation{"eipalloc-xxxxxxxxxxxxxxxxx", "eipalloc-yyyyyyyyyyyyyyyyy"},
490+
),
491+
// setDefaultPublishingStrategy sets CLB empty provider parameters in the status of IC once the LB Type is changed from NLB to CLB.
492+
strategyStatus: clb(operatorv1.ExternalLoadBalancer),
493+
proxyNeeded: true,
494+
expectService: true,
495+
eipAllocationsAWSFeatureEnabled: true,
496+
expectedExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal,
497+
expectedServiceAnnotations: map[string]annotationExpectation{
498+
awsInternalLBAnnotation: {false, ""},
499+
awsLBAdditionalResourceTags: {false, ""},
500+
awsLBHealthCheckHealthyThresholdAnnotation: {true, awsLBHealthCheckHealthyThresholdDefault},
501+
awsLBHealthCheckIntervalAnnotation: {true, "5"},
502+
awsLBHealthCheckTimeoutAnnotation: {true, awsLBHealthCheckTimeoutDefault},
503+
awsLBHealthCheckUnhealthyThresholdAnnotation: {true, awsLBHealthCheckUnhealthyThresholdDefault},
504+
awsLBProxyProtocolAnnotation: {true, "*"},
505+
AWSLBTypeAnnotation: {false, AWSNLBAnnotation},
506+
localWithFallbackAnnotation: {true, ""},
507+
awsLBSubnetsAnnotation: {false, ""},
508+
// The CIO sets the annotation value to blank however, the CCM however removes the `service.beta.kubernetes.io/aws-load-balancer-eip-allocations` from service.
509+
awsEIPAllocationsAnnotation: {true, ""},
510+
},
511+
},
457512
{
458513
description: "network load balancer with eipAllocations for aws platform when feature gate is enabled",
459514
platformStatus: platformStatus(configv1.AWSPlatformType),

test/e2e/all_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func TestAll(t *testing.T) {
8888
t.Run("TestUnmanagedAWSLBSubnets", TestUnmanagedAWSLBSubnets)
8989
t.Run("TestAWSEIPAllocationsForNLB", TestAWSEIPAllocationsForNLB)
9090
t.Run("TestUnmanagedAWSEIPAllocations", TestUnmanagedAWSEIPAllocations)
91+
t.Run("TestAWSEIPAllocationsLBTypeChangeFromNLBToClassic", TestAWSEIPAllocationsLBTypeChangeFromNLBToClassic)
9192
})
9293

9394
t.Run("serial", func(t *testing.T) {

test/e2e/lb_eip_test.go

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,121 @@ func TestAWSEIPAllocationsForNLB(t *testing.T) {
180180
verifyExternalIngressController(t, externalTestPodName, testHostname, elbHostname)
181181
}
182182

183+
func TestAWSEIPAllocationsLBTypeChangeFromNLBToClassic(t *testing.T) {
184+
t.Parallel()
185+
if infraConfig.Status.PlatformStatus == nil {
186+
t.Skip("test skipped on nil platform")
187+
}
188+
if infraConfig.Status.PlatformStatus.Type != configv1.AWSPlatformType {
189+
t.Skipf("test skipped on platform %q", infraConfig.Status.PlatformStatus.Type)
190+
}
191+
if enabled, err := isFeatureGateEnabled(features.FeatureGateSetEIPForNLBIngressController); err != nil {
192+
t.Fatalf("failed to get feature gate: %v", err)
193+
} else if !enabled {
194+
t.Skipf("test skipped because %q feature gate is not enabled", features.FeatureGateSetEIPForNLBIngressController)
195+
}
196+
197+
// Create an ingress controller with EIPs mentioned in the Ingress Controller CR.
198+
var eipAllocations []operatorv1.EIPAllocation
199+
200+
ec2ServiceClient := createEC2ServiceClient(t, infraConfig)
201+
clusterName, err := getClusterName(infraConfig)
202+
if err != nil {
203+
t.Fatal(err)
204+
}
205+
vpcID, err := getVPCId(ec2ServiceClient, clusterName)
206+
if err != nil {
207+
t.Fatalf("failed to get VPC ID due to error: %v", err)
208+
}
209+
validEIPAllocations, err := createAWSEIPs(t, ec2ServiceClient, clusterName, vpcID)
210+
if err != nil {
211+
t.Fatalf("failed to create EIPs due to error: %v", err)
212+
}
213+
t.Cleanup(func() { assertEIPAllocationDeleted(t, ec2ServiceClient, 5*time.Minute, clusterName) })
214+
215+
for _, validEIPAllocation := range validEIPAllocations {
216+
eipAllocations = append(eipAllocations, operatorv1.EIPAllocation(validEIPAllocation))
217+
}
218+
219+
t.Logf("creating ingresscontroller with valid EIPs: %s", validEIPAllocations)
220+
name := types.NamespacedName{Namespace: operatorNamespace, Name: "lbtypechange"}
221+
domain := name.Name + "." + dnsConfig.Spec.BaseDomain
222+
ic := newLoadBalancerController(name, domain)
223+
ic.Spec.EndpointPublishingStrategy.LoadBalancer = &operatorv1.LoadBalancerStrategy{
224+
Scope: operatorv1.ExternalLoadBalancer,
225+
DNSManagementPolicy: operatorv1.ManagedLoadBalancerDNS,
226+
ProviderParameters: &operatorv1.ProviderLoadBalancerParameters{
227+
Type: operatorv1.AWSLoadBalancerProvider,
228+
AWS: &operatorv1.AWSLoadBalancerParameters{
229+
Type: operatorv1.AWSNetworkLoadBalancer,
230+
NetworkLoadBalancerParameters: &operatorv1.AWSNetworkLoadBalancerParameters{
231+
EIPAllocations: eipAllocations,
232+
},
233+
},
234+
},
235+
}
236+
237+
if err := kclient.Create(context.TODO(), ic); err != nil {
238+
t.Fatalf("failed to create ingresscontroller: %v", err)
239+
}
240+
t.Cleanup(func() {
241+
assertIngressControllerDeleted(t, kclient, ic)
242+
serviceName := controller.LoadBalancerServiceName(ic)
243+
// Waits for the service to clean up so EIPs can be released in the next t.Cleanup.
244+
if err := waitForIngressControllerServiceDeleted(t, ic, 4*time.Minute); err != nil {
245+
t.Errorf("failed to delete IngressController service %s/%s due to error: %v", serviceName.Namespace, serviceName.Name, err)
246+
}
247+
})
248+
249+
// Wait for the load balancer and DNS to be ready.
250+
if err := waitForIngressControllerCondition(t, kclient, 5*time.Minute, name, availableNotProgressingConditionsForIngressControllerWithLoadBalancer...); err != nil {
251+
t.Fatalf("failed to observe expected conditions: %v", err)
252+
}
253+
254+
// Get ELB host name from the service status for use with verifyExternalIngressController.
255+
// Using the ELB host name (not the IngressController wildcard domain) results in much quicker DNS resolution.
256+
elbHostname := getIngressControllerLBAddress(t, ic)
257+
258+
// Ensure the expected eipAllocation annotation is on the service.
259+
waitForLBAnnotation(t, ic, awsLBEIPAllocationAnnotation, true, ingress.JoinAWSEIPAllocations(eipAllocations, ","))
260+
261+
// Verify the eipAllocations status field is configured to what we expect.
262+
verifyIngressControllerEIPAllocationStatus(t, name)
263+
264+
// Verify we can reach the NLB with the provided eipAllocations.
265+
externalTestPodName := types.NamespacedName{Name: name.Name + "-external-verify", Namespace: name.Namespace}
266+
testHostname := "apps." + ic.Spec.Domain
267+
t.Logf("verifying external connectivity for ingresscontroller %q using an NLB with specified eipAllocations", ic.Name)
268+
verifyExternalIngressController(t, externalTestPodName, testHostname, elbHostname)
269+
270+
// Now, update the IngressController to change from NLB to Classic LBType.
271+
t.Logf("updating ingresscontroller %q to Classic LBType", ic.Name)
272+
if err := updateIngressControllerWithRetryOnConflict(t, name, 5*time.Minute, func(ic *operatorv1.IngressController) {
273+
ic.Spec.EndpointPublishingStrategy.LoadBalancer.ProviderParameters.AWS.Type = operatorv1.AWSClassicLoadBalancer
274+
}); err != nil {
275+
t.Fatalf("failed to update ingresscontroller: %v", err)
276+
}
277+
278+
//effectuateIngressControllerEIPAllocations(t, ic, nil, availableNotProgressingConditionsForIngressControllerWithLoadBalancer...)
279+
// Wait for the load balancer and DNS to be ready.
280+
if err := waitForIngressControllerCondition(t, kclient, 5*time.Minute, name, availableNotProgressingConditionsForIngressControllerWithLoadBalancer...); err != nil {
281+
t.Fatalf("failed to observe expected conditions: %v", err)
282+
}
283+
284+
// Ensure the expected eipAllocation annotation is on the service.
285+
waitForLBAnnotation(t, ic, awsLBEIPAllocationAnnotation, false, "")
286+
287+
// Get ELB host name from the service status for use with verifyExternalIngressController.
288+
// Using the ELB host name (not the IngressController wildcard domain) results in much quicker DNS resolution.
289+
elbHostname = getIngressControllerLBAddress(t, ic)
290+
291+
// Verify we can reach the CLB with the no eipAllocations.
292+
externalTestPodName = types.NamespacedName{Name: name.Name + "-external-verify", Namespace: name.Namespace}
293+
testHostname = "apps." + ic.Spec.Domain
294+
t.Logf("verifying external connectivity for ingresscontroller %q using an NLB with specified eipAllocations", ic.Name)
295+
verifyExternalIngressController(t, externalTestPodName, testHostname, elbHostname)
296+
}
297+
183298
// TestUnmanagedAWSEIPAllocations tests compatibility for unmanaged service.beta.kubernetes.io/aws-load-balancer-eip-allocations
184299
// annotations on the IngressController service. This is done by directly configuring the annotation on the service
185300
// and then updating the IngressController to match the unmanaged eipAllocation annotation.
@@ -357,7 +472,7 @@ func effectuateIngressControllerEIPAllocations(t *testing.T, ic *operatorv1.Ingr
357472
}
358473

359474
// Delete and recreate the IngressController service to effectuate.
360-
t.Logf("recreating the service to effectuate the subnets: %s/%s", controller.LoadBalancerServiceName(ic).Namespace, controller.LoadBalancerServiceName(ic).Namespace)
475+
t.Logf("recreating the service to effectuate the eipAllocations: %s/%s", controller.LoadBalancerServiceName(ic).Namespace, controller.LoadBalancerServiceName(ic).Namespace)
361476
if err := recreateIngressControllerService(t, ic); err != nil {
362477
t.Fatalf("failed to delete and recreate service: %v", err)
363478
}

0 commit comments

Comments
 (0)