diff --git a/go-controller/pkg/node/default_node_network_controller.go b/go-controller/pkg/node/default_node_network_controller.go index 4b65ab95e0..f05de14473 100644 --- a/go-controller/pkg/node/default_node_network_controller.go +++ b/go-controller/pkg/node/default_node_network_controller.go @@ -1191,9 +1191,15 @@ func (nc *DefaultNodeNetworkController) reconcileConntrackUponEndpointSliceEvent return fmt.Errorf("cannot reconcile conntrack: %v", err) } svc, err := nc.watchFactory.GetService(namespacedName.Namespace, namespacedName.Name) - if err != nil && !kerrors.IsNotFound(err) { + if err != nil { + if kerrors.IsNotFound(err) { + klog.V(5).Infof("Service %s/%s not found (might have been deleted) when reconciling conntrack for endpointslice %s", + namespacedName.Namespace, namespacedName.Name, oldEndpointSlice.Name) + // service is not found, likely deleted, flushing service conntrack entries will be handled at service reconciliation. No-op here. + return nil + } return fmt.Errorf("error while retrieving service for endpointslice %s/%s when reconciling conntrack: %v", - newEndpointSlice.Namespace, newEndpointSlice.Name, err) + oldEndpointSlice.Namespace, oldEndpointSlice.Name, err) } for _, oldPort := range oldEndpointSlice.Ports { if *oldPort.Protocol != kapi.ProtocolUDP { // flush conntrack only for UDP @@ -1207,10 +1213,21 @@ func (nc *DefaultNodeNetworkController) reconcileConntrackUponEndpointSliceEvent if newEndpointSlice != nil && util.DoesEndpointSliceContainEligibleEndpoint(newEndpointSlice, oldIPStr, *oldPort.Port, *oldPort.Protocol, svc) { continue } + portName := "" + if oldPort.Name != nil { + portName = *oldPort.Name + } + servicePort, err := util.FindServicePortForEndpointSlicePort(svc, portName, *oldPort.Protocol) + if err != nil { + klog.Errorf("Failed to get service port for endpoint %s: %v", oldIPStr, err) + continue + } // upon update and delete events, flush conntrack only for UDP - if err := util.DeleteConntrackServicePort(oldIPStr, *oldPort.Port, *oldPort.Protocol, + klog.V(5).Infof("Deleting conntrack entry for endpoint %s, port %d, protocol %s", oldIPStr, servicePort.Port, *oldPort.Protocol) + if err := util.DeleteConntrackServicePort(oldIPStr, servicePort.Port, *oldPort.Protocol, netlink.ConntrackReplyAnyIP, nil); err != nil { - klog.Errorf("Failed to delete conntrack entry for %s: %v", oldIPStr, err) + klog.Errorf("Failed to delete conntrack entry for %s port %d: %v", oldIPStr, servicePort.Port, err) + errors = append(errors, err) } } } diff --git a/go-controller/pkg/node/default_node_network_controller_test.go b/go-controller/pkg/node/default_node_network_controller_test.go index bbfab0b07e..47e1470cb6 100644 --- a/go-controller/pkg/node/default_node_network_controller_test.go +++ b/go-controller/pkg/node/default_node_network_controller_test.go @@ -4,22 +4,29 @@ import ( "context" "fmt" "net" + "syscall" + "github.com/stretchr/testify/mock" "github.com/urfave/cli/v2" "github.com/vishvananda/netlink" "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/config" + "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/factory" "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/kube/mocks" - ovntest "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/testing" netlink_mocks "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/testing/mocks/github.com/vishvananda/netlink" "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/types" util "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/util" utilMocks "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/util/mocks" + corev1 "k8s.io/api/core/v1" kapi "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes/fake" . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" ) @@ -624,4 +631,439 @@ var _ = Describe("Node", func() { Expect(err).NotTo(HaveOccurred()) }) }) + + Describe("reconcileConntrackUponEndpointSliceEvents", func() { + const ( + testNamespace = "test-ns" + testServiceName = "test-service" + testEndpointSlice = "test-endpointslice" + ) + + var ( + udpProtocol = corev1.ProtocolUDP + tcpProtocol = corev1.ProtocolTCP + testEndpointPort1 = int32(8080) + testEndpointPort2 = int32(8443) + testServicePort1 = int32(80) + testServicePort2 = int32(443) + ) + + // expectedConntrackFilter represents the expected parameters for a conntrack filter + type expectedConntrackFilter struct { + ip string + port uint16 + protocol uint8 + family netlink.InetFamily + } + + // Test data structure for table-driven tests + type reconcileConntrackTestCase struct { + desc string + service *corev1.Service // nil means service not found + oldEndpointSlice *discovery.EndpointSlice + newEndpointSlice *discovery.EndpointSlice + expectedConntrackCalls int + expectedFilters []expectedConntrackFilter + } + + // Helper to create EndpointSlice + makeEndpointSlice := func(portConfigs []struct { + name *string + port int32 + protocol corev1.Protocol + }, addresses []string) *discovery.EndpointSlice { + ports := make([]discovery.EndpointPort, len(portConfigs)) + for i, pc := range portConfigs { + p := pc.port + proto := pc.protocol + ports[i] = discovery.EndpointPort{ + Name: pc.name, + Port: &p, + Protocol: &proto, + } + } + + return &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: testEndpointSlice, + Namespace: testNamespace, + Labels: map[string]string{ + discovery.LabelServiceName: testServiceName, + }, + }, + Ports: ports, + Endpoints: []discovery.Endpoint{ + {Addresses: addresses}, + }, + } + } + + // Helper to create Service + makeService := func(portConfigs []struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }) *corev1.Service { + ports := make([]corev1.ServicePort, len(portConfigs)) + for i, pc := range portConfigs { + ports[i] = corev1.ServicePort{ + Name: pc.name, + Port: pc.port, + TargetPort: intstr.FromInt(int(pc.targetPort)), + Protocol: pc.protocol, + } + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceName, + Namespace: testNamespace, + }, + Spec: corev1.ServiceSpec{ + Ports: ports, + }, + } + } + + // Helper function to build expected ConntrackFilter for verification + buildExpectedFilter := func(ef expectedConntrackFilter) *netlink.ConntrackFilter { + filter := &netlink.ConntrackFilter{} + + // Add protocol + if err := filter.AddProtocol(ef.protocol); err != nil { + GinkgoT().Fatalf("Failed to add protocol to expected filter: %v", err) + } + + // Add port + if ef.port > 0 { + if err := filter.AddPort(netlink.ConntrackOrigDstPort, ef.port); err != nil { + GinkgoT().Fatalf("Failed to add port to expected filter: %v", err) + } + } + + // Add IP + ipAddr := net.ParseIP(ef.ip) + if ipAddr == nil { + GinkgoT().Fatalf("Invalid IP address: %s", ef.ip) + } + if err := filter.AddIP(netlink.ConntrackReplyAnyIP, ipAddr); err != nil { + GinkgoT().Fatalf("Failed to add IP to expected filter: %v", err) + } + + return filter + } + + DescribeTable("should handle conntrack deletion correctly", + func(tc reconcileConntrackTestCase) { + // Setup mock for ConntrackDeleteFilter + mockNetLinkOps := new(utilMocks.NetLinkOps) + util.SetNetLinkOpMockInst(mockNetLinkOps) + defer util.ResetNetLinkOpMockInst() + + // Mock ConntrackDeleteFilter + mockNetLinkOps.On("ConntrackDeleteFilter", + mock.AnythingOfType("netlink.ConntrackTableType"), + mock.AnythingOfType("netlink.InetFamily"), + mock.AnythingOfType("*netlink.ConntrackFilter")). + Return(uint(1), nil).Maybe() + + // Setup fake client with service if provided + var fakeClient *fake.Clientset + if tc.service != nil { + fakeClient = fake.NewSimpleClientset(tc.service) + } else { + fakeClient = fake.NewSimpleClientset() + } + + wf, err := factory.NewNodeWatchFactory(&util.OVNNodeClientset{ + KubeClient: fakeClient, + }, "test-node") + Expect(err).NotTo(HaveOccurred()) + defer wf.Shutdown() + + err = wf.Start() + Expect(err).NotTo(HaveOccurred()) + + nc := &DefaultNodeNetworkController{ + BaseNodeNetworkController: BaseNodeNetworkController{ + CommonNodeNetworkControllerInfo: CommonNodeNetworkControllerInfo{ + watchFactory: wf, + }, + }, + } + + // Execute the function under test + err = nc.reconcileConntrackUponEndpointSliceEvents(tc.oldEndpointSlice, tc.newEndpointSlice) + Expect(err).NotTo(HaveOccurred()) + + // Verify the number of ConntrackDeleteFilter calls + mockNetLinkOps.AssertNumberOfCalls(GinkgoT(), "ConntrackDeleteFilter", tc.expectedConntrackCalls) + + // Collect all actual filters from the mock calls. + actualFilters := []*netlink.ConntrackFilter{} + for _, call := range mockNetLinkOps.Calls { + if call.Method == "ConntrackDeleteFilter" { + _, ok1 := call.Arguments.Get(0).(netlink.ConntrackTableType) + _, ok2 := call.Arguments.Get(1).(netlink.InetFamily) + filter, ok3 := call.Arguments.Get(2).(*netlink.ConntrackFilter) + + if ok1 && ok2 && ok3 { + actualFilters = append(actualFilters, filter) + } + } + } + + // Build the list of expected filters. + expectedNetlinkFilters := make([]*netlink.ConntrackFilter, 0, len(tc.expectedFilters)) + for _, expectedFilter := range tc.expectedFilters { + expectedNetlinkFilters = append(expectedNetlinkFilters, buildExpectedFilter(expectedFilter)) + } + + // Use gomega's ConsistOf to compare the actual and expected filters. + Expect(actualFilters).To(ConsistOf(expectedNetlinkFilters), "The set of conntrack filters to be deleted should match the expected set.") + }, + + Entry("old endpointslice is nil", + reconcileConntrackTestCase{ + desc: "should not delete any conntrack entries when old endpoint is nil", + service: makeService([]struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }{{name: "", port: testServicePort1, targetPort: testEndpointPort1, protocol: udpProtocol}}), + oldEndpointSlice: nil, + newEndpointSlice: &discovery.EndpointSlice{}, + expectedConntrackCalls: 0, + }, + ), + + Entry("service exists with matching unnamed port", + reconcileConntrackTestCase{ + desc: "should delete conntrack with service port for unnamed port", + service: makeService([]struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }{{name: "", port: testServicePort1, targetPort: testEndpointPort1, protocol: udpProtocol}}), + oldEndpointSlice: makeEndpointSlice( + []struct { + name *string + port int32 + protocol corev1.Protocol + }{{name: nil, port: testEndpointPort1, protocol: udpProtocol}}, + []string{"10.0.0.1"}, + ), + newEndpointSlice: nil, + expectedConntrackCalls: 1, + expectedFilters: []expectedConntrackFilter{ + {ip: "10.0.0.1", port: uint16(testServicePort1), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V4}, + }, + }, + ), + + Entry("service exists with matching named port", + reconcileConntrackTestCase{ + desc: "should delete conntrack with service port for named port", + service: makeService([]struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }{{name: "http", port: testServicePort1, targetPort: testEndpointPort1, protocol: udpProtocol}}), + oldEndpointSlice: makeEndpointSlice( + []struct { + name *string + port int32 + protocol corev1.Protocol + }{{name: strPtr("http"), port: testEndpointPort1, protocol: udpProtocol}}, + []string{"10.0.0.1"}, + ), + newEndpointSlice: nil, + expectedConntrackCalls: 1, + expectedFilters: []expectedConntrackFilter{ + {ip: "10.0.0.1", port: uint16(testServicePort1), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V4}, + }, + }, + ), + + Entry("service exists but port name mismatch", + reconcileConntrackTestCase{ + desc: "should skip conntrack deletion when port name doesn't match", + service: makeService([]struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }{{name: "http", port: testServicePort1, targetPort: testEndpointPort1, protocol: udpProtocol}}), + oldEndpointSlice: makeEndpointSlice( + []struct { + name *string + port int32 + protocol corev1.Protocol + }{{name: strPtr("grpc"), port: testEndpointPort1, protocol: udpProtocol}}, + []string{"10.0.0.1"}, + ), + newEndpointSlice: nil, + expectedConntrackCalls: 0, + }, + ), + + Entry("service not found", + reconcileConntrackTestCase{ + desc: "should return early without deleting conntrack when service not found", + service: nil, + oldEndpointSlice: makeEndpointSlice( + []struct { + name *string + port int32 + protocol corev1.Protocol + }{{name: nil, port: testEndpointPort1, protocol: udpProtocol}}, + []string{"10.0.0.1"}, + ), + newEndpointSlice: nil, + expectedConntrackCalls: 0, + }, + ), + + Entry("TCP protocol should be skipped", + reconcileConntrackTestCase{ + desc: "should skip conntrack deletion for TCP protocol", + service: makeService([]struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }{{name: "", port: testServicePort1, targetPort: testEndpointPort1, protocol: tcpProtocol}}), + oldEndpointSlice: makeEndpointSlice( + []struct { + name *string + port int32 + protocol corev1.Protocol + }{{name: nil, port: testEndpointPort1, protocol: tcpProtocol}}, + []string{"10.0.0.1"}, + ), + newEndpointSlice: nil, + expectedConntrackCalls: 0, + }, + ), + + Entry("multiple endpoints", + reconcileConntrackTestCase{ + desc: "should delete conntrack for each endpoint", + service: makeService([]struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }{{name: "", port: testServicePort1, targetPort: testEndpointPort1, protocol: udpProtocol}}), + oldEndpointSlice: makeEndpointSlice( + []struct { + name *string + port int32 + protocol corev1.Protocol + }{{name: nil, port: testEndpointPort1, protocol: udpProtocol}}, + []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}, + ), + newEndpointSlice: nil, + expectedConntrackCalls: 3, + expectedFilters: []expectedConntrackFilter{ + {ip: "10.0.0.1", port: uint16(testServicePort1), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V4}, + {ip: "10.0.0.2", port: uint16(testServicePort1), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V4}, + {ip: "10.0.0.3", port: uint16(testServicePort1), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V4}, + }, + }, + ), + + Entry("IPv6 endpoint", + reconcileConntrackTestCase{ + desc: "should delete conntrack for IPv6 endpoint", + service: makeService([]struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }{{name: "", port: testServicePort1, targetPort: testEndpointPort1, protocol: udpProtocol}}), + oldEndpointSlice: makeEndpointSlice( + []struct { + name *string + port int32 + protocol corev1.Protocol + }{{name: nil, port: testEndpointPort1, protocol: udpProtocol}}, + []string{"fd00::1"}, + ), + newEndpointSlice: nil, + expectedConntrackCalls: 1, + expectedFilters: []expectedConntrackFilter{ + {ip: "fd00::1", port: uint16(testServicePort1), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V6}, + }, + }, + ), + + Entry("dual-stack endpoints", + reconcileConntrackTestCase{ + desc: "should delete conntrack for both IPv4 and IPv6", + service: makeService([]struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }{{name: "", port: testServicePort1, targetPort: testEndpointPort1, protocol: udpProtocol}}), + oldEndpointSlice: makeEndpointSlice( + []struct { + name *string + port int32 + protocol corev1.Protocol + }{{name: nil, port: testEndpointPort1, protocol: udpProtocol}}, + []string{"10.0.0.1", "fd00::1"}, + ), + newEndpointSlice: nil, + expectedConntrackCalls: 2, + expectedFilters: []expectedConntrackFilter{ + {ip: "10.0.0.1", port: uint16(testServicePort1), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V4}, + {ip: "fd00::1", port: uint16(testServicePort1), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V6}, + }, + }, + ), + + Entry("multiple service ports with matching names", + reconcileConntrackTestCase{ + desc: "should match correct service port by name for multiple ports", + service: makeService([]struct { + name string + port int32 + targetPort int32 + protocol corev1.Protocol + }{ + {name: "http", port: testServicePort1, targetPort: testEndpointPort1, protocol: udpProtocol}, + {name: "https", port: testServicePort2, targetPort: testEndpointPort2, protocol: udpProtocol}, + }), + oldEndpointSlice: makeEndpointSlice( + []struct { + name *string + port int32 + protocol corev1.Protocol + }{ + {name: strPtr("http"), port: testEndpointPort1, protocol: udpProtocol}, + {name: strPtr("https"), port: testEndpointPort2, protocol: udpProtocol}, + }, + []string{"10.0.0.1"}, + ), + newEndpointSlice: nil, + expectedConntrackCalls: 2, + expectedFilters: []expectedConntrackFilter{ + {ip: "10.0.0.1", port: uint16(testServicePort1), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V4}, + {ip: "10.0.0.1", port: uint16(testServicePort2), protocol: syscall.IPPROTO_UDP, family: netlink.FAMILY_V4}, + }, + }, + ), + ) + }) }) + +// Helper function to create string pointer +func strPtr(s string) *string { + return &s +} diff --git a/go-controller/pkg/util/util.go b/go-controller/pkg/util/util.go index e634bc0fa8..5d10ad5f64 100644 --- a/go-controller/pkg/util/util.go +++ b/go-controller/pkg/util/util.go @@ -390,3 +390,20 @@ func GenerateId(length int) string { } return string(b) } + +// FindServicePortForEndpointSlicePort returns the ServicePort that corresponds to an EndpointSlice port +// by matching the port name and protocol. This is the canonical way to map EndpointSlice ports to +// Service ports, as Kubernetes guarantees that ServicePort.Name matches EndpointPort.Name. +func FindServicePortForEndpointSlicePort(service *v1.Service, endpointslicePortName string, endpointslicePortProtocol v1.Protocol) (*v1.ServicePort, error) { + if service == nil { + return nil, fmt.Errorf("unable to resolve port for endpointslice %q/%q: service is nil", + endpointslicePortName, endpointslicePortProtocol) + } + for _, servicePort := range service.Spec.Ports { + if servicePort.Name == endpointslicePortName && servicePort.Protocol == endpointslicePortProtocol { + return &servicePort, nil + } + } + return nil, fmt.Errorf("service %s/%s has no port with name %q and protocol %s", + service.Namespace, service.Name, endpointslicePortName, endpointslicePortProtocol) +} diff --git a/go-controller/pkg/util/util_unit_test.go b/go-controller/pkg/util/util_unit_test.go index 9ab9055601..15519148d1 100644 --- a/go-controller/pkg/util/util_unit_test.go +++ b/go-controller/pkg/util/util_unit_test.go @@ -10,6 +10,9 @@ import ( "testing" ovntest "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/testing" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" mock_k8s_io_utils_exec "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/testing/mocks/k8s.io/utils/exec" "github.com/ovn-org/ovn-kubernetes/go-controller/pkg/util/mocks" @@ -246,3 +249,150 @@ func TestGenerateId(t *testing.T) { matchesPattern, _ := regexp.MatchString("([a-zA-Z0-9-]*)", id) assert.True(t, matchesPattern) } + +func TestFindServicePortForEndpointSlicePort(t *testing.T) { + tcp := v1.ProtocolTCP + udp := v1.ProtocolUDP + + tests := []struct { + name string + service *v1.Service + endpointslicePortName string + endpointslicePortProtocol v1.Protocol + wantPort *v1.ServicePort + wantErr bool + }{ + { + name: "Match named port with TCP protocol", + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + {Name: "http", Protocol: tcp, Port: 80, TargetPort: intstr.FromInt(8080)}, + {Name: "https", Protocol: tcp, Port: 443, TargetPort: intstr.FromInt(8443)}, + }, + }, + }, + endpointslicePortName: "http", + endpointslicePortProtocol: tcp, + wantPort: &v1.ServicePort{Name: "http", Protocol: tcp, Port: 80, TargetPort: intstr.FromInt(8080)}, + wantErr: false, + }, + { + name: "Match unnamed port (empty string)", + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + {Name: "", Protocol: tcp, Port: 80, TargetPort: intstr.FromInt(8080)}, + }, + }, + }, + endpointslicePortName: "", + endpointslicePortProtocol: tcp, + wantPort: &v1.ServicePort{Name: "", Protocol: tcp, Port: 80, TargetPort: intstr.FromInt(8080)}, + wantErr: false, + }, + { + name: "Protocol mismatch", + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + {Name: "dns", Protocol: tcp, Port: 53, TargetPort: intstr.FromInt(5353)}, + }, + }, + }, + endpointslicePortName: "dns", + endpointslicePortProtocol: udp, + wantPort: nil, + wantErr: true, + }, + { + name: "Port name not found", + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + {Name: "http", Protocol: tcp, Port: 80, TargetPort: intstr.FromInt(8080)}, + }, + }, + }, + endpointslicePortName: "https", + endpointslicePortProtocol: tcp, + wantPort: nil, + wantErr: true, + }, + { + name: "Multiple ports, match second one", + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + {Name: "http", Protocol: tcp, Port: 80, TargetPort: intstr.FromInt(8080)}, + {Name: "grpc", Protocol: tcp, Port: 9090, TargetPort: intstr.FromInt(9091)}, + {Name: "metrics", Protocol: tcp, Port: 8080, TargetPort: intstr.FromInt(8081)}, + }, + }, + }, + endpointslicePortName: "grpc", + endpointslicePortProtocol: tcp, + wantPort: &v1.ServicePort{Name: "grpc", Protocol: tcp, Port: 9090, TargetPort: intstr.FromInt(9091)}, + wantErr: false, + }, + { + name: "Named target port (not numeric)", + service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-ns", + Name: "test-svc", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + {Name: "web", Protocol: tcp, Port: 80, TargetPort: intstr.FromString("http")}, + }, + }, + }, + endpointslicePortName: "web", + endpointslicePortProtocol: tcp, + wantPort: &v1.ServicePort{Name: "web", Protocol: tcp, Port: 80, TargetPort: intstr.FromString("http")}, + wantErr: false, + }, + { + name: "Nil service input", + service: nil, + endpointslicePortName: "web", + endpointslicePortProtocol: tcp, + wantPort: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := FindServicePortForEndpointSlicePort(tt.service, tt.endpointslicePortName, tt.endpointslicePortProtocol) + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantPort, got) + } + }) + } +}