Repository: kubernetes-sigs/cloud-provider-azure (go)

Issue:
LB Etag should be updated after deleting private link service
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!

If the matter is security related, please disclose it privately via https://kubernetes.io/security/
-->


#### What happened:

After PLS is removed, the LB Etag will change, but we fail to catch this change, leading to 412 error when updating LB.
https://github.com/kubernetes-sigs/cloud-provider-azure/blob/master/pkg/provider/azure_loadbalancer.go#L1929

#### What you expected to happen:
412 should not be returned.

#### How to reproduce it (as minimally and precisely as possible):

#### Anything else we need to know?:

#### Environment:

- Kubernetes version (use `kubectl version`):
- Cloud provider or hardware configuration:
- OS (e.g: `cat /etc/os-release`):
- Kernel (e.g. `uname -a`):
- Install tools:
- Network plugin and version (if this is a network-related bug):
- Others:


Pull request description:
[release-1.30] fix: Retry reconcileLoadBalancer after removing PLS
<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://git.k8s.io/community/contributors/guide/first-contribution.md#your-first-contribution and developer guide https://git.k8s.io/community/contributors/devel/development.md#development-guide
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request. For reference on required PR/issue labels, read here:
https://git.k8s.io/community/contributors/devel/sig-release/release.md#issuepr-kind-label
3. Ensure you have added or ran the appropriate tests for your PR: https://git.k8s.io/community/contributors/devel/sig-testing/testing.md
4. If you want *faster* PR reviews, read how: https://git.k8s.io/community/contributors/guide/pull-requests.md#best-practices-for-faster-reviews
5. If the PR is unfinished, see how to mark it: https://git.k8s.io/community/contributors/guide/pull-requests.md#marking-unfinished-pull-requests
-->

#### What type of PR is this?

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

/kind bug

#### What this PR does / why we need it:

The LB ETag would change after deleting PLS, causing 412 error when putting the load balancer. This PR retries reconcileLoadBalancer function immediately after removing PLS. In the next run, lb will be re-listed so the latest ETag would be fetched.

#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #9108

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
fix: Retry reconcileLoadBalancer after removing PLS
```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:

<!--
This section can be blank if this pull request does not require a release note.

When adding links which point to resources within git repositories, like
KEPs or supporting documentation, please reference a specific commit and avoid
linking directly to the master branch. This ensures that links reference a
specific point in time, rather than a document that may change over time.

See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files

Please use the following format for linking documentation:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->
```docs

