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

Issue:
PublicIPReferencedByMultipleIPConfigs when reusing the same IP via pip-name annotation
<!-- 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:

PublicIPReferencedByMultipleIPConfigs failures would happen when reusing the same IP via service annotations.

Sample error message:

```
{
    "code": "PublicIPReferencedByMultipleIPConfigs",
    "message": "Public ip address /subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/pip is referenced by multiple ipconfigs in resource /subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/loadBalancers/kubernetes."
}
```

#### What you expected to happen:

The new service with pip-name annotation should succeed and reuse the public IP address when ports are not conflicted with existing rules.

#### 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:
[release-1.26] feat: support sharing IP address across services by public IP name
<!--  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 feature

#### What this PR does / why we need it:

We used to support sharing one pip across services by setting the IP address. This PR supports sharing by pip name.

#### 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 #4243

#### 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
feat: support sharing IP address across services by public IP name
```

#### 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 cf1e09ca3..c772f7562 100644
--- a/pkg/provider/azure_loadbalancer.go
+++ b/pkg/provider/azure_loadbalancer.go
@@ -902,7 +902,7 @@ func (az *Cloud) determinePublicIPName(clusterName string, service *v1.Service)
 
 	// For the services with loadBalancerIP set, an existing public IP is required, primary
 	// or secondary, or a public IP not found error would be reported.
-	pip, err := az.findMatchedPIPByLoadBalancerIP(service, loadBalancerIP, pipResourceGroup)
+	pip, err := az.findMatchedPIP(loadBalancerIP, "", pipResourceGroup)
 	if err != nil {
 		return "", false, err
 	}
@@ -914,20 +914,58 @@ func (az *Cloud) determinePublicIPName(clusterName string, service *v1.Service)
 	return "", false, fmt.Errorf("user supplied IP Address %s was not found in resource group %s", loadBalancerIP, pipResourceGroup)
 }
 
-func (az *Cloud) findMatchedPIPByLoadBalancerIP(service *v1.Service, loadBalancerIP, pipResourceGroup string) (*network.PublicIPAddress, error) {
+func (az *Cloud) findMatchedPIP(loadBalancerIP, pipName, pipResourceGroup string) (pip *network.PublicIPAddress, err error) {
 	pips, err := az.listPIP(pipResourceGroup, azcache.CacheReadTypeDefault)
 	if err != nil {
 		return nil, fmt.Errorf("findMatchedPIPByLoadBalancerIP: failed to listPIP: %w", err)
 	}
 
-	pip, err := getExpectedPIPFromListByIPAddress(pips, loadBalancerIP)
+	if loadBalancerIP != "" {
+		pip, err = az.findMatchedPIPByLoadBalancerIP(&pips, loadBalancerIP, pipResourceGroup)
+		if err != nil {
+			return nil, err
+		}
+		return pip, nil
+	}
+
+	if pipResourceGroup != "" {
+		pip, err = az.findMatchedPIPByName(&pips, pipName, pipResourceGroup)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return pip, nil
+}
+
+func (az *Cloud) findMatchedPIPByName(pips *[]network.PublicIPAddress, pipName, pipResourceGroup string) (*network.PublicIPAddress, error) {
+	for _, pip := range *pips {
+		if strings.EqualFold(pointer.StringDeref(pip.Name, ""), pipName) {
+			return &pip, nil
+		}
+	}
+
+	pipList, err := az.listPIP(pipResourceGroup, azcache.CacheReadTypeForceRefresh)
+	if err != nil {
+		return nil, fmt.Errorf("findMatchedPIPByName: failed to listPIP force refresh: %w", err)
+	}
+	for _, pip := range pipList {
+		if strings.EqualFold(pointer.StringDeref(pip.Name, ""), pipName) {
+			return &pip, nil
+		}
+	}
+
+	return nil, fmt.Errorf("findMatchedPIPByName: failed to find PIP %s in resource group %s", pipName, pipResourceGroup)
+}
+
+func (az *Cloud) findMatchedPIPByLoadBalancerIP(pips *[]network.PublicIPAddress, loadBalancerIP, pipResourceGroup string) (*network.PublicIPAddress, error) {
+	pip, err := getExpectedPIPFromListByIPAddress(*pips, loadBalancerIP)
 	if err != nil {
-		pips, err = az.listPIP(pipResourceGroup, azcache.CacheReadTypeForceRefresh)
+		pipList, err := az.listPIP(pipResourceGroup, azcache.CacheReadTypeForceRefresh)
 		if err != nil {
 			return nil, fmt.Errorf("findMatchedPIPByLoadBalancerIP: failed to listPIP force refresh: %w", err)
 		}
 
-		pip, err = getExpectedPIPFromListByIPAddress(pips, loadBalancerIP)
+		pip, err = getExpectedPIPFromListByIPAddress(pipList, loadBalancerIP)
 		if err != nil {
 			return nil, fmt.Errorf("findMatchedPIPByLoadBalancerIP: cannot find public IP with IP address %s in resource group %s", loadBalancerIP, pipResourceGroup)
 		}
@@ -3544,7 +3582,7 @@ func serviceOwnsPublicIP(service *v1.Service, pip *network.PublicIPAddress, clus
 
 		// if there is no service tag on the pip, it is user-created pip
 		if serviceTag == "" {
-			return isServiceLoadBalancerIPMatchesPIP(service, pip), true
+			return isServiceSelectPIP(service, pip), true
 		}
 
 		// if there is service tag on the pip, it is system-created pip
@@ -3560,18 +3598,26 @@ func serviceOwnsPublicIP(service *v1.Service, pip *network.PublicIPAddress, clus
 		}
 
 		// if the service is not included in the tags of the system-created pip, check the ip address
-		// this could happen for secondary services
-		return isServiceLoadBalancerIPMatchesPIP(service, pip), false
+		// or pip name, this could happen for secondary services
+		return isServiceSelectPIP(service, pip), false
 	}
 
 	// if the pip has no tags, it should be user-created
-	return isServiceLoadBalancerIPMatchesPIP(service, pip), true
+	return isServiceSelectPIP(service, pip), true
 }
 
 func isServiceLoadBalancerIPMatchesPIP(service *v1.Service, pip *network.PublicIPAddress) bool {
 	return strings.EqualFold(pointer.StringDeref(pip.IPAddress, ""), getServiceLoadBalancerIP(service))
 }
 
+func isServicePIPNameMatchesPIP(service *v1.Service, pip *network.PublicIPAddress) bool {
+	return strings.EqualFold(pointer.StringDeref(pip.Name, ""), getServicePIPName(service))
+}
+
+func isServiceSelectPIP(service *v1.Service, pip *network.PublicIPAddress) bool {
+	return isServiceLoadBalancerIPMatchesPIP(service, pip) || isServicePIPNameMatchesPIP(service, pip)
+}
+
 func isSVCNameInPIPTag(tag, svcName string) bool {
 	svcNames := parsePIPServiceTag(&tag)
 
diff --git a/pkg/provider/azure_standard.go b/pkg/provider/azure_standard.go
index d7e3e8ac4..7358c7aef 100644
--- a/pkg/provider/azure_standard.go
+++ b/pkg/provider/azure_standard.go
@@ -340,6 +340,22 @@ func (az *Cloud) serviceOwnsRule(service *v1.Service, rule string) bool {
 	return strings.HasPrefix(strings.ToUpper(rule), strings.ToUpper(prefix))
 }
 
+func publicIPOwnsFrontendIP(service *v1.Service, fip *network.FrontendIPConfiguration, pip *network.PublicIPAddress) bool {
+	if pip != nil &&
+		pip.ID != nil &&
+		pip.PublicIPAddressPropertiesFormat != nil &&
+		pip.PublicIPAddressPropertiesFormat.IPAddress != nil &&
+		fip != nil &&
+		fip.FrontendIPConfigurationPropertiesFormat != nil &&
+		fip.FrontendIPConfigurationPropertiesFormat.PublicIPAddress != nil {
+		if strings.EqualFold(pointer.StringDeref(pip.ID, ""), pointer.StringDeref(fip.PublicIPAddress.ID, "")) {
+			klog.V(6).Infof("publicIPOwnsFrontendIP:found secondary service %s of the frontend IP config %s", service.Name, *fip.Name)
+			return true
+		}
+	}
+	return false
+}
+
 // There are two cases when a service owns the frontend IP config:
 // 1. The primary service, which means the frontend IP config is created after the creation of the service.
 // This means the name of the config can be tracked by the service UID.
@@ -355,33 +371,37 @@ func (az *Cloud) serviceOwnsFrontendIP(fip network.FrontendIPConfiguration, serv
 	}
 
 	loadBalancerIP := getServiceLoadBalancerIP(service)
+	pipResourceGroup := az.getPublicIPAddressResourceGroup(service)
 	if loadBalancerIP == "" {
-		// it is a must that the secondary services set the loadBalancer IP
+		if !requiresInternalLoadBalancer(service) {
+			pipName := getServicePIPName(service)
+			if pipName != "" {
+				pip, err := az.findMatchedPIP("", pipName, pipResourceGroup)
+				if err != nil {
+					klog.Warningf("serviceOwnsFrontendIP: unexpected error when finding match public IP of the service %s with name %s: %v", service.Name, pipName, err)
+					return false, isPrimaryService, nil
+				}
+				if publicIPOwnsFrontendIP(service, &fip, pip) {
+					return true, isPrimaryService, nil
+				}
+			}
+		}
+		// it is a must that the secondary services set the loadBalancer IP or pip name
 		return false, isPrimaryService, nil
 	}
 
 	// for external secondary service the public IP address should be checked
 	if !requiresInternalLoadBalancer(service) {
-		pipResourceGroup := az.getPublicIPAddressResourceGroup(service)
-		pip, err := az.findMatchedPIPByLoadBalancerIP(service, loadBalancerIP, pipResourceGroup)
+		pip, err := az.findMatchedPIP(loadBalancerIP, "", pipResourceGroup)
 		if err != nil {
-			klog.Warningf("serviceOwnsFrontendIP: unexpected error when finding match public IP of the service %s with loadBalancerLP %s: %v", service.Name, loadBalancerIP, err)
+			klog.Warningf("serviceOwnsFrontendIP: unexpected error when finding match public IP of the service %s with loadBalancerIP %s: %v", service.Name, loadBalancerIP, err)
 			return false, isPrimaryService, nil
 		}
 
-		if pip != nil &&
-			pip.ID != nil &&
-			pip.PublicIPAddressPropertiesFormat != nil &&
-			pip.IPAddress != nil &&
-			fip.FrontendIPConfigurationPropertiesFormat != nil &&
-			fip.FrontendIPConfigurationPropertiesFormat.PublicIPAddress != nil {
-			if strings.EqualFold(pointer.StringDeref(pip.ID, ""), pointer.StringDeref(fip.PublicIPAddress.ID, "")) {
-				klog.V(4).Infof("serviceOwnsFrontendIP: found secondary service %s of the frontend IP config %s", service.Name, *fip.Name)
-
-				return true, isPrimaryService, nil
-			}
-			klog.V(4).Infof("serviceOwnsFrontendIP: the public IP with ID %s is being referenced by other service with public IP address %s", *pip.ID, *pip.IPAddress)
+		if publicIPOwnsFrontendIP(service, &fip, pip) {
+			return true, isPrimaryService, nil
 		}
+		klog.V(4).Infof("serviceOwnsFrontendIP: the public IP with ID %s is being referenced by other service with public IP address %s", *pip.ID, *pip.IPAddress)
 
 		return false, isPrimaryService, nil
 	}
diff --git a/pkg/provider/azure_utils.go b/pkg/provider/azure_utils.go
index 17cc70450..0c5242ba8 100644
--- a/pkg/provider/azure_utils.go
+++ b/pkg/provider/azure_utils.go
@@ -364,3 +364,10 @@ func countIPsOnBackendPool(backendPool network.BackendAddressPool) int {
 
 	return ipsCount
 }
+
+func getServicePIPName(service *v1.Service) string {
+	if service == nil {
+		return ""
+	}
+	return service.Annotations[consts.ServiceAnnotationPIPNameDualStack[false]]
+}


Test patch:
diff --git a/pkg/provider/azure_loadbalancer_test.go b/pkg/provider/azure_loadbalancer_test.go
index 6a4d39167..26831fe76 100644
--- a/pkg/provider/azure_loadbalancer_test.go
+++ b/pkg/provider/azure_loadbalancer_test.go
@@ -799,6 +799,7 @@ func TestServiceOwnsPublicIP(t *testing.T) {
 		clusterName             string
 		serviceName             string
 		serviceLBIP             string
+		serviceLBName           string
 		expectedOwns            bool
 		expectedUserAssignedPIP bool
 	}{
@@ -811,6 +812,7 @@ func TestServiceOwnsPublicIP(t *testing.T) {
 		{
 			desc: "false should be returned when service name tag doesn't match",
 			pip: &network.PublicIPAddress{
+				Name: pointer.String("pip1"),
 				Tags: map[string]*string{
 					consts.ServiceTagKey: pointer.String("default/nginx"),
 				},
@@ -853,6 +855,7 @@ func TestServiceOwnsPublicIP(t *testing.T) {
 		{
 			desc: "false should be returned when cluster name matches while service name doesn't match",
 			pip: &network.PublicIPAddress{
+				Name: pointer.String("pip1"),
 				Tags: map[string]*string{
 					consts.ServiceTagKey:  pointer.String("default/web"),
 					consts.ClusterNameKey: pointer.String("kubernetes"),
@@ -883,6 +886,7 @@ func TestServiceOwnsPublicIP(t *testing.T) {
 		{
 			desc: "false should be returned when the tag is empty and load balancer IP does not match",
 			pip: &network.PublicIPAddress{
+				Name: pointer.String("pip1"),
 				Tags: map[string]*string{
 					consts.ServiceTagKey:  pointer.String(""),
 					consts.ClusterNameKey: pointer.String("kubernetes"),
@@ -914,6 +918,7 @@ func TestServiceOwnsPublicIP(t *testing.T) {
 		{
 			desc: "false should be returned if there is not a match among a multi-service tag",
 			pip: &network.PublicIPAddress{
+				Name: pointer.String("pip1"),
 				Tags: map[string]*string{
 					consts.ServiceTagKey:  pointer.String("default/nginx1,default/nginx2"),
 					consts.ClusterNameKey: pointer.String("kubernetes"),
@@ -970,6 +975,45 @@ func TestServiceOwnsPublicIP(t *testing.T) {
 			expectedOwns:            true,
 			expectedUserAssignedPIP: true,
 		},
+		{
+			desc: "should be true if the pip name matches",
+			pip: &network.PublicIPAddress{
+				Name: pointer.String("pip1"),
+				PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
+					IPAddress: pointer.String("1.2.3.4"),
+				},
+			},
+			serviceLBName:           "pip1",
+			expectedOwns:            true,
+			expectedUserAssignedPIP: true,
+		},
+		{
+			desc: "should be true if the pip with tag matches the pip name",
+			pip: &network.PublicIPAddress{
+				Name: pointer.String("pip1"),
+				Tags: map[string]*string{},
+				PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
+					IPAddress: pointer.String("1.2.3.4"),
+				},
+			},
+			serviceLBName:           "pip1",
+			expectedOwns:            true,
+			expectedUserAssignedPIP: true,
+		},
+		{
+			desc: "should be true if the pip with service tag matches the pip name",
+			pip: &network.PublicIPAddress{
+				Name: pointer.String("pip1"),
+				Tags: map[string]*string{
+					consts.ServiceTagKey: pointer.String("default/web"),
+				},
+				PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
+					IPAddress: pointer.String("1.2.3.4"),
+				},
+			},
+			serviceLBName: "pip1",
+			expectedOwns:  true,
+		},
 	}
 
 	for i, c := range tests {
@@ -978,6 +1022,13 @@ func TestServiceOwnsPublicIP(t *testing.T) {
 			if c.serviceLBIP != "" {
 				setServiceLoadBalancerIP(&service, c.serviceLBIP)
 			}
+			if c.serviceLBName != "" {
+				if service.ObjectMeta.Annotations == nil {
+					service.ObjectMeta.Annotations = map[string]string{consts.ServiceAnnotationPIPNameDualStack[false]: "pip1"}
+				} else {
+					service.ObjectMeta.Annotations[consts.ServiceAnnotationPIPNameDualStack[false]] = "pip1"
+				}
+			}
 			owns, isUserAssignedPIP := serviceOwnsPublicIP(&service, c.pip, c.clusterName)
 			assert.Equal(t, c.expectedOwns, owns, "TestCase[%d]: %s", i, c.desc)
 			assert.Equal(t, c.expectedUserAssignedPIP, isUserAssignedPIP, "TestCase[%d]: %s", i, c.desc)
@@ -2072,6 +2123,7 @@ func TestFindMatchedPIPByLoadBalancerIP(t *testing.T) {
 	testCases := []struct {
 		desc               string
 		pips               []network.PublicIPAddress
+		pipsSecondTime     []network.PublicIPAddress
 		shouldRefreshCache bool
 		expectedPIP        *network.PublicIPAddress
 		expectedError      bool
@@ -2089,7 +2141,7 @@ func TestFindMatchedPIPByLoadBalancerIP(t *testing.T) {
 		},
 		{
 			desc:               "findMatchedPIPByLoadBalancerIP should refresh cache if no matched ip is found",
-			pips:               []network.PublicIPAddress{testPIP},
+			pipsSecondTime:     []network.PublicIPAddress{testPIP},
 			shouldRefreshCache: true,
 			expectedPIP:        &testPIP,
 		},
@@ -2097,21 +2149,94 @@ func TestFindMatchedPIPByLoadBalancerIP(t *testing.T) {
 	for _, test := range testCases {
 		t.Run(test.desc, func(t *testing.T) {
 			az := GetTestCloud(ctrl)
-			service := getTestService("test1", v1.ProtocolTCP, nil, false, 80)
-			setServiceLoadBalancerIP(&service, "1.2.3.4")
 
 			mockPIPsClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface)
 			if test.shouldRefreshCache {
-				mockPIPsClient.EXPECT().List(gomock.Any(), "rg").Return([]network.PublicIPAddress{}, nil)
+				mockPIPsClient.EXPECT().List(gomock.Any(), "rg").Return(test.pipsSecondTime, nil)
 			}
-			mockPIPsClient.EXPECT().List(gomock.Any(), "rg").Return(test.pips, nil)
-			pip, err := az.findMatchedPIPByLoadBalancerIP(&service, "1.2.3.4", "rg")
+			pip, err := az.findMatchedPIPByLoadBalancerIP(&test.pips, "1.2.3.4", "rg")
 			assert.Equal(t, test.expectedPIP, pip)
 			assert.Equal(t, test.expectedError, err != nil)
 		})
 	}
 }
 
+func TestFindMatchedPIP(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	testPIP := network.PublicIPAddress{
+		Name: pointer.String("pipName"),
+		PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
+			IPAddress: pointer.String("1.2.3.4"),
+		},
+	}
+
+	for _, tc := range []struct {
+		description         string
+		pips                []network.PublicIPAddress
+		pipsSecondTime      []network.PublicIPAddress
+		pipName             string
+		loadBalancerIP      string
+		shouldRefreshCache  bool
+		listError           *retry.Error
+		listErrorSecondTime *retry.Error
+		expectedPIP         *network.PublicIPAddress
+		expectedError       error
+	}{
+		{
+			description:        "should ignore pipName if loadBalancerIP is specified",
+			pips:               []network.PublicIPAddress{testPIP},
+			pipsSecondTime:     []network.PublicIPAddress{testPIP},
+			shouldRefreshCache: true,
+			loadBalancerIP:     "2.3.4.5",
+			pipName:            "pipName",
+			expectedError:      errors.New("findMatchedPIPByLoadBalancerIP: cannot find public IP with IP address 2.3.4.5 in resource group rg"),
+		},
+		{
+			description:   "should report an error if failed to list pip",
+			listError:     retry.NewError(false, errors.New("list error")),
+			expectedError: errors.New("findMatchedPIPByLoadBalancerIP: failed to listPIP: Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: list error"),
+		},
+		{
+			description:        "should refresh the cache if failed to search by name",
+			pips:               []network.PublicIPAddress{},
+			pipsSecondTime:     []network.PublicIPAddress{testPIP},
+			shouldRefreshCache: true,
+			pipName:            "pipName",
+			expectedPIP:        &testPIP,
+		},
+		{
+			description: "should return the expected pip by name",
+			pips:        []network.PublicIPAddress{testPIP},
+			pipName:     "pipName",
+			expectedPIP: &testPIP,
+		},
+		{
+			description:         "should report an error if failed to list pip second time",
+			pips:                []network.PublicIPAddress{},
+			listErrorSecondTime: retry.NewError(false, errors.New("list error")),
+			shouldRefreshCache:  true,
+			expectedError:       errors.New("findMatchedPIPByName: failed to listPIP force refresh: Retriable: false, RetryAfter: 0s, HTTPStatusCode: 0, RawError: list error"),
+		},
+	} {
+		t.Run(tc.description, func(t *testing.T) {
+			az := GetTestCloud(ctrl)
+			mockPIPsClient := az.PublicIPAddressesClient.(*mockpublicipclient.MockInterface)
+			mockPIPsClient.EXPECT().List(gomock.Any(), "rg").Return(tc.pips, tc.listError)
+			if tc.shouldRefreshCache {
+				mockPIPsClient.EXPECT().List(gomock.Any(), "rg").Return(tc.pipsSecondTime, tc.listErrorSecondTime)
+			}
+
+			pip, err := az.findMatchedPIP(tc.loadBalancerIP, tc.pipName, "rg")
+			assert.Equal(t, tc.expectedPIP, pip)
+			if tc.expectedError != nil {
+				assert.Equal(t, tc.expectedError.Error(), err.Error())
+			}
+		})
+	}
+}
+
 func TestDeterminePublicIPName(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
diff --git a/pkg/provider/azure_standard_test.go b/pkg/provider/azure_standard_test.go
index 33bef00a0..00b6d4d4a 100644
--- a/pkg/provider/azure_standard_test.go
+++ b/pkg/provider/azure_standard_test.go
@@ -1655,6 +1655,7 @@ func TestServiceOwnsFrontendIP(t *testing.T) {
 		isOwned      bool
 		isPrimary    bool
 		expectedErr  error
+		listError    *retry.Error
 	}{
 		{
 			desc: "serviceOwnsFrontendIP should detect the primary service",
@@ -1802,6 +1803,37 @@ func TestServiceOwnsFrontendIP(t *testing.T) {
 			},
 			isOwned: true,
 		},
+		{
+			desc:      "serviceOwnsFrontendIP should return false if failed to find matched pip by name",
+			service:   &v1.Service{},
+			listError: retry.NewError(false, errors.New("error")),
+		},
+		{
+			desc: "serviceOwnsFrontnedIP should support search pip by name",
+			service: &v1.Service{
+				ObjectMeta: meta.ObjectMeta{
+					Annotations: map[string]string{consts.ServiceAnnotationPIPNameDualStack[false]: "pip1"},
+				},
+			},
+			existingPIPs: []network.PublicIPAddress{
+				{
+					Name: pointer.String("pip1"),
+					ID:   pointer.String("pip1"),
+					PublicIPAddressPropertiesFormat: &network.PublicIPAddressPropertiesFormat{
+						IPAddress: pointer.String("1.2.3.4"),
+					},
+				},
+			},
+			fip: network.FrontendIPConfiguration{
+				Name: pointer.String("test"),
+				FrontendIPConfigurationPropertiesFormat: &network.FrontendIPConfigurationPropertiesFormat{
+					PublicIPAddress: &network.PublicIPAddress{
+						ID: pointer.String("pip1"),
+					},
+				},
+			},
+			isOwned: true,
+		},
 	}
 
 	for _, test := range testCases {
@@ -1809,7 +1841,7 @@ func TestServiceOwnsFrontendIP(t *testing.T) {
 			cloud := GetTestCloud(ctrl)
 			if test.existingPIPs != nil {
 				mockPIPsClient := cloud.PublicIPAddressesClient.(*mockpublicipclient.MockInterface)
-				mockPIPsClient.EXPECT().List(gomock.Any(), "rg").Return(test.existingPIPs, nil).MaxTimes(2)
+				mockPIPsClient.EXPECT().List(gomock.Any(), "rg").Return(test.existingPIPs, test.listError).MaxTimes(2)
 			}
 			isOwned, isPrimary, err := cloud.serviceOwnsFrontendIP(test.fip, test.service)
 			assert.Equal(t, test.expectedErr, err)
diff --git a/tests/e2e/network/ensureloadbalancer.go b/tests/e2e/network/ensureloadbalancer.go
index a0ce592f5..b3146a3ff 100644
--- a/tests/e2e/network/ensureloadbalancer.go
+++ b/tests/e2e/network/ensureloadbalancer.go
@@ -392,7 +392,7 @@ var _ = Describe("Ensure LoadBalancer", Label(utils.TestSuiteLabelLB), func() {
 		targetIP := pointer.StringDeref(pip.IPAddress, "")
 
 		serviceNames := []string{}
-		for i := 0; i < 2; i++ {
+		for i := 0; i < 3; i++ {
 			serviceLabels := labels
 			deploymentName := testDeploymentName
 			tcpPort := int32(80 + i)
@@ -411,6 +411,7 @@ var _ = Describe("Ensure LoadBalancer", Label(utils.TestSuiteLabelLB), func() {
 				Expect(err).NotTo(HaveOccurred())
 			}
 
+			var service *v1.Service
 			serviceName := fmt.Sprintf("%s-%d", testServiceName, i)
 			utils.Logf("Creating Service %q", serviceName)
 			serviceNames = append(serviceNames, serviceName)
@@ -418,9 +419,17 @@ var _ = Describe("Ensure LoadBalancer", Label(utils.TestSuiteLabelLB), func() {
 				Port:       tcpPort,
 				TargetPort: intstr.FromInt(int(tcpPort)),
 			}}
-			service := utils.CreateLoadBalancerServiceManifest(serviceName, nil, serviceLabels, ns.Name, servicePort)
-			service = updateServiceLBIPs(service, false, []string{targetIP})
-			_, err = cs.CoreV1().Services(ns.Name).Create(context.TODO(), service, metav1.CreateOptions{})
+			service = utils.CreateLoadBalancerServiceManifest(serviceName, nil, serviceLabels, ns.Name, servicePort)
+			if i < 2 {
+				targetIPs := []string{targetIP}
+				service = updateServiceLBIPs(service, false, targetIPs)
+			} else {
+				var pipNames []string
+				pipNames = append(pipNames, ipName)
+				utils.Logf("update pip names %s", strings.Join(pipNames, ","))
+				service = updateServicePIPNames(service, pipNames)
+			}
+			_, err := cs.CoreV1().Services(ns.Name).Create(context.TODO(), service, metav1.CreateOptions{})
 			defer func() {
 				err = utils.DeleteService(cs, ns.Name, serviceName)
 				Expect(err).NotTo(HaveOccurred())
@@ -466,8 +475,16 @@ var _ = Describe("Ensure LoadBalancer", Label(utils.TestSuiteLabelLB), func() {
 				TargetPort: intstr.FromInt(int(tcpPort)),
 			}}
 			service := utils.CreateLoadBalancerServiceManifest(serviceName, nil, serviceLabels, ns.Name, servicePort)
-			if len(sharedIPs) != 0 {
-				service = updateServiceLBIPs(service, false, sharedIPs)
+			if i < 2 {
+				if len(sharedIPs) != 0 {
+					service = updateServiceLBIPs(service, false, sharedIPs)
+				}
+			} else {
+				pip, err := tc.GetPublicIPFromAddress(tc.GetResourceGroup(), sharedIPs[0])
+				Expect(err).NotTo(HaveOccurred())
+				pipNames := []string{pointer.StringDeref(pip.Name, "")}
+				utils.Logf("update pip names %s", strings.Join(pipNames, ","))
+				service = updateServicePIPNames(service, pipNames)
 			}
 			_, err := cs.CoreV1().Services(ns.Name).Create(context.TODO(), service, metav1.CreateOptions{})
 			Expect(err).NotTo(HaveOccurred())
@@ -1040,6 +1057,22 @@ func updateServiceLBIPs(service *v1.Service, isInternal bool, ips []string) (res
 	return
 }
 
+func updateServicePIPNames(service *v1.Service, pipNames []string) *v1.Service {
+	if service.Annotations == nil {
+		service.Annotations = map[string]string{}
+	}
+
+	for _, pipName := range pipNames {
+		if strings.HasSuffix(pipName, "-IPv6") {
+			service.Annotations[consts.ServiceAnnotationPIPNameDualStack[consts.IPVersionIPv6]] = pipName
+		} else {
+			service.Annotations[consts.ServiceAnnotationPIPNameDualStack[consts.IPVersionIPv4]] = pipName
+		}
+	}
+
+	return service
+}
+
 func defaultPublicIPAddress(ipName string, isIPv6 bool) aznetwork.PublicIPAddress {
 	// The default sku for LoadBalancer and PublicIP is basic.
 	skuName := aznetwork.PublicIPAddressSkuNameBasic