diff --git a/docs/openstack-cloud-controller-manager/expose-applications-using-loadbalancer-type-service.md b/docs/openstack-cloud-controller-manager/expose-applications-using-loadbalancer-type-service.md index a74a3b44b3..b818f58a79 100644 --- a/docs/openstack-cloud-controller-manager/expose-applications-using-loadbalancer-type-service.md +++ b/docs/openstack-cloud-controller-manager/expose-applications-using-loadbalancer-type-service.md @@ -118,6 +118,14 @@ Request Body: Member subnet ID of the load balancer created. +- `loadbalancer.openstack.org/node-address-type` + + The node address type to use when selecting the IP address for load balancer pool members. By default, InternalIP is preferred over ExternalIP. Set to `ExternalIP` to reverse this priority, which is useful when nodes have multiple interfaces and the member subnet corresponds to the ExternalIP network. + + Default: InternalIP is tried first, then ExternalIP. + + Possible values: `ExternalIP` + - `loadbalancer.openstack.org/network-id` The network ID which will allocate virtual IP for loadbalancer. diff --git a/pkg/openstack/loadbalancer.go b/pkg/openstack/loadbalancer.go index 0a007e840a..596216a26b 100644 --- a/pkg/openstack/loadbalancer.go +++ b/pkg/openstack/loadbalancer.go @@ -73,6 +73,7 @@ const ( ServiceAnnotationLoadBalancerSubnetID = "loadbalancer.openstack.org/subnet-id" ServiceAnnotationLoadBalancerNetworkID = "loadbalancer.openstack.org/network-id" ServiceAnnotationLoadBalancerMemberSubnetID = "loadbalancer.openstack.org/member-subnet-id" + ServiceAnnotationLoadBalancerNodeAddressType = "loadbalancer.openstack.org/node-address-type" ServiceAnnotationLoadBalancerTimeoutClientData = "loadbalancer.openstack.org/timeout-client-data" ServiceAnnotationLoadBalancerTimeoutMemberConnect = "loadbalancer.openstack.org/timeout-member-connect" ServiceAnnotationLoadBalancerTimeoutMemberData = "loadbalancer.openstack.org/timeout-member-data" @@ -145,7 +146,8 @@ type serviceConfig struct { healthMonitorTimeout int healthMonitorMaxRetries int healthMonitorMaxRetriesDown int - preferredIPFamily corev1.IPFamily // preferred (the first) IP family indicated in service's `spec.ipFamilies` + preferredIPFamily corev1.IPFamily // preferred (the first) IP family indicated in service's `spec.ipFamilies` + nodeAddressType corev1.NodeAddressType // preferred node address type for pool members } type listenerKey struct { @@ -382,13 +384,17 @@ func (lbaas *LbaasV2) getLoadBalancerLegacyName(service *corev1.Service) string // If neither InternalIP nor ExternalIP can be found an error is // returned. // If preferredIPFamily is specified, only address of the specified IP family can be returned. -func nodeAddressForLB(node *corev1.Node, preferredIPFamily corev1.IPFamily) (string, error) { +// If preferredAddrType is ExternalIP, ExternalIP is tried first. +func nodeAddressForLB(node *corev1.Node, preferredIPFamily corev1.IPFamily, preferredAddrType corev1.NodeAddressType) (string, error) { addrs := node.Status.Addresses if len(addrs) == 0 { return "", cpoerrors.ErrNoAddressFound } allowedAddrTypes := []corev1.NodeAddressType{corev1.NodeInternalIP, corev1.NodeExternalIP} + if preferredAddrType == corev1.NodeExternalIP { + allowedAddrTypes = []corev1.NodeAddressType{corev1.NodeExternalIP, corev1.NodeInternalIP} + } for _, allowedAddrType := range allowedAddrTypes { for _, addr := range addrs { if addr.Type == allowedAddrType { @@ -493,7 +499,7 @@ func getProxyProtocolFromServiceAnnotation(service *corev1.Service) *v2pools.Pro // getSubnetIDForLB returns subnet-id for a specific node func getSubnetIDForLB(ctx context.Context, network *gophercloud.ServiceClient, node corev1.Node, preferredIPFamily corev1.IPFamily) (string, error) { - ipAddress, err := nodeAddressForLB(&node, preferredIPFamily) + ipAddress, err := nodeAddressForLB(&node, preferredIPFamily, "") if err != nil { return "", err } @@ -1017,7 +1023,7 @@ func (lbaas *LbaasV2) buildBatchUpdateMemberOpts(ctx context.Context, port corev newMembers := sets.New[string]() for _, node := range nodes { - addr, err := nodeAddressForLB(node, svcConf.preferredIPFamily) + addr, err := nodeAddressForLB(node, svcConf.preferredIPFamily, svcConf.nodeAddressType) if err != nil { if err == cpoerrors.ErrNoAddressFound { // Node failure, do not create member @@ -1308,6 +1314,8 @@ func (lbaas *LbaasV2) checkServiceUpdate(ctx context.Context, service *corev1.Se svcConf.preferredIPFamily = service.Spec.IPFamilies[0] } + svcConf.nodeAddressType = corev1.NodeAddressType(getStringFromServiceAnnotation(service, ServiceAnnotationLoadBalancerNodeAddressType, "")) + // Find subnet ID for creating members memberSubnetID, err := lbaas.getMemberSubnetID(service) if err != nil { @@ -1371,6 +1379,8 @@ func (lbaas *LbaasV2) checkService(ctx context.Context, service *corev1.Service, svcConf.preferredIPFamily = service.Spec.IPFamilies[0] } + svcConf.nodeAddressType = corev1.NodeAddressType(getStringFromServiceAnnotation(service, ServiceAnnotationLoadBalancerNodeAddressType, "")) + // If in the config file internal-lb=true, user is not allowed to create external service. if lbaas.opts.InternalLB { if !getBoolFromServiceAnnotation(service, ServiceAnnotationLoadBalancerInternal, false) { diff --git a/pkg/openstack/loadbalancer_sg.go b/pkg/openstack/loadbalancer_sg.go index 3d35342790..404f88be0e 100644 --- a/pkg/openstack/loadbalancer_sg.go +++ b/pkg/openstack/loadbalancer_sg.go @@ -57,7 +57,7 @@ func applyNodeSecurityGroupIDForLB(ctx context.Context, network *gophercloud.Ser return fmt.Errorf("error getting server ID from the node: %w", err) } - addr, _ := nodeAddressForLB(node, svcConf.preferredIPFamily) + addr, _ := nodeAddressForLB(node, svcConf.preferredIPFamily, svcConf.nodeAddressType) if addr == "" { // If node has no viable address let's ignore it. continue diff --git a/pkg/openstack/loadbalancer_test.go b/pkg/openstack/loadbalancer_test.go index 27304a45ad..f84efcd692 100644 --- a/pkg/openstack/loadbalancer_test.go +++ b/pkg/openstack/loadbalancer_test.go @@ -1470,6 +1470,7 @@ func Test_nodeAddressForLB(t *testing.T) { type testArgs struct { node *corev1.Node preferredIPFamily corev1.IPFamily + preferredAddrType corev1.NodeAddressType } tests := []struct { @@ -1775,11 +1776,76 @@ func Test_nodeAddressForLB(t *testing.T) { expect: "", expectedErr: cpoerrors.ErrNoAddressFound, }, + { + name: "ExternalIP preferred over InternalIP with IPv4", + testArgs: testArgs{ + node: &corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + { + Type: corev1.NodeExternalIP, + Address: "10.0.0.1", + }, + }, + }, + }, + preferredIPFamily: corev1.IPv4Protocol, + preferredAddrType: corev1.NodeExternalIP, + }, + expect: "10.0.0.1", + expectedErr: nil, + }, + { + name: "ExternalIP preferred falls back to InternalIP when no ExternalIP", + testArgs: testArgs{ + node: &corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "192.168.1.1", + }, + }, + }, + }, + preferredIPFamily: corev1.IPv4Protocol, + preferredAddrType: corev1.NodeExternalIP, + }, + expect: "192.168.1.1", + expectedErr: nil, + }, + { + name: "ExternalIP preferred with IPv6", + testArgs: testArgs{ + node: &corev1.Node{ + Status: corev1.NodeStatus{ + Addresses: []corev1.NodeAddress{ + { + Type: corev1.NodeInternalIP, + Address: "2001:db8::1", + }, + { + Type: corev1.NodeExternalIP, + Address: "2001:db8::2", + }, + }, + }, + }, + preferredIPFamily: corev1.IPv6Protocol, + preferredAddrType: corev1.NodeExternalIP, + }, + expect: "2001:db8::2", + expectedErr: nil, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := nodeAddressForLB(test.testArgs.node, test.testArgs.preferredIPFamily) + got, err := nodeAddressForLB(test.testArgs.node, test.testArgs.preferredIPFamily, test.testArgs.preferredAddrType) if test.expectedErr != nil { assert.EqualError(t, err, test.expectedErr.Error()) } else {