```


Patch:
diff --git a/pkg/provider/azure_loadbalancer.go b/pkg/provider/azure_loadbalancer.go
index 42c64905b..e80c01566 100644
--- a/pkg/provider/azure_loadbalancer.go
+++ b/pkg/provider/azure_loadbalancer.go
@@ -102,13 +102,13 @@ func (az *Cloud) GetLoadBalancer(ctx context.Context, clusterName string, servic
 		return nil, az.existsPip(ctx, clusterName, service), err
 	}
 
-	_, _, status, _, existsLb, err := az.getServiceLoadBalancer(ctx, service, clusterName, nil, false, &existingLBs)
+	_, _, status, _, existsLb, _, err := az.getServiceLoadBalancer(ctx, service, clusterName, nil, false, &existingLBs)
 	if err != nil || existsLb {
 		return status, existsLb || az.existsPip(ctx, clusterName, service), err
 	}
 
 	flippedService := flipServiceInternalAnnotation(service)
-	_, _, status, _, existsLb, err = az.getServiceLoadBalancer(ctx, flippedService, clusterName, nil, false, &existingLBs)
+	_, _, status, _, existsLb, _, err = az.getServiceLoadBalancer(ctx, flippedService, clusterName, nil, false, &existingLBs)
 	if err != nil || existsLb {
 		return status, existsLb || az.existsPip(ctx, clusterName, service), err
 	}
@@ -136,11 +136,19 @@ func (az *Cloud) reconcileService(ctx context.Context, clusterName string, servi
 
 	logger.V(2).Info("Start reconciling Service", "lb", az.GetLoadBalancerName(ctx, clusterName, service))
 
-	lb, err := az.reconcileLoadBalancer(ctx, clusterName, service, nodes, true /* wantLb */)
+	lb, needRetry, err := az.reconcileLoadBalancer(ctx, clusterName, service, nodes, true /* wantLb */)
 	if err != nil {
 		logger.Error(err, "Failed to reconcile LoadBalancer")
 		return nil, err
 	}
+	if needRetry {
+		logger.V(2).Info("Reconciling Service again after deleting PLS, as the LB ETag has changed.")
+		lb, _, err = az.reconcileLoadBalancer(ctx, clusterName, service, nodes, true /* wantLb */)
+		if err != nil {
+			logger.Error(err, "Failed to reconcile LoadBalancer")
+			return nil, err
+		}
+	}
 
 	lbStatus, lbIPsPrimaryPIPs, fipConfigs, err := az.getServiceLoadBalancerStatus(ctx, service, lb)
 	if err != nil {
@@ -156,7 +164,7 @@ func (az *Cloud) reconcileService(ctx context.Context, clusterName string, servi
 	}
 
 	for _, fipConfig := range fipConfigs {
-		if err := az.reconcilePrivateLinkService(ctx, clusterName, service, fipConfig, true /* wantPLS */); err != nil {
+		if _, err := az.reconcilePrivateLinkService(ctx, clusterName, service, fipConfig, true /* wantPLS */); err != nil {
 			logger.Error(err, "Failed to reconcile PrivateLinkService")
 			return nil, err
 		}
@@ -164,7 +172,7 @@ func (az *Cloud) reconcileService(ctx context.Context, clusterName string, servi
 
 	updateService := updateServiceLoadBalancerIPs(service, lbIPsPrimaryPIPs)
 	flippedService := flipServiceInternalAnnotation(updateService)
-	if _, err := az.reconcileLoadBalancer(ctx, clusterName, flippedService, nil, false /* wantLb */); err != nil {
+	if _, _, err := az.reconcileLoadBalancer(ctx, clusterName, flippedService, nil, false /* wantLb */); err != nil {
 		logger.Error(err, "Failed to reconcile flipped LoadBalancer")
 		return nil, err
 	}
@@ -464,7 +472,7 @@ func (az *Cloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName stri
 		}
 	}()
 
-	lb, _, _, lbIPsPrimaryPIPs, _, err := az.getServiceLoadBalancer(ctx, service, clusterName, nil, false, &[]network.LoadBalancer{})
+	lb, _, _, lbIPsPrimaryPIPs, _, _, err := az.getServiceLoadBalancer(ctx, service, clusterName, nil, false, &[]network.LoadBalancer{})
 	if err != nil && !retry.HasStatusForbiddenOrIgnoredError(err) {
 		return err
 	}
@@ -474,14 +482,20 @@ func (az *Cloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName stri
 		return err
 	}
 
-	_, err = az.reconcileLoadBalancer(ctx, clusterName, service, nil, false /* wantLb */)
+	_, needRetry, err := az.reconcileLoadBalancer(ctx, clusterName, service, nil, false /* wantLb */)
 	if err != nil && !retry.HasStatusForbiddenOrIgnoredError(err) {
 		return err
 	}
+	if needRetry {
+		_, _, err := az.reconcileLoadBalancer(ctx, clusterName, service, nil, false /* wantLb */)
+		if err != nil && !retry.HasStatusForbiddenOrIgnoredError(err) {
+			return err
+		}
+	}
 
 	// check flipped service also
 	flippedService := flipServiceInternalAnnotation(service)
-	if _, err := az.reconcileLoadBalancer(ctx, clusterName, flippedService, nil, false /* wantLb */); err != nil {
+	if _, _, err := az.reconcileLoadBalancer(ctx, clusterName, flippedService, nil, false /* wantLb */); err != nil {
 		return err
 	}
 
@@ -559,9 +573,9 @@ func (az *Cloud) shouldChangeLoadBalancer(service *v1.Service, currLBName, clust
 // removeFrontendIPConfigurationFromLoadBalancer removes the given ip configs from the load balancer
 // and delete the load balancer if there is no ip config on it. It returns the name of the deleted load balancer
 // and it will be used in reconcileLoadBalancer to remove the load balancer from the list.
-func (az *Cloud) removeFrontendIPConfigurationFromLoadBalancer(ctx context.Context, lb *network.LoadBalancer, existingLBs *[]network.LoadBalancer, fips []*network.FrontendIPConfiguration, clusterName string, service *v1.Service) (string, error) {
+func (az *Cloud) removeFrontendIPConfigurationFromLoadBalancer(ctx context.Context, lb *network.LoadBalancer, existingLBs *[]network.LoadBalancer, fips []*network.FrontendIPConfiguration, clusterName string, service *v1.Service) (string, bool /* deleted PLS */, error) {
 	if lb == nil || lb.LoadBalancerPropertiesFormat == nil || lb.FrontendIPConfigurations == nil {
-		return "", nil
+		return "", false, nil
 	}
 	fipConfigs := *lb.FrontendIPConfigurations
 	for i, fipConfig := range fipConfigs {
@@ -599,13 +613,24 @@ func (az *Cloud) removeFrontendIPConfigurationFromLoadBalancer(ctx context.Conte
 	}
 
 	// PLS does not support IPv6 so there will not be additional API calls.
+	var deletedPLS bool
 	for _, fip := range fips {
 		// clean up any private link service associated with the frontEndIPConfig
-		if err := az.reconcilePrivateLinkService(ctx, clusterName, service, fip, false /* wantPLS */); err != nil {
+		var (
+			deleted bool
+			err     error
+		)
+		if deleted, err = az.reconcilePrivateLinkService(ctx, clusterName, service, fip, false /* wantPLS */); err != nil {
 			klog.Errorf("removeFrontendIPConfigurationFromLoadBalancer(%s, %s, %s, %s): failed to clean up PLS: %v", ptr.Deref(lb.Name, ""), ptr.Deref(fip.Name, ""), clusterName, service.Name, err)
-			return "", err
+			return "", false, err
+		}
+		if deleted {
+			deletedPLS = true
 		}
 	}
+	if deletedPLS {
+		return "", true, nil
+	}
 
 	var deletedLBName string
 	fipNames := []string{}
@@ -618,7 +643,7 @@ func (az *Cloud) removeFrontendIPConfigurationFromLoadBalancer(ctx context.Conte
 		err := az.cleanOrphanedLoadBalancer(ctx, lb, *existingLBs, service, clusterName)
 		if err != nil {
 			klog.Errorf("%s: failed to cleanupOrphanedLoadBalancer: %v", logPrefix, err)
-			return "", err
+			return "", false, err
 		}
 		deletedLBName = pointer.StringDeref(lb.Name, "")
 	} else {
@@ -626,11 +651,11 @@ func (az *Cloud) removeFrontendIPConfigurationFromLoadBalancer(ctx context.Conte
 		err := az.CreateOrUpdateLB(ctx, service, *lb)
 		if err != nil {
 			klog.Errorf("%s: failed to CreateOrUpdateLB: %v", logPrefix, err)
-			return "", err
+			return "", false, err
 		}
 		_ = az.lbCache.Delete(pointer.StringDeref(lb.Name, ""))
 	}
-	return deletedLBName, nil
+	return deletedLBName, false, nil
 }
 
 func (az *Cloud) cleanOrphanedLoadBalancer(ctx context.Context, lb *network.LoadBalancer, existingLBs []network.LoadBalancer, service *v1.Service, clusterName string) error {
@@ -759,7 +784,7 @@ func (az *Cloud) getServiceLoadBalancer(
 	nodes []*v1.Node,
 	wantLb bool,
 	existingLBs *[]network.LoadBalancer,
-) (lb *network.LoadBalancer, refreshedLBs *[]network.LoadBalancer, status *v1.LoadBalancerStatus, lbIPsPrimaryPIPs []string, exists bool, err error) {
+) (lb *network.LoadBalancer, refreshedLBs *[]network.LoadBalancer, status *v1.LoadBalancerStatus, lbIPsPrimaryPIPs []string, exists, deletedPLS bool, err error) {
 	logger := log.FromContextOrBackground(ctx)
 
 	isInternal := requiresInternalLoadBalancer(service)
@@ -767,14 +792,14 @@ func (az *Cloud) getServiceLoadBalancer(
 	primaryVMSetName := az.VMSet.GetPrimaryVMSetName()
 	defaultLBName, err := az.getAzureLoadBalancerName(ctx, service, existingLBs, clusterName, primaryVMSetName, isInternal)
 	if err != nil {
-		return nil, nil, nil, nil, false, err
+		return nil, nil, nil, nil, false, false, err
 	}
 
 	// reuse the lb list from reconcileSharedLoadBalancer to reduce the api call
 	if existingLBs == nil || len(*existingLBs) == 0 {
 		lbs, err := az.ListLB(ctx, service)
 		if err != nil {
-			return nil, nil, nil, nil, false, err
+			return nil, nil, nil, nil, false, false, err
 		}
 		existingLBs = &lbs
 	}
@@ -794,7 +819,7 @@ func (az *Cloud) getServiceLoadBalancer(
 		var fipConfigs []*network.FrontendIPConfiguration
 		status, lbIPsPrimaryPIPs, fipConfigs, err = az.getServiceLoadBalancerStatus(ctx, service, &existingLB)
 		if err != nil {
-			return nil, nil, nil, nil, false, err
+			return nil, nil, nil, nil, false, false, err
 		}
 		if status == nil {
 			// service is not on this load balancer
@@ -814,10 +839,13 @@ func (az *Cloud) getServiceLoadBalancer(
 			for _, fipConfig := range fipConfigs {
 				fipConfigNames = append(fipConfigNames, pointer.StringDeref(fipConfig.Name, ""))
 			}
-			deletedLBName, err = az.removeFrontendIPConfigurationFromLoadBalancer(ctx, &existingLB, existingLBs, fipConfigs, clusterName, service)
+			deletedLBName, deletedPLS, err = az.removeFrontendIPConfigurationFromLoadBalancer(ctx, &existingLB, existingLBs, fipConfigs, clusterName, service)
 			if err != nil {
 				logger.Error(err, fmt.Sprintf("getServiceLoadBalancer(%s, %s, %v): failed to remove frontend IP configurations %q from load balancer", service.Name, clusterName, wantLb, fipConfigNames))
-				return nil, nil, nil, nil, false, err
+				return nil, nil, nil, nil, false, false, err
+			}
+			if deletedPLS {
+				return nil, nil, nil, nil, false, true, nil
 			}
 			if deletedLBName != "" {
 				removeLBFromList(existingLBs, deletedLBName)
@@ -842,7 +870,7 @@ func (az *Cloud) getServiceLoadBalancer(
 					newLBs, err := az.cleanupLocalServiceBackendPool(ctx, service, nodes, existingLBs, clusterName)
 					if err != nil {
 						logger.Error(err, fmt.Sprintf("getServiceLoadBalancer(%s, %s, %v): failed to cleanup backend pool for local service", service.Name, clusterName, wantLb))
-						return nil, nil, nil, nil, false, err
+						return nil, nil, nil, nil, false, false, err
 					}
 					existingLBs = newLBs
 				}
@@ -850,7 +878,7 @@ func (az *Cloud) getServiceLoadBalancer(
 			break
 		}
 
-		return &existingLB, existingLBs, status, lbIPsPrimaryPIPs, true, nil
+		return &existingLB, existingLBs, status, lbIPsPrimaryPIPs, true, false, nil
 	}
 
 	// Service does not have a load balancer, select one.
@@ -860,10 +888,10 @@ func (az *Cloud) getServiceLoadBalancer(
 		// select new load balancer for service
 		selectedLB, exists, err := az.selectLoadBalancer(ctx, clusterName, service, existingLBs, nodes)
 		if err != nil {
-			return nil, existingLBs, nil, nil, false, err
+			return nil, existingLBs, nil, nil, false, false, err
 		}
 
-		return selectedLB, existingLBs, status, lbIPsPrimaryPIPs, exists, err
+		return selectedLB, existingLBs, status, lbIPsPrimaryPIPs, exists, false, err
 	}
 
 	// If the service moves to a different load balancer, return the one
@@ -871,7 +899,7 @@ func (az *Cloud) getServiceLoadBalancer(
 	if shouldChangeLB {
 		for _, existingLB := range *existingLBs {
 			if strings.EqualFold(pointer.StringDeref(existingLB.Name, ""), defaultLBName) {
-				return &existingLB, existingLBs, status, lbIPsPrimaryPIPs, true, nil
+				return &existingLB, existingLBs, status, lbIPsPrimaryPIPs, true, false, nil
 			}
 		}
 	}
@@ -900,7 +928,7 @@ func (az *Cloud) getServiceLoadBalancer(
 		}
 	}
 
-	return defaultLB, existingLBs, nil, nil, false, nil
+	return defaultLB, existingLBs, nil, nil, false, false, nil
 }
 
 // selectLoadBalancer selects load balancer for the service in the cluster.
@@ -1778,19 +1806,19 @@ func (az *Cloud) reconcileMultipleStandardLoadBalancerConfigurations(
 // This also reconciles the Service's Ports with the LoadBalancer config.
 // This entails adding rules/probes for expected Ports and removing stale rules/ports.
 // nodes only used if wantLb is true
-func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node, wantLb bool) (*network.LoadBalancer, error) {
+func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node, wantLb bool) (*network.LoadBalancer, bool /*needRetry*/, error) {
 	isBackendPoolPreConfigured := az.isBackendPoolPreConfigured(service)
 	serviceName := getServiceName(service)
 	klog.V(2).Infof("reconcileLoadBalancer for service(%s) - wantLb(%t): started", serviceName, wantLb)
 
 	existingLBs, err := az.ListManagedLBs(ctx, service, nodes, clusterName)
 	if err != nil {
-		return nil, fmt.Errorf("reconcileLoadBalancer: failed to list managed LB: %w", err)
+		return nil, false, fmt.Errorf("reconcileLoadBalancer: failed to list managed LB: %w", err)
 	}
 
 	if existingLBs, err = az.cleanupBasicLoadBalancer(ctx, clusterName, service, existingLBs); err != nil {
 		klog.ErrorS(err, "reconcileLoadBalancer: failed to check and remove outdated basic load balancers", "service", serviceName)
-		return nil, err
+		return nil, false, err
 	}
 
 	// Delete backend pools for local service if:
@@ -1800,19 +1828,23 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 		existingLBs, err = az.cleanupLocalServiceBackendPool(ctx, service, nodes, existingLBs, clusterName)
 		if err != nil {
 			klog.Errorf("reconcileLoadBalancer: failed to cleanup local service backend pool for service %q, error: %s", serviceName, err.Error())
-			return nil, err
+			return nil, false, err
 		}
 	}
 
 	if err := az.reconcileMultipleStandardLoadBalancerConfigurations(ctx, existingLBs, service, clusterName, existingLBs, nodes); err != nil {
 		klog.Errorf("reconcileLoadBalancer: failed to reconcile multiple standard load balancer configurations: %s", err.Error())
-		return nil, err
+		return nil, false, err
 	}
 
-	lb, newLBs, lbStatus, _, _, err := az.getServiceLoadBalancer(ctx, service, clusterName, nodes, wantLb, existingLBs)
+	lb, newLBs, lbStatus, _, _, deletedPLS, err := az.getServiceLoadBalancer(ctx, service, clusterName, nodes, wantLb, existingLBs)
 	if err != nil {
 		klog.Errorf("reconcileLoadBalancer: failed to get load balancer for service %q, error: %v", serviceName, err)
-		return nil, err
+		return nil, false, err
+	}
+	if deletedPLS {
+		klog.V(2).InfoS("reconcileLoadBalancer: PLS is deleted and the LB ETag has changed, need to retry", "service", serviceName)
+		return lb, true, nil
 	}
 	existingLBs = newLBs
 
@@ -1836,7 +1868,7 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 		)
 		preConfig, backendPoolsUpdated, lb, err = az.LoadBalancerBackendPool.ReconcileBackendPools(ctx, clusterName, service, lb)
 		if err != nil {
-			return lb, err
+			return lb, false, err
 		}
 		if backendPoolsUpdated {
 			dirtyLb = true
@@ -1851,7 +1883,7 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 	// reconcile the load balancer's frontend IP configurations.
 	ownedFIPConfigs, toDeleteConfigs, fipChanged, err := az.reconcileFrontendIPConfigs(ctx, clusterName, service, lb, lbStatus, wantLb, lbFrontendIPConfigNames)
 	if err != nil {
-		return lb, err
+		return lb, false, err
 	}
 	if fipChanged {
 		dirtyLb = true
@@ -1863,7 +1895,7 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 			continue
 		}
 		if ownedFIPConfig.ID == nil {
-			return nil, fmt.Errorf("reconcileLoadBalancer for service (%s)(%t): nil ID for frontend IP config", serviceName, wantLb)
+			return nil, false, fmt.Errorf("reconcileLoadBalancer for service (%s)(%t): nil ID for frontend IP config", serviceName, wantLb)
 		}
 
 		var isIPv6 bool
@@ -1873,7 +1905,7 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 			isIPv6 = fipIPVersion == network.IPv6
 		} else {
 			if isIPv6, err = az.isFIPIPv6(service, ownedFIPConfig); err != nil {
-				return nil, err
+				return nil, false, err
 			}
 		}
 		lbFrontendIPConfigIDs[isIPv6] = *ownedFIPConfig.ID
@@ -1893,18 +1925,18 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 	v4Enabled, v6Enabled := getIPFamiliesEnabled(service)
 	if wantLb && v4Enabled {
 		if err = az.checkLoadBalancerResourcesConflicts(lb, lbFrontendIPConfigIDs[false], service); err != nil {
-			return nil, err
+			return nil, false, err
 		}
 		if err := getExpectedLBRule(consts.IPVersionIPv4); err != nil {
-			return nil, err
+			return nil, false, err
 		}
 	}
 	if wantLb && v6Enabled {
 		if err = az.checkLoadBalancerResourcesConflicts(lb, lbFrontendIPConfigIDs[true], service); err != nil {
-			return nil, err
+			return nil, false, err
 		}
 		if err := getExpectedLBRule(consts.IPVersionIPv6); err != nil {
-			return nil, err
+			return nil, false, err
 		}
 	}
 
@@ -1924,9 +1956,10 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 	// If it is not exist, and no change to that, we don't CreateOrUpdate LB
 	if dirtyLb {
 		if len(toDeleteConfigs) > 0 {
+			var needRetry bool
 			for i := range toDeleteConfigs {
 				fipConfigToDel := toDeleteConfigs[i]
-				err := az.reconcilePrivateLinkService(ctx, clusterName, service, &fipConfigToDel, false /* wantPLS */)
+				deletedPLS, err := az.reconcilePrivateLinkService(ctx, clusterName, service, &fipConfigToDel, false /* wantPLS */)
 				if err != nil {
 					klog.Errorf(
 						"reconcileLoadBalancer for service(%s): lb(%s) - failed to clean up PrivateLinkService for frontEnd(%s): %v",
@@ -1936,6 +1969,13 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 						err,
 					)
 				}
+				if deletedPLS {
+					needRetry = true
+				}
+			}
+			if needRetry {
+				klog.V(2).InfoS("reconcileLoadBalancer: PLS is deleted and the LB ETag has changed, need to retry", "service", serviceName)
+				return lb, true, nil
 			}
 		}
 
@@ -1943,24 +1983,24 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 			err := az.cleanOrphanedLoadBalancer(ctx, lb, *existingLBs, service, clusterName)
 			if err != nil {
 				klog.Errorf("reconcileLoadBalancer for service(%s): lb(%s) - failed to cleanOrphanedLoadBalancer: %v", serviceName, lbName, err)
-				return nil, err
+				return nil, false, err
 			}
 		} else {
 			klog.V(2).Infof("reconcileLoadBalancer: reconcileLoadBalancer for service(%s): lb(%s) - updating", serviceName, lbName)
 			err := az.CreateOrUpdateLB(ctx, service, *lb)
 			if err != nil {
 				klog.Errorf("reconcileLoadBalancer for service(%s) abort backoff: lb(%s) - updating: %s", serviceName, lbName, err.Error())
-				return nil, err
+				return nil, false, err
 			}
 
 			// Refresh updated lb which will be used later in other places.
 			newLB, exist, err := az.getAzureLoadBalancer(ctx, lbName, azcache.CacheReadTypeDefault)
 			if err != nil {
 				klog.Errorf("reconcileLoadBalancer for service(%s): getAzureLoadBalancer(%s) failed: %v", serviceName, lbName, err)
-				return nil, err
+				return nil, false, err
 			}
 			if !exist {
-				return nil, fmt.Errorf("load balancer %q not found", lbName)
+				return nil, false, fmt.Errorf("load balancer %q not found", lbName)
 			}
 			lb = newLB
 
@@ -1979,7 +2019,7 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 		if az.useMultipleStandardLoadBalancers() {
 			err := az.reconcileMultipleStandardLoadBalancerBackendNodes(ctx, clusterName, lbName, existingLBs, service, nodes, false)
 			if err != nil {
-				return nil, err
+				return nil, false, err
 			}
 		}
 
@@ -1994,7 +2034,7 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 		}
 		lb, err = az.reconcileBackendPoolHosts(ctx, lb, lbToReconcile, service, nodes, clusterName, vmSetName, lbBackendPoolIDs)
 		if err != nil {
-			return nil, err
+			return nil, false, err
 		}
 	}
 
@@ -2003,7 +2043,7 @@ func (az *Cloud) reconcileLoadBalancer(ctx context.Context, clusterName string,
 	}
 
 	klog.V(2).Infof("reconcileLoadBalancer for service(%s): lb(%s) finished", serviceName, lbName)
-	return lb, nil
+	return lb, false, nil
 }
 
 func (az *Cloud) reconcileBackendPoolHosts(
@@ -3182,7 +3222,7 @@ func (az *Cloud) shouldUpdateLoadBalancer(ctx context.Context, clusterName strin
 		return false, fmt.Errorf("shouldUpdateLoadBalancer: failed to list managed load balancers: %w", err)
 	}
 
-	_, _, _, _, existsLb, _ := az.getServiceLoadBalancer(ctx, service, clusterName, nodes, false, existingManagedLBs)
+	_, _, _, _, existsLb, _, _ := az.getServiceLoadBalancer(ctx, service, clusterName, nodes, false, existingManagedLBs)
 	return existsLb && service.ObjectMeta.DeletionTimestamp == nil && service.Spec.Type == v1.ServiceTypeLoadBalancer, nil
 }
 
diff --git a/pkg/provider/azure_privatelinkservice.go b/pkg/provider/azure_privatelinkservice.go
index 48852fe5d..359272892 100644
--- a/pkg/provider/azure_privatelinkservice.go
+++ b/pkg/provider/azure_privatelinkservice.go
@@ -47,7 +47,7 @@ func (az *Cloud) reconcilePrivateLinkService(
 	service *v1.Service,
 	fipConfig *network.FrontendIPConfiguration,
 	wantPLS bool,
-) error {
+) (bool /*deleted PLS*/, error) {
 	isinternal := requiresInternalLoadBalancer(service)
 	_, _, fipIPVersion := az.serviceOwnsFrontendIP(ctx, *fipConfig, service)
 	serviceName := getServiceName(service)
@@ -58,7 +58,7 @@ func (az *Cloud) reconcilePrivateLinkService(
 	} else {
 		if isIPv6, err = az.isFIPIPv6(service, fipConfig); err != nil {
 			klog.Errorf("reconcilePrivateLinkService for service(%s): failed to get FIP IP family: %v", serviceName, err)
-			return err
+			return false, err
 		}
 	}
 	createPLS := wantPLS && serviceRequiresPLS(service)
@@ -66,9 +66,9 @@ func (az *Cloud) reconcilePrivateLinkService(
 	if isIPv6 {
 		if isDualStack || !createPLS {
 			klog.V(2).Infof("IPv6 is not supported for private link service, skip reconcilePrivateLinkService for service(%s)", serviceName)
-			return nil
+			return false, nil
 		}
-		return fmt.Errorf("IPv6 is not supported for private link service")
+		return false, fmt.Errorf("IPv6 is not supported for private link service")
 	}
 
 	fipConfigID := fipConfig.ID
@@ -88,21 +88,21 @@ func (az *Cloud) reconcilePrivateLinkService(
 	if createPLS {
 		// Firstly, make sure it's internal service
 		if !isinternal && !consts.IsK8sServiceDisableLoadBalancerFloatingIP(service) {
-			return fmt.Errorf("reconcilePrivateLinkService for service(%s): service requiring private link service must be internal or disable floating ip", serviceName)
+			return false, fmt.Errorf("reconcilePrivateLinkService for service(%s): service requiring private link service must be internal or disable floating ip", serviceName)
 		}
 
 		// Secondly, check if there is a private link service already created
 		existingPLS, err := az.plsRepo.Get(ctx, az.getPLSResourceGroup(service), *fipConfigID, azcache.CacheReadTypeDefault)
 		if err != nil {
 			klog.Errorf("reconcilePrivateLinkService for service(%s): getPrivateLinkService(%s) failed: %v", serviceName, pointer.StringDeref(fipConfigID, ""), err)
-			return err
+			return false, err
 		}
 
 		exists := !strings.EqualFold(pointer.StringDeref(existingPLS.ID, ""), consts.PrivateLinkServiceNotExistID)
 		if exists {
 			klog.V(4).Infof("reconcilePrivateLinkService for service(%s): found existing private link service attached(%s)", serviceName, pointer.StringDeref(existingPLS.Name, ""))
 			if !isManagedPrivateLinkSerivce(existingPLS, clusterName) {
-				return fmt.Errorf(
+				return false, fmt.Errorf(
 					"reconcilePrivateLinkService for service(%s) failed: LB frontend(%s) already has unmanaged private link service(%s)",
 					serviceName,
 					pointer.StringDeref(fipConfigID, ""),
@@ -113,7 +113,7 @@ func (az *Cloud) reconcilePrivateLinkService(
 			ownerService := getPrivateLinkServiceOwner(existingPLS)
 			if !strings.EqualFold(ownerService, serviceName) {
 				if serviceHasAdditionalConfigs(service) {
-					return fmt.Errorf(
+					return false, fmt.Errorf(
 						"reconcilePrivateLinkService for service(%s) failed: LB frontend(%s) already has existing private link service(%s) owned by service(%s)",
 						serviceName,
 						pointer.StringDeref(fipConfigID, ""),
@@ -127,7 +127,7 @@ func (az *Cloud) reconcilePrivateLinkService(
 					pointer.StringDeref(existingPLS.Name, ""),
 					ownerService,
 				)
-				return nil
+				return false, nil
 			}
 		} else {
 			existingPLS.ID = nil
@@ -143,12 +143,12 @@ func (az *Cloud) reconcilePrivateLinkService(
 
 		plsName, err := az.getPrivateLinkServiceName(existingPLS, service, fipConfig)
 		if err != nil {
-			return err
+			return false, err
 		}
 
 		dirtyPLS, err := az.getExpectedPrivateLinkService(ctx, existingPLS, &plsName, &clusterName, service, fipConfig)
 		if err != nil {
-			return err
+			return false, err
 		}
 
 		if dirtyPLS {
@@ -156,20 +156,20 @@ func (az *Cloud) reconcilePrivateLinkService(
 			err := az.disablePLSNetworkPolicy(ctx, service)
 			if err != nil {
 				klog.Errorf("reconcilePrivateLinkService for service(%s) disable PLS network policy failed for pls(%s): %v", serviceName, plsName, err.Error())
-				return err
+				return false, err
 			}
 			existingPLS.Etag = pointer.String("")
 			_, err = az.plsRepo.CreateOrUpdate(ctx, az.getPLSResourceGroup(service), *existingPLS)
 			if err != nil {
 				klog.Errorf("reconcilePrivateLinkService for service(%s) abort backoff: pls(%s) - updating: %s", serviceName, plsName, err.Error())
-				return err
+				return false, err
 			}
 		}
 	} else if !wantPLS {
 		existingPLS, err := az.plsRepo.Get(ctx, az.getPLSResourceGroup(service), *fipConfigID, azcache.CacheReadTypeDefault)
 		if err != nil {
 			klog.Errorf("reconcilePrivateLinkService for service(%s): getPrivateLinkService(%s) failed: %v", serviceName, pointer.StringDeref(fipConfigID, ""), err)
-			return err
+			return false, err
 		}
 
 		exists := !strings.EqualFold(pointer.StringDeref(existingPLS.ID, ""), consts.PrivateLinkServiceNotExistID)
@@ -177,14 +177,17 @@ func (az *Cloud) reconcilePrivateLinkService(
 			deleteErr := az.safeDeletePLS(ctx, existingPLS, service)
 			if deleteErr != nil {
 				klog.Errorf("reconcilePrivateLinkService for service(%s): deletePLS for frontEnd(%s) failed: %v", serviceName, pointer.StringDeref(fipConfigID, ""), err)
-				return deleteErr
+				return false, deleteErr
 			}
+			isOperationSucceeded = true
+			klog.V(2).Infof("reconcilePrivateLinkService for service(%s) finished", serviceName)
+			return true, nil // return true for successfully deleted PLS
 		}
 	}
 
 	isOperationSucceeded = true
 	klog.V(2).Infof("reconcilePrivateLinkService for service(%s) finished", serviceName)
-	return nil
+	return false, nil
 }
 
 func (az *Cloud) getPLSResourceGroup(service *v1.Service) string {


Test patch:
diff --git a/pkg/provider/azure_loadbalancer_test.go b/pkg/provider/azure_loadbalancer_test.go
index 5ce258956..46881a52d 100644
--- a/pkg/provider/azure_loadbalancer_test.go
+++ b/pkg/provider/azure_loadbalancer_test.go
@@ -1906,7 +1906,7 @@ func TestGetServiceLoadBalancerMultiSLB(t *testing.T) {
 				tc.service.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyLocal
 			}
 
-			lb, lbs, _, _, _, err := cloud.getServiceLoadBalancer(context.TODO(), &tc.service, testClusterName,
+			lb, lbs, _, _, _, _, err := cloud.getServiceLoadBalancer(context.TODO(), &tc.service, testClusterName,
 				[]*v1.Node{}, true, &tc.existingLBs)
 			assert.Equal(t, tc.expectedError, err)
 			assert.Equal(t, tc.expectedLB, lb)
@@ -2085,7 +2085,7 @@ func TestGetServiceLoadBalancerCommon(t *testing.T) {
 			}
 			az.LoadBalancerSku = test.sku
 			service := test.service
-			lb, _, status, _, exists, err := az.getServiceLoadBalancer(context.TODO(), &service, testClusterName,
+			lb, _, status, _, exists, _, err := az.getServiceLoadBalancer(context.TODO(), &service, testClusterName,
 				clusterResources.nodes, test.wantLB, &[]network.LoadBalancer{})
 			assert.Equal(t, test.expectedLB, lb)
 			assert.Equal(t, test.expectedStatus, status)
@@ -2118,7 +2118,7 @@ func TestGetServiceLoadBalancerWithExtendedLocation(t *testing.T) {
 	mockLBsClient.EXPECT().List(gomock.Any(), "rg").Return(nil, nil)
 	az.LoadBalancerClient = mockLBsClient
 
-	lb, _, status, _, exists, err := az.getServiceLoadBalancer(context.TODO(), &service, testClusterName,
+	lb, _, status, _, exists, _, err := az.getServiceLoadBalancer(context.TODO(), &service, testClusterName,
 		clusterResources.nodes, false, &[]network.LoadBalancer{})
 	assert.Equal(t, expectedLB, lb, "GetServiceLoadBalancer shall return a default LB with expected location.")
 	assert.Nil(t, status, "GetServiceLoadBalancer: Status should be nil for default LB.")
@@ -2143,7 +2143,7 @@ func TestGetServiceLoadBalancerWithExtendedLocation(t *testing.T) {
 	mockLBsClient.EXPECT().List(gomock.Any(), "rg").Return(nil, nil)
 	az.LoadBalancerClient = mockLBsClient
 
-	lb, _, status, _, exists, err = az.getServiceLoadBalancer(context.TODO(), &service, testClusterName,
+	lb, _, status, _, exists, _, err = az.getServiceLoadBalancer(context.TODO(), &service, testClusterName,
 		clusterResources.nodes, true, &[]network.LoadBalancer{})
 	assert.Equal(t, expectedLB, lb, "GetServiceLoadBalancer shall return a new LB with expected location.")
 	assert.Nil(t, status, "GetServiceLoadBalancer: Status should be nil for new LB.")
@@ -4049,7 +4049,7 @@ func TestReconcileLoadBalancerCommon(t *testing.T) {
 			mockPLSRepo := az.plsRepo.(*privatelinkservice.MockRepository)
 			mockPLSRepo.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&armnetwork.PrivateLinkService{ID: to.Ptr(consts.PrivateLinkServiceNotExistID)}, nil).AnyTimes()
 
-			lb, rerr := az.reconcileLoadBalancer(context.TODO(), "testCluster", &service, clusterResources.nodes, test.wantLb)
+			lb, _, rerr := az.reconcileLoadBalancer(context.TODO(), "testCluster", &service, clusterResources.nodes, test.wantLb)
 			assert.Equal(t, test.expectedError, rerr)
 
 			if test.expectedError == nil {
@@ -6349,7 +6349,7 @@ func TestRemoveFrontendIPConfigurationFromLoadBalancerDelete(t *testing.T) {
 		mockPLSRepo := cloud.plsRepo.(*privatelinkservice.MockRepository)
 		mockPLSRepo.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&armnetwork.PrivateLinkService{ID: to.Ptr(consts.PrivateLinkServiceNotExistID)}, nil)
 		existingLBs := []network.LoadBalancer{{Name: ptr.To("lb")}}
-		_, err := cloud.removeFrontendIPConfigurationFromLoadBalancer(context.TODO(), &lb, &existingLBs, []*network.FrontendIPConfiguration{fip}, "testCluster", &service)
+		_, _, err := cloud.removeFrontendIPConfigurationFromLoadBalancer(context.TODO(), &lb, &existingLBs, []*network.FrontendIPConfiguration{fip}, "testCluster", &service)
 		assert.NoError(t, err)
 	})
 }
@@ -6379,7 +6379,7 @@ func TestRemoveFrontendIPConfigurationFromLoadBalancerUpdate(t *testing.T) {
 		mockLBClient.EXPECT().CreateOrUpdate(gomock.Any(), "rg", "lb", gomock.Any(), gomock.Any()).Return(nil)
 		mockPLSRepo := cloud.plsRepo.(*privatelinkservice.MockRepository)
 		mockPLSRepo.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&armnetwork.PrivateLinkService{ID: to.Ptr(consts.PrivateLinkServiceNotExistID)}, nil)
-		_, err := cloud.removeFrontendIPConfigurationFromLoadBalancer(context.TODO(), &lb, &[]network.LoadBalancer{}, []*network.FrontendIPConfiguration{fip}, "testCluster", &service)
+		_, _, err := cloud.removeFrontendIPConfigurationFromLoadBalancer(context.TODO(), &lb, &[]network.LoadBalancer{}, []*network.FrontendIPConfiguration{fip}, "testCluster", &service)
 		assert.NoError(t, err)
 	})
 }
diff --git a/pkg/provider/azure_privatelinkservice_test.go b/pkg/provider/azure_privatelinkservice_test.go
index cb8d0a6cb..b4732737b 100644
--- a/pkg/provider/azure_privatelinkservice_test.go
+++ b/pkg/provider/azure_privatelinkservice_test.go
@@ -55,6 +55,7 @@ func TestReconcilePrivateLinkService(t *testing.T) {
 		expectedPLS       *armnetwork.PrivateLinkService
 		expectedPLSDelete bool
 		expectedError     bool
+		expectedDeleted   bool // new field to test the deleted PLS return value
 	}{
 		{
 			desc:    "reconcilePrivateLinkService should do nothing if service does not create any PLS",
@@ -320,6 +321,42 @@ func TestReconcilePrivateLinkService(t *testing.T) {
 				},
 			},
 			expectedPLSDelete: true,
+			expectedDeleted:   true, // PLS should be successfully deleted
+		},
+		{
+			desc: "reconcilePrivateLinkService should delete existing PLS when wantPLS is false",
+			annotations: map[string]string{
+				consts.ServiceAnnotationPLSCreation:          "true",
+				consts.ServiceAnnotationLoadBalancerInternal: "true",
+				consts.ServiceAnnotationPLSName:              "testpls",
+			},
+			wantPLS:         false, // This is the key - we don't want PLS anymore
+			expectedPLSList: true,
+			existingPLSList: []*armnetwork.PrivateLinkService{
+				{
+					Name: ptr.To("testpls"),
+					Properties: &armnetwork.PrivateLinkServiceProperties{
+						LoadBalancerFrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{{ID: ptr.To("fipConfigID")}},
+					},
+					Tags: map[string]*string{
+						consts.ClusterNameTagKey:  ptr.To(testClusterName),
+						consts.OwnerServiceTagKey: ptr.To("default/test"),
+					},
+				},
+			},
+			expectedPLSDelete: true,
+			expectedDeleted:   true, // PLS should be successfully deleted and return true
+		},
+		{
+			desc: "reconcilePrivateLinkService should return false when wantPLS is false but no PLS exists",
+			annotations: map[string]string{
+				consts.ServiceAnnotationPLSCreation:          "true",
+				consts.ServiceAnnotationLoadBalancerInternal: "true",
+			},
+			wantPLS:         false, // We don't want PLS
+			expectedPLSList: true,
+			existingPLSList: []*armnetwork.PrivateLinkService{}, // No existing PLS
+			expectedDeleted: false,                              // Should return false since nothing was deleted
 		},
 	}
 	for _, test := range testCases {
@@ -364,8 +401,9 @@ func TestReconcilePrivateLinkService(t *testing.T) {
 			if test.expectedPLSDelete {
 				mockPLSRepo.EXPECT().Delete(gomock.Any(), "rg", "testpls", *fipConfig.ID).Return(nil).Times(1)
 			}
-			err := az.reconcilePrivateLinkService(context.TODO(), clusterName, &service, fipConfig, test.wantPLS)
+			deleted, err := az.reconcilePrivateLinkService(context.TODO(), clusterName, &service, fipConfig, test.wantPLS)
 			assert.Equal(t, test.expectedError, err != nil, "error: %v", err)
+			assert.Equal(t, test.expectedDeleted, deleted, "expected deleted PLS return value mismatch")
 		})
 	}
 }
diff --git a/pkg/provider/azure_test.go b/pkg/provider/azure_test.go
index f44f4697a..811d45d57 100644
--- a/pkg/provider/azure_test.go
+++ b/pkg/provider/azure_test.go
@@ -122,7 +122,7 @@ func TestAddPort(t *testing.T) {
 	mockPLSRepo := az.plsRepo.(*privatelinkservice.MockRepository)
 	mockPLSRepo.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&armnetwork.PrivateLinkService{ID: to.Ptr(consts.PrivateLinkServiceNotExistID)}, nil).MaxTimes(1)
 
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	assert.Nil(t, err)
 
 	// ensure we got a frontend ip configuration
@@ -906,7 +906,7 @@ func TestReconcileLoadBalancerAddServiceOnInternalSubnet(t *testing.T) {
 	}).AnyTimes()
 	mockLBBackendPool.EXPECT().EnsureHostsInPool(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	assert.Nil(t, err)
 
 	// ensure we got 2 frontend ip configurations
@@ -939,7 +939,7 @@ func TestReconcileLoadBalancerAddServicesOnMultipleSubnets(t *testing.T) {
 	mockPLSRepo.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&armnetwork.PrivateLinkService{ID: to.Ptr(consts.PrivateLinkServiceNotExistID)}, nil)
 
 	// svc1 is using LB without "-internal" suffix
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc1, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc1, clusterResources.nodes, true /* wantLb */)
 	if err != nil {
 		t.Errorf("Unexpected error reconciling svc1: %q", err)
 	}
@@ -956,7 +956,7 @@ func TestReconcileLoadBalancerAddServicesOnMultipleSubnets(t *testing.T) {
 	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 2, true)
 
 	// svc2 is using LB with "-internal" suffix
-	lb, err = az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc2, clusterResources.nodes, true /* wantLb */)
+	lb, _, err = az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc2, clusterResources.nodes, true /* wantLb */)
 	if err != nil {
 		t.Errorf("Unexpected error reconciling svc2: %q", err)
 	}
@@ -992,7 +992,7 @@ func TestReconcileLoadBalancerEditServiceSubnet(t *testing.T) {
 	mockPLSRepo.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&armnetwork.PrivateLinkService{ID: to.Ptr(consts.PrivateLinkServiceNotExistID)}, nil).AnyTimes()
 	az.plsRepo = mockPLSRepo
 
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	if err != nil {
 		t.Errorf("Unexpected error reconciling initial svc: %q", err)
 	}
@@ -1005,7 +1005,7 @@ func TestReconcileLoadBalancerEditServiceSubnet(t *testing.T) {
 	expectedLBs = make([]network.LoadBalancer, 0)
 	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 1, true)
 
-	lb, err = az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	lb, _, err = az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	if err != nil {
 		t.Errorf("Unexpected error reconciling edits to svc: %q", err)
 	}
@@ -1041,7 +1041,7 @@ func TestReconcileLoadBalancerNodeHealth(t *testing.T) {
 	mockPLSRepo.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
 		Return(&armnetwork.PrivateLinkService{ID: to.Ptr(consts.PrivateLinkServiceNotExistID)}, nil).AnyTimes()
 
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	assert.Nil(t, err)
 
 	// ensure we got a frontend ip configuration
@@ -1074,7 +1074,7 @@ func TestReconcileLoadBalancerRemoveService(t *testing.T) {
 	mockPLSRepo.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
 		Return(&armnetwork.PrivateLinkService{ID: to.Ptr(consts.PrivateLinkServiceNotExistID)}, nil).AnyTimes()
 
-	_, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	_, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	assert.Nil(t, err)
 
 	expectedLBs[0].FrontendIPConfigurations = &[]network.FrontendIPConfiguration{}
@@ -1085,7 +1085,7 @@ func TestReconcileLoadBalancerRemoveService(t *testing.T) {
 	mockLBsClient.EXPECT().List(gomock.Any(), az.ResourceGroup).Return(expectedLBs, nil).MaxTimes(3)
 	mockLBsClient.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
 
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, false /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, false /* wantLb */)
 	assert.Nil(t, err)
 
 	// ensure we abandoned the frontend ip configuration
@@ -1117,7 +1117,7 @@ func TestReconcileLoadBalancerRemoveAllPortsRemovesFrontendConfig(t *testing.T)
 	mockPLSRepo := az.plsRepo.(*privatelinkservice.MockRepository)
 	mockPLSRepo.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&armnetwork.PrivateLinkService{ID: to.Ptr(consts.PrivateLinkServiceNotExistID)}, nil).AnyTimes()
 
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	assert.Nil(t, err)
 	validateLoadBalancer(t, lb, svc)
 
@@ -1131,7 +1131,7 @@ func TestReconcileLoadBalancerRemoveAllPortsRemovesFrontendConfig(t *testing.T)
 	mockLBsClient.EXPECT().List(gomock.Any(), az.ResourceGroup).Return(expectedLBs, nil).MaxTimes(3)
 	mockLBsClient.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
 
-	lb, err = az.reconcileLoadBalancer(context.TODO(), testClusterName, &svcUpdated, clusterResources.nodes, false /* wantLb*/)
+	lb, _, err = az.reconcileLoadBalancer(context.TODO(), testClusterName, &svcUpdated, clusterResources.nodes, false /* wantLb*/)
 	assert.Nil(t, err)
 
 	// ensure we abandoned the frontend ip configuration
@@ -1162,13 +1162,13 @@ func TestReconcileLoadBalancerRemovesPort(t *testing.T) {
 	expectedLBs := make([]network.LoadBalancer, 0)
 	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 1, false)
 	svc := getTestServiceDualStack("service1", v1.ProtocolTCP, nil, 80, 443)
-	_, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	_, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	assert.Nil(t, err)
 
 	expectedLBs = make([]network.LoadBalancer, 0)
 	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 1, false)
 	svcUpdated := getTestServiceDualStack("service1", v1.ProtocolTCP, nil, 80)
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svcUpdated, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svcUpdated, clusterResources.nodes, true /* wantLb */)
 	assert.Nil(t, err)
 
 	validateLoadBalancer(t, lb, svcUpdated)
@@ -1203,12 +1203,12 @@ func TestReconcileLoadBalancerMultipleServices(t *testing.T) {
 
 	az.plsRepo = mockPLSRepo
 
-	_, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc1, clusterResources.nodes, true /* wantLb */)
+	_, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc1, clusterResources.nodes, true /* wantLb */)
 	assert.Nil(t, err)
 
 	setMockLBsDualStack(az, ctrl, &expectedLBs, "service", 1, 2, false)
 
-	updatedLoadBalancer, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc2, clusterResources.nodes, true /* wantLb */)
+	updatedLoadBalancer, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc2, clusterResources.nodes, true /* wantLb */)
 	assert.Nil(t, err)
 
 	validateLoadBalancer(t, updatedLoadBalancer, svc1, svc2)
@@ -1263,7 +1263,7 @@ func TestServiceDefaultsToNoSessionPersistence(t *testing.T) {
 	}).AnyTimes()
 	mockLBBackendPool.EXPECT().EnsureHostsInPool(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	if err != nil {
 		t.Errorf("Unexpected error reconciling svc1: %q", err)
 	}
@@ -1324,7 +1324,7 @@ func TestServiceRespectsNoSessionAffinity(t *testing.T) {
 	}).AnyTimes()
 	mockLBBackendPool.EXPECT().EnsureHostsInPool(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	if err != nil {
 		t.Errorf("Unexpected error reconciling svc1: %q", err)
 	}
@@ -1387,7 +1387,7 @@ func TestServiceRespectsClientIPSessionAffinity(t *testing.T) {
 	}).AnyTimes()
 	mockLBBackendPool.EXPECT().EnsureHostsInPool(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 
-	lb, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
+	lb, _, err := az.reconcileLoadBalancer(context.TODO(), testClusterName, &svc, clusterResources.nodes, true /* wantLb */)
 	if err != nil {
 		t.Errorf("Unexpected error reconciling svc1: %q", err)
 	}