From 8c431e46f405b15a83f54a589f397ee4d2c39cc8 Mon Sep 17 00:00:00 2001 From: Shilpa-Gokul Date: Wed, 29 Jan 2025 15:19:04 +0530 Subject: [PATCH] Add listener based on the machine label and listener label --- api/v1beta2/ibmvpccluster_types.go | 5 + api/v1beta2/zz_generated.deepcopy.go | 1 + cloud/scope/powervs_machine.go | 85 +++- cloud/scope/powervs_machine_test.go | 364 +++++++++++++++++- ...e.cluster.x-k8s.io_ibmpowervsclusters.yaml | 48 +++ ...r.x-k8s.io_ibmpowervsclustertemplates.yaml | 49 +++ ...cture.cluster.x-k8s.io_ibmvpcclusters.yaml | 97 +++++ ...uster.x-k8s.io_ibmvpcclustertemplates.yaml | 98 +++++ controllers/ibmpowervsmachine_controller.go | 12 - internal/webhooks/ibmpowervscluster.go | 45 ++- internal/webhooks/ibmpowervscluster_test.go | 165 ++++++++ pkg/cloud/services/vpc/mock/vpc_generated.go | 16 + pkg/cloud/services/vpc/service.go | 5 + pkg/cloud/services/vpc/vpc.go | 1 + 14 files changed, 944 insertions(+), 47 deletions(-) diff --git a/api/v1beta2/ibmvpccluster_types.go b/api/v1beta2/ibmvpccluster_types.go index 60caf354d..682229d0d 100644 --- a/api/v1beta2/ibmvpccluster_types.go +++ b/api/v1beta2/ibmvpccluster_types.go @@ -127,6 +127,11 @@ type AdditionalListenerSpec struct { // Will default to TCP protocol if not specified. // +optional Protocol *VPCLoadBalancerListenerProtocol `json:"protocol,omitempty"` + + // The selector is used to find IBMPowerVSMachines with matching labels. + // If the label matches, the machine is then added to the load balancer listener configuration. + // +kubebuilder:validation:Optional + Selector metav1.LabelSelector `json:"selector,omitempty"` } // VPCLoadBalancerBackendPoolSpec defines the desired configuration of a VPC Load Balancer Backend Pool. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index b0ba240ba..73f53dc01 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -40,6 +40,7 @@ func (in *AdditionalListenerSpec) DeepCopyInto(out *AdditionalListenerSpec) { *out = new(VPCLoadBalancerListenerProtocol) **out = **in } + in.Selector.DeepCopyInto(&out.Selector) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalListenerSpec. diff --git a/cloud/scope/powervs_machine.go b/cloud/scope/powervs_machine.go index 0ca986350..eb2b4b30f 100644 --- a/cloud/scope/powervs_machine.go +++ b/cloud/scope/powervs_machine.go @@ -44,6 +44,8 @@ import ( "github.com/IBM/vpc-go-sdk/vpcv1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/tools/cache" @@ -996,7 +998,49 @@ func (m *PowerVSMachineScope) CreateVPCLoadBalancerPoolMember(ctx context.Contex internalIP := m.GetMachineInternalIP() + // lbAdditionalListeners is a mapping of additionalListener's port-protocol to the additionalListener as defined in the specification + // It will be used later to get the default pool associated with the listener + lbAdditionalListeners := map[string]infrav1beta2.AdditionalListenerSpec{} + for _, additionalListener := range lb.AdditionalListeners { + if additionalListener.Protocol == nil { + additionalListener.Protocol = &infrav1beta2.VPCLoadBalancerListenerProtocolTCP + } + lbAdditionalListeners[fmt.Sprintf("%d-%s", additionalListener.Port, *additionalListener.Protocol)] = additionalListener + } + + // loadBalancerListeners is a mapping of the loadBalancer listener's defaultPoolName to the additionalListener + // as the default pool name might be empty in spec and should be fetched from the cloud's listener + loadBalancerListeners := map[string]infrav1beta2.AdditionalListenerSpec{} + for _, listener := range loadBalancer.Listeners { + listenerOptions := &vpcv1.GetLoadBalancerListenerOptions{} + listenerOptions.SetLoadBalancerID(*loadBalancer.ID) + listenerOptions.SetID(*listener.ID) + loadBalancerListener, _, err := m.IBMVPCClient.GetLoadBalancerListener(listenerOptions) + if err != nil { + return nil, fmt.Errorf("failed to list %s load balancer listener: %w", *listener.ID, err) + } + if additionalListener, ok := lbAdditionalListeners[fmt.Sprintf("%d-%s", *loadBalancerListener.Port, *loadBalancerListener.Protocol)]; ok { + if loadBalancerListener.DefaultPool != nil { + loadBalancerListeners[*loadBalancerListener.DefaultPool.Name] = additionalListener + } + // loadBalancerListeners map is created only with the listeners provided in the spec, + // and targetPort is populated only if there is an entry in the map. + // Inorder for the default pool 6443 to be added to all control plane machines, creating an entry in the map for the same. + } else if loadBalancerListener.Port != nil && *loadBalancerListener.Port == int64(6443) { + protocol := infrav1beta2.VPCLoadBalancerListenerProtocol(*loadBalancerListener.Protocol) + listener := infrav1beta2.AdditionalListenerSpec{ + Port: *loadBalancerListener.Port, + Protocol: &protocol, + } + if loadBalancerListener.DefaultPool != nil { + loadBalancerListeners[*loadBalancerListener.DefaultPool.Name] = listener + } else { + log.V(3).Error(fmt.Errorf("unable to get the default pool details"), "default pool is nil", "port", loadBalancerListener.Port) + } + } + } // Update each LoadBalancer pool + // For each pool, get the additionalListener associated with the pool from the loadBalancerListeners map. for _, pool := range loadBalancer.Pools { log.V(3).Info("Updating LoadBalancer pool member", "pool", *pool.Name, "loadBalancerName", *loadBalancer.Name, "IP", internalIP) listOptions := &vpcv1.ListLoadBalancerPoolMembersOptions{} @@ -1009,32 +1053,35 @@ func (m *PowerVSMachineScope) CreateVPCLoadBalancerPoolMember(ctx context.Contex var targetPort int64 var alreadyRegistered bool - if len(listLoadBalancerPoolMembers.Members) == 0 { - // For adding the first member to the pool we depend on the pool name to get the target port - // pool name will have port number appended at the end - lbNameSplit := strings.Split(*pool.Name, "-") - if len(lbNameSplit) == 0 { - // user might have created additional pool - log.V(3).Info("Not updating pool as it might be created externally", "poolName", *pool.Name) + if loadBalancerListener, ok := loadBalancerListeners[*pool.Name]; ok { + targetPort = loadBalancerListener.Port + log.V(3).Info("Checking if machine label matches with the label selector in listener", "machineLabel", m.IBMPowerVSMachine.Labels, "labelSelector", loadBalancerListener.Selector) + selector, err := metav1.LabelSelectorAsSelector(&loadBalancerListener.Selector) + if err != nil { + log.V(5).Error(err, "Skipping listener addition, failed to get label selector from spec selector") continue } - targetPort, err = strconv.ParseInt(lbNameSplit[len(lbNameSplit)-1], 10, 64) - if err != nil { - // user might have created additional pool - log.Error(err, "unable to fetch target port from pool name", "poolName", *pool.Name) + + if selector.Empty() && !util.IsControlPlaneMachine(m.Machine) { + log.V(3).Info("Skipping listener addition as the selector is empty and not a control plane machine") continue } - } else { - for _, member := range listLoadBalancerPoolMembers.Members { - if target, ok := member.Target.(*vpcv1.LoadBalancerPoolMemberTarget); ok { - targetPort = *member.Port - if *target.Address == internalIP { - alreadyRegistered = true - log.V(3).Info("Target IP already configured for pool", "IP", internalIP, "poolName", *pool.Name) - } + // Skip adding the listener if the selector does not match + if !selector.Empty() && !selector.Matches(labels.Set(m.IBMPowerVSMachine.Labels)) { + log.V(3).Info("Skip adding listener, machine label doesn't match with the listener label selector", "pool", *pool.Name, "IP", internalIP) + continue + } + } + + for _, member := range listLoadBalancerPoolMembers.Members { + if target, ok := member.Target.(*vpcv1.LoadBalancerPoolMemberTarget); ok { + if *target.Address == internalIP { + alreadyRegistered = true + log.V(3).Info("Target IP already configured for pool", "IP", internalIP, "poolName", *pool.Name) } } } + if alreadyRegistered { log.V(3).Info("PoolMember already exist", "poolName", *pool.Name, "IP", internalIP, "targetPort", targetPort) continue diff --git a/cloud/scope/powervs_machine_test.go b/cloud/scope/powervs_machine_test.go index dbe385f8d..63af599ad 100644 --- a/cloud/scope/powervs_machine_test.go +++ b/cloud/scope/powervs_machine_test.go @@ -1509,6 +1509,356 @@ func TestCreateVPCLoadBalancerPoolMemberPowerVSMachine(t *testing.T) { nodeAddress := "10.0.0.1" loadBalancerID := "xyz-xyz-xyz" + loadBalancerName := "load-balancer-0" + t.Run("Skip adding listener if the machine label and listener label doesnot match", func(t *testing.T) { + g := NewWithT(t) + setup(t) + t.Cleanup(teardown) + loadBalancerName := loadBalancerName + loadBalancers := &vpcv1.LoadBalancer{ + ID: ptr.To(loadBalancerID), + Name: ptr.To(loadBalancerName), + ProvisioningStatus: (*string)(&infrav1beta2.VPCLoadBalancerStateActive), + Pools: []vpcv1.LoadBalancerPoolReference{ + { + ID: ptr.To("pool-id-23"), + Name: ptr.To("pool-23"), + }, + }, + Listeners: []vpcv1.LoadBalancerListenerReference{ + { + ID: ptr.To("pool-id-23"), + }, + }, + } + loadBalancerListener := &vpcv1.LoadBalancerListener{ + DefaultPool: &vpcv1.LoadBalancerPoolReference{ + Name: ptr.To("pool-23"), + }, + ID: ptr.To("pool-id-23"), + Port: ptr.To(int64(23)), + Protocol: ptr.To("tcp"), + } + mockClient := vpcmock.NewMockVpc(mockCtrl) + + scope := PowerVSMachineScope{ + Machine: &capiv1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{}, + }, + IBMVPCClient: mockClient, + IBMPowerVSMachine: &infrav1beta2.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "listener-selector": "port-22", + }, + }, + }, + IBMPowerVSCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: loadBalancerName, + ID: ptr.To(loadBalancerID), + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 23, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "listener-selector": "port-23", + }, + }, + }, + }, + }, + }, + }, + Status: infrav1beta2.IBMPowerVSClusterStatus{ + LoadBalancers: map[string]infrav1beta2.VPCLoadBalancerStatus{ + loadBalancerName: { + ID: ptr.To(loadBalancerID), + }, + }, + }, + }, + } + + mockClient.EXPECT().GetLoadBalancer(gomock.AssignableToTypeOf(&vpcv1.GetLoadBalancerOptions{})).Return(loadBalancers, nil, nil).AnyTimes() + mockClient.EXPECT().GetLoadBalancerListener(gomock.AssignableToTypeOf(&vpcv1.GetLoadBalancerListenerOptions{})).Return(loadBalancerListener, nil, nil).AnyTimes() + mockClient.EXPECT().ListLoadBalancerPoolMembers(gomock.AssignableToTypeOf(&vpcv1.ListLoadBalancerPoolMembersOptions{})).Return(&vpcv1.LoadBalancerPoolMemberCollection{}, nil, nil).AnyTimes() + result, err := scope.CreateVPCLoadBalancerPoolMember(ctx) + + g.Expect(err).To(BeNil()) + g.Expect(result).To(BeNil()) + }) + + t.Run("Add listener if the machine label and listener label matches", func(t *testing.T) { + g := NewWithT(t) + setup(t) + t.Cleanup(teardown) + loadBalancerName := loadBalancerName + loadBalancers := &vpcv1.LoadBalancer{ + ID: ptr.To(loadBalancerID), + Name: ptr.To(loadBalancerName), + ProvisioningStatus: (*string)(&infrav1beta2.VPCLoadBalancerStateActive), + Pools: []vpcv1.LoadBalancerPoolReference{ + { + ID: ptr.To("pool-id-22"), + Name: ptr.To("pool-22"), + }, + }, + Listeners: []vpcv1.LoadBalancerListenerReference{ + { + ID: ptr.To("pool-id-22"), + }, + { + ID: ptr.To("pool-id-23"), + }, + }, + } + loadBalancerListener := &vpcv1.LoadBalancerListener{ + DefaultPool: &vpcv1.LoadBalancerPoolReference{ + Name: ptr.To("pool-22"), + }, + ID: ptr.To("pool-id-22"), + Port: ptr.To(int64(22)), + Protocol: ptr.To("tcp"), + } + mockClient := vpcmock.NewMockVpc(mockCtrl) + + scope := PowerVSMachineScope{ + Machine: &capiv1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{}, + }, + IBMVPCClient: mockClient, + IBMPowerVSMachine: &infrav1beta2.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "listener-selector": "port-22", + }, + }, + }, + IBMPowerVSCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: loadBalancerName, + ID: ptr.To(loadBalancerID), + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 22, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "listener-selector": "port-22", + }, + }, + }, + }, + }, + }, + }, + Status: infrav1beta2.IBMPowerVSClusterStatus{ + LoadBalancers: map[string]infrav1beta2.VPCLoadBalancerStatus{ + loadBalancerName: { + ID: ptr.To(loadBalancerID), + }, + }, + }, + }, + } + + mockClient.EXPECT().GetLoadBalancer(gomock.AssignableToTypeOf(&vpcv1.GetLoadBalancerOptions{})).Return(loadBalancers, nil, nil).AnyTimes() + mockClient.EXPECT().GetLoadBalancerListener(gomock.AssignableToTypeOf(&vpcv1.GetLoadBalancerListenerOptions{})).Return(loadBalancerListener, nil, nil).AnyTimes() + mockClient.EXPECT().ListLoadBalancerPoolMembers(gomock.AssignableToTypeOf(&vpcv1.ListLoadBalancerPoolMembersOptions{})).Return(&vpcv1.LoadBalancerPoolMemberCollection{}, nil, nil).AnyTimes() + expectedLoadBalancerPoolMemberID := "pool-member-3" + expectedLoadBalancerPoolMember := &vpcv1.LoadBalancerPoolMember{ID: ptr.To(expectedLoadBalancerPoolMemberID)} + mockClient.EXPECT().CreateLoadBalancerPoolMember(gomock.AssignableToTypeOf(&vpcv1.CreateLoadBalancerPoolMemberOptions{})).Return(expectedLoadBalancerPoolMember, nil, nil).AnyTimes() + result, err := scope.CreateVPCLoadBalancerPoolMember(ctx) + + g.Expect(err).To(BeNil()) + g.Expect(*result.ID).To(Equal(expectedLoadBalancerPoolMemberID)) + }) + + t.Run("Skip adding non control plane nodes if there is no selector", func(t *testing.T) { + g := NewWithT(t) + setup(t) + t.Cleanup(teardown) + loadBalancerName := loadBalancerName + loadBalancers := &vpcv1.LoadBalancer{ + ID: ptr.To(loadBalancerID), + Name: ptr.To(loadBalancerName), + ProvisioningStatus: (*string)(&infrav1beta2.VPCLoadBalancerStateActive), + Pools: []vpcv1.LoadBalancerPoolReference{ + { + ID: ptr.To("pool-id-6443"), + Name: ptr.To("pool-6443"), + }, + }, + Listeners: []vpcv1.LoadBalancerListenerReference{ + { + ID: ptr.To("pool-id-6443"), + }, + { + ID: ptr.To("pool-id-1"), + }, + }, + } + loadBalancerListener := &vpcv1.LoadBalancerListener{ + DefaultPool: &vpcv1.LoadBalancerPoolReference{ + Name: ptr.To("pool-6443"), + }, + ID: ptr.To("pool-id-6443"), + Port: ptr.To(int64(6443)), + Protocol: ptr.To("tcp"), + } + mockClient := vpcmock.NewMockVpc(mockCtrl) + + scope := PowerVSMachineScope{ + Machine: &capiv1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{}, + }, + IBMVPCClient: mockClient, + IBMPowerVSMachine: &infrav1beta2.IBMPowerVSMachine{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "listener-selector": "port-6443", + }, + }, + }, + IBMPowerVSCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: loadBalancerName, + ID: ptr.To(loadBalancerID), + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 6443, + }, + }, + }, + }, + }, + Status: infrav1beta2.IBMPowerVSClusterStatus{ + LoadBalancers: map[string]infrav1beta2.VPCLoadBalancerStatus{ + loadBalancerName: { + ID: ptr.To(loadBalancerID), + }, + }, + }, + }, + } + + mockClient.EXPECT().GetLoadBalancer(gomock.AssignableToTypeOf(&vpcv1.GetLoadBalancerOptions{})).Return(loadBalancers, nil, nil).AnyTimes() + mockClient.EXPECT().GetLoadBalancerListener(gomock.AssignableToTypeOf(&vpcv1.GetLoadBalancerListenerOptions{})).Return(loadBalancerListener, nil, nil).AnyTimes() + mockClient.EXPECT().ListLoadBalancerPoolMembers(gomock.AssignableToTypeOf(&vpcv1.ListLoadBalancerPoolMembersOptions{})).Return(&vpcv1.LoadBalancerPoolMemberCollection{}, nil, nil).AnyTimes() + result, err := scope.CreateVPCLoadBalancerPoolMember(ctx) + + g.Expect(err).To(BeNil()) + g.Expect(result).To(BeNil()) + }) + t.Run("Adding control plane nodes even if there is no selector", func(t *testing.T) { + g := NewWithT(t) + setup(t) + t.Cleanup(teardown) + loadBalancerName := loadBalancerName + loadBalancers := &vpcv1.LoadBalancer{ + ID: ptr.To(loadBalancerID), + Name: ptr.To(loadBalancerName), + ProvisioningStatus: (*string)(&infrav1beta2.VPCLoadBalancerStateActive), + Pools: []vpcv1.LoadBalancerPoolReference{ + { + ID: ptr.To("pool-id-6443"), + Name: ptr.To("pool-6443"), + }, + { + ID: ptr.To("pool-id-24"), + Name: ptr.To("pool-24"), + }, + }, + Listeners: []vpcv1.LoadBalancerListenerReference{ + { + ID: ptr.To("pool-id-6443"), + }, + { + ID: ptr.To("pool-id-24"), + }, + }, + } + loadBalancerListener6443 := &vpcv1.LoadBalancerListener{ + DefaultPool: &vpcv1.LoadBalancerPoolReference{ + Name: ptr.To("pool-6443"), + }, + ID: ptr.To("pool-id-6443"), + Port: ptr.To(int64(6443)), + Protocol: ptr.To("tcp"), + } + loadBalancerListener24 := &vpcv1.LoadBalancerListener{ + DefaultPool: &vpcv1.LoadBalancerPoolReference{ + Name: ptr.To("pool-24"), + }, + ID: ptr.To("pool-id-24"), + Port: ptr.To(int64(24)), + Protocol: ptr.To("tcp"), + } + mockClient := vpcmock.NewMockVpc(mockCtrl) + + scope := PowerVSMachineScope{ + Machine: &capiv1beta1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "cluster.x-k8s.io/control-plane": "true", + }, + }, + }, + IBMVPCClient: mockClient, + IBMPowerVSMachine: &infrav1beta2.IBMPowerVSMachine{}, + IBMPowerVSCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: loadBalancerName, + ID: ptr.To(loadBalancerID), + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 6443, + }, + { + Port: 24, + }, + }, + }, + }, + }, + Status: infrav1beta2.IBMPowerVSClusterStatus{ + LoadBalancers: map[string]infrav1beta2.VPCLoadBalancerStatus{ + loadBalancerName: { + ID: ptr.To(loadBalancerID), + }, + }, + }, + }, + } + + mockClient.EXPECT().GetLoadBalancer(gomock.AssignableToTypeOf(&vpcv1.GetLoadBalancerOptions{})).Return(loadBalancers, nil, nil).AnyTimes() + mockClient.EXPECT().GetLoadBalancerListener(gomock.AssignableToTypeOf(&vpcv1.GetLoadBalancerListenerOptions{LoadBalancerID: ptr.To(loadBalancerID), ID: ptr.To("pool-id-6443")})).Return(loadBalancerListener6443, nil, nil).AnyTimes() + mockClient.EXPECT().GetLoadBalancerListener(gomock.AssignableToTypeOf(&vpcv1.GetLoadBalancerListenerOptions{LoadBalancerID: ptr.To(loadBalancerID), ID: ptr.To("pool-id-24")})).Return(loadBalancerListener24, nil, nil).AnyTimes() + mockClient.EXPECT().ListLoadBalancerPoolMembers(gomock.AssignableToTypeOf(&vpcv1.ListLoadBalancerPoolMembersOptions{})).Return(&vpcv1.LoadBalancerPoolMemberCollection{}, nil, nil).AnyTimes() + expectedLoadBalancerPoolMemberID6443 := "pool-member-6443" + expectedLoadBalancerPoolMember6443 := &vpcv1.LoadBalancerPoolMember{ID: ptr.To(expectedLoadBalancerPoolMemberID6443)} + mockClient.EXPECT().CreateLoadBalancerPoolMember(gomock.AssignableToTypeOf(&vpcv1.CreateLoadBalancerPoolMemberOptions{})).Return(expectedLoadBalancerPoolMember6443, nil, nil).Times(1) + result, err := scope.CreateVPCLoadBalancerPoolMember(ctx) + + g.Expect(err).To(BeNil()) + g.Expect(*result.ID).To(Equal(expectedLoadBalancerPoolMemberID6443)) + + expectedLoadBalancerPoolMemberID24 := "pool-member-24" + expectedLoadBalancerPoolMember24 := &vpcv1.LoadBalancerPoolMember{ID: ptr.To(expectedLoadBalancerPoolMemberID24)} + mockClient.EXPECT().CreateLoadBalancerPoolMember(gomock.AssignableToTypeOf(&vpcv1.CreateLoadBalancerPoolMemberOptions{})).Return(expectedLoadBalancerPoolMember24, nil, nil).Times(1) + result1, err1 := scope.CreateVPCLoadBalancerPoolMember(ctx) + + g.Expect(err1).To(BeNil()) + g.Expect(*result1.ID).To(Equal(expectedLoadBalancerPoolMemberID24)) + }) t.Run("Create VPC Load Balancer Pool Member", func(t *testing.T) { t.Run("No load balancers present in status", func(t *testing.T) { g := NewWithT(t) @@ -1539,14 +1889,14 @@ func TestCreateVPCLoadBalancerPoolMemberPowerVSMachine(t *testing.T) { Spec: infrav1beta2.IBMPowerVSClusterSpec{ LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ { - Name: "load-balancer-0", + Name: loadBalancerName, ID: ptr.To(loadBalancerID), }, }, }, Status: infrav1beta2.IBMPowerVSClusterStatus{ LoadBalancers: map[string]infrav1beta2.VPCLoadBalancerStatus{ - "load-balancer-0": { + loadBalancerName: { ID: ptr.To(loadBalancerID), }, }, @@ -1575,14 +1925,14 @@ func TestCreateVPCLoadBalancerPoolMemberPowerVSMachine(t *testing.T) { Spec: infrav1beta2.IBMPowerVSClusterSpec{ LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ { - Name: "load-balancer-0", + Name: loadBalancerName, ID: ptr.To(loadBalancerID), }, }, }, Status: infrav1beta2.IBMPowerVSClusterStatus{ LoadBalancers: map[string]infrav1beta2.VPCLoadBalancerStatus{ - "load-balancer-0": { + loadBalancerName: { ID: ptr.To(loadBalancerID), }, }, @@ -1610,14 +1960,14 @@ func TestCreateVPCLoadBalancerPoolMemberPowerVSMachine(t *testing.T) { Spec: infrav1beta2.IBMPowerVSClusterSpec{ LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ { - Name: "load-balancer-0", + Name: loadBalancerName, ID: ptr.To(loadBalancerID), }, }, }, Status: infrav1beta2.IBMPowerVSClusterStatus{ LoadBalancers: map[string]infrav1beta2.VPCLoadBalancerStatus{ - "load-balancer-0": { + loadBalancerName: { ID: ptr.To(loadBalancerID), }, }, @@ -1634,7 +1984,6 @@ func TestCreateVPCLoadBalancerPoolMemberPowerVSMachine(t *testing.T) { g := NewWithT(t) setup(t) t.Cleanup(teardown) - loadBalancerName := "load-balancer-0" targetPort := 3430 loadBalancers := &vpcv1.LoadBalancer{ ID: ptr.To(loadBalancerID), @@ -1721,7 +2070,6 @@ func TestCreateVPCLoadBalancerPoolMemberPowerVSMachine(t *testing.T) { g := NewWithT(t) setup(t) t.Cleanup(teardown) - loadBalancerName := "load-balancer-0" targetPort := 3430 loadBalancers := &vpcv1.LoadBalancer{ diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmpowervsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmpowervsclusters.yaml index daaa224b2..3161cf001 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmpowervsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmpowervsclusters.yaml @@ -301,6 +301,54 @@ spec: - tcp - udp type: string + selector: + description: |- + The selector is used to find IBMPowerVSMachines with matching labels. + If the label matches, the machine is then added to the load balancer listener configuration. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic required: - port type: object diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmpowervsclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmpowervsclustertemplates.yaml index 79e17cf6f..0a5b74c5c 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmpowervsclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmpowervsclustertemplates.yaml @@ -336,6 +336,55 @@ spec: - tcp - udp type: string + selector: + description: |- + The selector is used to find IBMPowerVSMachines with matching labels. + If the label matches, the machine is then added to the load balancer listener configuration. + properties: + matchExpressions: + description: matchExpressions is a list of + label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic required: - port type: object diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclusters.yaml index a76af55c2..3c7126e8f 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclusters.yaml @@ -293,6 +293,54 @@ spec: - tcp - udp type: string + selector: + description: |- + The selector is used to find IBMPowerVSMachines with matching labels. + If the label matches, the machine is then added to the load balancer listener configuration. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic required: - port type: object @@ -566,6 +614,55 @@ spec: - tcp - udp type: string + selector: + description: |- + The selector is used to find IBMPowerVSMachines with matching labels. + If the label matches, the machine is then added to the load balancer listener configuration. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic required: - port type: object diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclustertemplates.yaml index 2d08691fe..6b6af9c48 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclustertemplates.yaml @@ -136,6 +136,55 @@ spec: - tcp - udp type: string + selector: + description: |- + The selector is used to find IBMPowerVSMachines with matching labels. + If the label matches, the machine is then added to the load balancer listener configuration. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic required: - port type: object @@ -417,6 +466,55 @@ spec: - tcp - udp type: string + selector: + description: |- + The selector is used to find IBMPowerVSMachines with matching labels. + If the label matches, the machine is then added to the load balancer listener configuration. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic required: - port type: object diff --git a/controllers/ibmpowervsmachine_controller.go b/controllers/ibmpowervsmachine_controller.go index 1f38dea18..8f4715205 100644 --- a/controllers/ibmpowervsmachine_controller.go +++ b/controllers/ibmpowervsmachine_controller.go @@ -400,18 +400,6 @@ func (r *IBMPowerVSMachineReconciler) reconcileNormal(ctx context.Context, machi return ctrl.Result{RequeueAfter: 2 * time.Minute}, nil } - // We configure load balancer for only control-plane machines - if !util.IsControlPlaneMachine(machineScope.Machine) { - log.Info("Skipping load balancer configuration for worker machine") - conditions.MarkTrue(machineScope.IBMPowerVSMachine, infrav1beta2.InstanceReadyCondition) - v1beta2conditions.Set(machineScope.IBMPowerVSMachine, metav1.Condition{ - Type: infrav1beta2.IBMPowerVSMachineInstanceReadyV1Beta2Condition, - Status: metav1.ConditionTrue, - Reason: infrav1beta2.IBMPowerVSMachineInstanceReadyV1Beta2Reason, - }) - return ctrl.Result{}, nil - } - if machineScope.IBMPowerVSCluster.Spec.VPC == nil || machineScope.IBMPowerVSCluster.Spec.VPC.Region == nil { log.Info("Skipping configuring machine to load balancer as VPC is not set") conditions.MarkTrue(machineScope.IBMPowerVSMachine, infrav1beta2.InstanceReadyCondition) diff --git a/internal/webhooks/ibmpowervscluster.go b/internal/webhooks/ibmpowervscluster.go index cff59571f..4b68bb2a8 100644 --- a/internal/webhooks/ibmpowervscluster.go +++ b/internal/webhooks/ibmpowervscluster.go @@ -19,11 +19,13 @@ package webhooks import ( "context" "fmt" + "reflect" "strconv" regionUtil "github.com/ppc64le-cloud/powervs-utils" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" @@ -64,16 +66,20 @@ func (r *IBMPowerVSCluster) ValidateCreate(_ context.Context, obj runtime.Object if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a IBMPowerVSCluster but got a %T", obj)) } - return validateIBMPowerVSCluster(objValue) + return validateIBMPowerVSCluster(nil, objValue) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type. -func (r *IBMPowerVSCluster) ValidateUpdate(_ context.Context, _, newObj runtime.Object) (warnings admission.Warnings, err error) { - objValue, ok := newObj.(*infrav1beta2.IBMPowerVSCluster) +func (r *IBMPowerVSCluster) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (warnings admission.Warnings, err error) { + oldObjValue, ok := oldObj.(*infrav1beta2.IBMPowerVSCluster) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a IBMPowerVSCluster but got a %T", oldObj)) + } + newObjValue, ok := newObj.(*infrav1beta2.IBMPowerVSCluster) if !ok { return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a IBMPowerVSCluster but got a %T", newObj)) } - return validateIBMPowerVSCluster(objValue) + return validateIBMPowerVSCluster(oldObjValue, newObjValue) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type. @@ -81,15 +87,21 @@ func (r *IBMPowerVSCluster) ValidateDelete(_ context.Context, _ runtime.Object) return nil, nil } -func validateIBMPowerVSCluster(cluster *infrav1beta2.IBMPowerVSCluster) (admission.Warnings, error) { +func validateIBMPowerVSCluster(oldCluster, newCluster *infrav1beta2.IBMPowerVSCluster) (admission.Warnings, error) { var allErrs field.ErrorList - if err := validateIBMPowerVSClusterNetwork(cluster); err != nil { + if err := validateIBMPowerVSClusterNetwork(newCluster); err != nil { allErrs = append(allErrs, err) } - if err := validateIBMPowerVSClusterCreateInfraPrereq(cluster); err != nil { + if err := validateIBMPowerVSClusterCreateInfraPrereq(newCluster); err != nil { allErrs = append(allErrs, err...) } + // Need not validate for create operation + if oldCluster != nil { + if err := validateAdditionalListenerSelector(newCluster, oldCluster); err != nil { + allErrs = append(allErrs, err...) + } + } if len(allErrs) == 0 { return nil, nil @@ -97,7 +109,7 @@ func validateIBMPowerVSCluster(cluster *infrav1beta2.IBMPowerVSCluster) (admissi return nil, apierrors.NewInvalid( schema.GroupKind{Group: "infrastructure.cluster.x-k8s.io", Kind: "IBMPowerVSCluster"}, - cluster.Name, allErrs) + newCluster.Name, allErrs) } func validateIBMPowerVSClusterNetwork(cluster *infrav1beta2.IBMPowerVSCluster) *field.Error { @@ -234,3 +246,20 @@ func validateIBMPowerVSClusterCreateInfraPrereq(cluster *infrav1beta2.IBMPowerVS return allErrs } + +func validateAdditionalListenerSelector(newCluster, oldCluster *infrav1beta2.IBMPowerVSCluster) (allErrs field.ErrorList) { + newLoadBalancerListeners := map[string]metav1.LabelSelector{} + for _, loadbalancer := range newCluster.Spec.LoadBalancers { + for _, additionalListener := range loadbalancer.AdditionalListeners { + newLoadBalancerListeners[fmt.Sprintf("%d-%s", additionalListener.Port, *additionalListener.Protocol)] = additionalListener.Selector + } + } + for _, loadbalancer := range oldCluster.Spec.LoadBalancers { + for _, additionalListener := range loadbalancer.AdditionalListeners { + if selector, ok := newLoadBalancerListeners[fmt.Sprintf("%d-%s", additionalListener.Port, *additionalListener.Protocol)]; ok && !reflect.DeepEqual(selector, additionalListener.Selector) { + allErrs = append(allErrs, field.Forbidden(field.NewPath("selector"), "Selector is immutable")) + } + } + } + return allErrs +} diff --git a/internal/webhooks/ibmpowervscluster_test.go b/internal/webhooks/ibmpowervscluster_test.go index 8d6c287d4..9750699f4 100644 --- a/internal/webhooks/ibmpowervscluster_test.go +++ b/internal/webhooks/ibmpowervscluster_test.go @@ -237,6 +237,171 @@ func TestIBMPowerVSCluster_update(t *testing.T) { }, wantErr: true, }, + { + name: "Should error if the additionalListener selector is changed for same port and protocol", + oldPowervsCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + ServiceInstanceID: "capi-si-id", + Network: infrav1beta2.IBMPowerVSResourceReference{ + ID: ptr.To("capi-net-id"), + }, + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: "load-balancer-1", + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 23, + Protocol: &infrav1beta2.VPCLoadBalancerListenerProtocolTCP, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "listener-selector": "port-23", + }, + }, + }, + }, + }, + }, + }, + }, + newPowervsCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + ServiceInstanceID: "capi-si-id", + Network: infrav1beta2.IBMPowerVSResourceReference{ + ID: ptr.To("capi-net-id"), + }, + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: "load-balancer-1", + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 23, + Protocol: &infrav1beta2.VPCLoadBalancerListenerProtocolTCP, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "listener-selector": "port-23-1", + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "Should work if there is an additional listener added", + oldPowervsCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + ServiceInstanceID: "capi-si-id", + Network: infrav1beta2.IBMPowerVSResourceReference{ + ID: ptr.To("capi-net-id"), + }, + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: "load-balancer-1", + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 23, + Protocol: &infrav1beta2.VPCLoadBalancerListenerProtocolTCP, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "listener-selector": "port-23", + }, + }, + }, + }, + }, + }, + }, + }, + newPowervsCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + ServiceInstanceID: "capi-si-id", + Network: infrav1beta2.IBMPowerVSResourceReference{ + ID: ptr.To("capi-net-id"), + }, + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: "load-balancer-1", + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 23, + Protocol: &infrav1beta2.VPCLoadBalancerListenerProtocolTCP, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "listener-selector": "port-23", + }, + }, + }, + { + Port: 25, + Protocol: &infrav1beta2.VPCLoadBalancerListenerProtocolTCP, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "listener-selector": "port-25", + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Should work if the additionalListener selector is updated with new port and protocol", + oldPowervsCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + ServiceInstanceID: "capi-si-id", + Network: infrav1beta2.IBMPowerVSResourceReference{ + ID: ptr.To("capi-net-id"), + }, + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: "load-balancer-1", + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 23, + Protocol: &infrav1beta2.VPCLoadBalancerListenerProtocolTCP, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "listener-selector": "port-23", + }, + }, + }, + }, + }, + }, + }, + }, + newPowervsCluster: &infrav1beta2.IBMPowerVSCluster{ + Spec: infrav1beta2.IBMPowerVSClusterSpec{ + ServiceInstanceID: "capi-si-id", + Network: infrav1beta2.IBMPowerVSResourceReference{ + ID: ptr.To("capi-net-id"), + }, + LoadBalancers: []infrav1beta2.VPCLoadBalancerSpec{ + { + Name: "load-balancer-1", + AdditionalListeners: []infrav1beta2.AdditionalListenerSpec{ + { + Port: 25, + Protocol: &infrav1beta2.VPCLoadBalancerListenerProtocolTCP, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "listener-selector": "port-25", + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: false, + }, } for _, tc := range tests { diff --git a/pkg/cloud/services/vpc/mock/vpc_generated.go b/pkg/cloud/services/vpc/mock/vpc_generated.go index 008f4e332..5d8308830 100644 --- a/pkg/cloud/services/vpc/mock/vpc_generated.go +++ b/pkg/cloud/services/vpc/mock/vpc_generated.go @@ -414,6 +414,22 @@ func (mr *MockVpcMockRecorder) GetLoadBalancerByName(loadBalancerName any) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoadBalancerByName", reflect.TypeOf((*MockVpc)(nil).GetLoadBalancerByName), loadBalancerName) } +// GetLoadBalancerListener mocks base method. +func (m *MockVpc) GetLoadBalancerListener(options *vpcv1.GetLoadBalancerListenerOptions) (*vpcv1.LoadBalancerListener, *core.DetailedResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLoadBalancerListener", options) + ret0, _ := ret[0].(*vpcv1.LoadBalancerListener) + ret1, _ := ret[1].(*core.DetailedResponse) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetLoadBalancerListener indicates an expected call of GetLoadBalancerListener. +func (mr *MockVpcMockRecorder) GetLoadBalancerListener(options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoadBalancerListener", reflect.TypeOf((*MockVpc)(nil).GetLoadBalancerListener), options) +} + // GetLoadBalancerPoolByName mocks base method. func (m *MockVpc) GetLoadBalancerPoolByName(loadBalancerID, poolName string) (*vpcv1.LoadBalancerPool, error) { m.ctrl.T.Helper() diff --git a/pkg/cloud/services/vpc/service.go b/pkg/cloud/services/vpc/service.go index a1abce385..f980c6612 100644 --- a/pkg/cloud/services/vpc/service.go +++ b/pkg/cloud/services/vpc/service.go @@ -199,6 +199,11 @@ func (s *Service) ListLoadBalancerPoolMembers(options *vpcv1.ListLoadBalancerPoo return s.vpcService.ListLoadBalancerPoolMembers(options) } +// GetLoadBalancerListener returns the associated listeners of a load balancer. +func (s *Service) GetLoadBalancerListener(options *vpcv1.GetLoadBalancerListenerOptions) (*vpcv1.LoadBalancerListener, *core.DetailedResponse, error) { + return s.vpcService.GetLoadBalancerListener(options) +} + // ListKeys returns list of keys in a region. func (s *Service) ListKeys(options *vpcv1.ListKeysOptions) (*vpcv1.KeyCollection, *core.DetailedResponse, error) { return s.vpcService.ListKeys(options) diff --git a/pkg/cloud/services/vpc/vpc.go b/pkg/cloud/services/vpc/vpc.go index a15847859..5c3d0fcd9 100644 --- a/pkg/cloud/services/vpc/vpc.go +++ b/pkg/cloud/services/vpc/vpc.go @@ -51,6 +51,7 @@ type Vpc interface { CreateLoadBalancerPoolMember(options *vpcv1.CreateLoadBalancerPoolMemberOptions) (*vpcv1.LoadBalancerPoolMember, *core.DetailedResponse, error) DeleteLoadBalancerPoolMember(options *vpcv1.DeleteLoadBalancerPoolMemberOptions) (*core.DetailedResponse, error) ListLoadBalancerPoolMembers(options *vpcv1.ListLoadBalancerPoolMembersOptions) (*vpcv1.LoadBalancerPoolMemberCollection, *core.DetailedResponse, error) + GetLoadBalancerListener(options *vpcv1.GetLoadBalancerListenerOptions) (*vpcv1.LoadBalancerListener, *core.DetailedResponse, error) ListKeys(options *vpcv1.ListKeysOptions) (*vpcv1.KeyCollection, *core.DetailedResponse, error) CreateImage(options *vpcv1.CreateImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) ListImages(options *vpcv1.ListImagesOptions) (*vpcv1.ImageCollection, *core.DetailedResponse, error)