package controllers import ( "reflect" "strconv" "testing" multiclusterv1alpha1 "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/apis/multicluster/v1alpha1" "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/pkg/model" "github.com/aws/aws-cloud-map-mcs-controller-for-k8s/test" "github.com/stretchr/testify/assert" v1 "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" ) func TestServicePortToPort(t *testing.T) { type args struct { svcPort v1.ServicePort } tests := []struct { name string args args want model.Port }{ { name: "happy case", args: args{ svcPort: v1.ServicePort{ Name: "http", Protocol: v1.ProtocolTCP, Port: 80, TargetPort: intstr.IntOrString{ Type: intstr.Int, IntVal: 8080, }, }, }, want: model.Port{ Name: "http", Port: 80, TargetPort: "8080", Protocol: "TCP", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ServicePortToPort(tt.args.svcPort); !reflect.DeepEqual(got, tt.want) { t.Errorf("ServicePortToPort() = %v, want %v", got, tt.want) } }) } } func TestServiceImportPortToPort(t *testing.T) { type args struct { svcImportPort multiclusterv1alpha1.ServicePort } tests := []struct { name string args args want model.Port }{ { name: "happy case", args: args{ svcImportPort: multiclusterv1alpha1.ServicePort{ Name: test.PortName1, Protocol: v1.ProtocolTCP, Port: 80, }, }, want: model.Port{ Name: test.PortName1, Port: 80, Protocol: test.Protocol1, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ServiceImportPortToPort(tt.args.svcImportPort); !reflect.DeepEqual(got, tt.want) { t.Errorf("ServiceImportPortToPort() = %v, want %v", got, tt.want) } }) } } func TestEndpointPortToPort(t *testing.T) { type args struct { port discovery.EndpointPort } name := "http" protocolTCP := v1.ProtocolTCP port := int32(80) tests := []struct { name string args args want model.Port }{ { name: "happy case", args: args{ port: discovery.EndpointPort{ Name: &name, Protocol: &protocolTCP, Port: &port, }, }, want: model.Port{ Name: "http", Port: 80, TargetPort: "", Protocol: "TCP", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := EndpointPortToPort(tt.args.port); !reflect.DeepEqual(got, tt.want) { t.Errorf("EndpointPortToPort() = %v, want %v", got, tt.want) } }) } } func TestPortToServicePort(t *testing.T) { type args struct { port model.Port } tests := []struct { name string args args want v1.ServicePort }{ { name: "happy case", args: args{ port: model.Port{ Name: "http", Port: 80, TargetPort: "8080", Protocol: "TCP", }, }, want: v1.ServicePort{ Name: "http", Protocol: v1.ProtocolTCP, Port: 80, TargetPort: intstr.IntOrString{ Type: intstr.Int, IntVal: 8080, }, }, }, { name: "happy case for string targertPort", args: args{ port: model.Port{ Name: "http", Port: 80, TargetPort: "https", Protocol: "TCP", }, }, want: v1.ServicePort{ Name: "http", Protocol: v1.ProtocolTCP, Port: 80, TargetPort: intstr.IntOrString{ Type: intstr.String, StrVal: "https", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := PortToServicePort(tt.args.port); !reflect.DeepEqual(got, tt.want) { t.Errorf("PortToServicePort() = %v, want %v", got, tt.want) } }) } } func TestPortToServiceImportPort(t *testing.T) { type args struct { port model.Port } tests := []struct { name string args args want multiclusterv1alpha1.ServicePort }{ { name: "happy case", args: args{ port: model.Port{ Name: test.PortName1, Port: test.Port1, TargetPort: test.PortStr2, // ignored Protocol: test.Protocol1, }, }, want: multiclusterv1alpha1.ServicePort{ Name: test.PortName1, Protocol: v1.ProtocolTCP, Port: test.Port1, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := PortToServiceImportPort(tt.args.port); !reflect.DeepEqual(got, tt.want) { t.Errorf("PortToServiceImportPort() = %v, want %v", got, tt.want) } }) } } func TestPortToEndpointPort(t *testing.T) { name := "http" protocolTCP := v1.ProtocolTCP port := int32(80) type args struct { port model.Port } tests := []struct { name string args args want discovery.EndpointPort }{ { name: "happy case", args: args{ port: model.Port{ Name: "http", Port: 80, Protocol: "TCP", }, }, want: discovery.EndpointPort{ Name: &name, Protocol: &protocolTCP, Port: &port, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := PortToEndpointPort(tt.args.port); !reflect.DeepEqual(got, tt.want) { t.Errorf("PortToEndpointPort() = %v, want %v", got, tt.want) } }) } } func TestExtractServicePorts(t *testing.T) { type args struct { endpoints []*model.Endpoint } tests := []struct { name string args args want []*model.Port }{ { name: "unique service ports extracted", args: args{ endpoints: []*model.Endpoint{ { ServicePort: model.Port{Protocol: test.Protocol1, Port: test.Port1}, }, { ServicePort: model.Port{Protocol: test.Protocol1, Port: test.Port2}, }, { ServicePort: model.Port{Protocol: test.Protocol2, Port: test.Port2}, }, }, }, want: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, {Protocol: test.Protocol1, Port: test.Port2}, {Protocol: test.Protocol2, Port: test.Port2}, }, }, { name: "duplicate and endpoint ports ignored", args: args{ endpoints: []*model.Endpoint{ { ServicePort: model.Port{Protocol: test.Protocol1, Port: test.Port1}, EndpointPort: model.Port{Protocol: test.Protocol1, Port: test.Port1}, }, { ServicePort: model.Port{Protocol: test.Protocol1, Port: test.Port1}, EndpointPort: model.Port{Protocol: test.Protocol2, Port: test.Port2}, }, }, }, want: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ExtractServicePorts(tt.args.endpoints); !PortsEqualIgnoreOrder(got, tt.want) { t.Errorf("ServicePortToPort() = %v, want %v", got, tt.want) } }) } } func TestExtractEndpointPorts(t *testing.T) { type args struct { endpoints []*model.Endpoint } tests := []struct { name string args args want []*model.Port }{ { name: "unique endpoint ports extracted", args: args{ endpoints: []*model.Endpoint{ { EndpointPort: model.Port{Protocol: test.Protocol1, Port: test.Port1}, }, { EndpointPort: model.Port{Protocol: test.Protocol1, Port: test.Port2}, }, { EndpointPort: model.Port{Protocol: test.Protocol2, Port: test.Port2}, }, }, }, want: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, {Protocol: test.Protocol1, Port: test.Port2}, {Protocol: test.Protocol2, Port: test.Port2}, }, }, { name: "duplicate and service ports ignored", args: args{ endpoints: []*model.Endpoint{ { EndpointPort: model.Port{Protocol: test.Protocol1, Port: test.Port1}, ServicePort: model.Port{Protocol: test.Protocol1, Port: test.Port1}, }, { EndpointPort: model.Port{Protocol: test.Protocol1, Port: test.Port1}, ServicePort: model.Port{Protocol: test.Protocol2, Port: test.Port2}, }, }, }, want: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ExtractEndpointPorts(tt.args.endpoints); !PortsEqualIgnoreOrder(got, tt.want) { t.Errorf("ServicePortToPort() = %v, want %v", got, tt.want) } }) } } func TestPortsEqualIgnoreOrder(t *testing.T) { type args struct { portsA []*model.Port portsB []*model.Port } tests := []struct { name string args args want bool }{ { name: "ports equal same order", args: args{ portsA: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, {Protocol: test.Protocol2, Port: test.Port2}, }, portsB: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, {Protocol: test.Protocol2, Port: test.Port2}, }, }, want: true, }, { name: "ports equal different order", args: args{ portsA: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, {Protocol: test.Protocol2, Port: test.Port2}, }, portsB: []*model.Port{ {Protocol: test.Protocol2, Port: test.Port2}, {Protocol: test.Protocol1, Port: test.Port1}, }, }, want: true, }, { name: "ports not equal", args: args{ portsA: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, {Protocol: test.Protocol2, Port: test.Port2}, }, portsB: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, {Protocol: test.Protocol2, Port: 3}, }, }, want: false, }, { name: "protocols not equal", args: args{ portsA: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, {Protocol: test.Protocol2, Port: test.Port2}, }, portsB: []*model.Port{ {Protocol: test.Protocol1, Port: test.Port1}, {Protocol: test.Protocol1, Port: test.Port2}, }, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := PortsEqualIgnoreOrder(tt.args.portsA, tt.args.portsB); !(got == tt.want) { t.Errorf("PortsEqualIgnoreOrder() = %v, want %v", got, tt.want) } }) } } func TestIPsEqualIgnoreOrder(t *testing.T) { type args struct { ipsA []string ipsB []string } tests := []struct { name string args args want bool }{ { name: "ips equal same order", args: args{ ipsA: []string{ test.ClusterIp1, test.ClusterIp2, }, ipsB: []string{ test.ClusterIp1, test.ClusterIp2, }, }, want: true, }, { name: "ips equal different order", args: args{ ipsA: []string{ test.ClusterIp1, test.ClusterIp2, }, ipsB: []string{ test.ClusterIp2, test.ClusterIp1, }, }, want: true, }, { name: "ips not equal", args: args{ ipsA: []string{ test.ClusterIp1, test.ClusterIp2, }, ipsB: []string{ test.ClusterIp1, "10.10.10.3", }, }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := IPsEqualIgnoreOrder(tt.args.ipsA, tt.args.ipsB); !(got == tt.want) { t.Errorf("IPsEqualIgnoreOrder() = %v, want %v", got, tt.want) } }) } } func TestGetClusterIpsFromServices(t *testing.T) { type args struct { services []*v1.Service } tests := []struct { name string args args want []string }{ { name: "happy case", args: args{ services: []*v1.Service{ { ObjectMeta: metav1.ObjectMeta{}, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeClusterIP, ClusterIP: test.ClusterIp1, }, }, { ObjectMeta: metav1.ObjectMeta{}, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeClusterIP, ClusterIP: test.ClusterIp2, }, }, }}, want: []string{ test.ClusterIp1, test.ClusterIp2, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := GetClusterIpsFromServices(tt.args.services); !reflect.DeepEqual(got, tt.want) { t.Errorf("GetClusterIpsFromServices() = %v, want %v", got, tt.want) } }) } } func TestDerivedService(t *testing.T) { const numTests = 100 derivedServiceMap := make(map[string]bool) for i := 0; i < numTests; i++ { namespace := test.HttpNsName name := "test-svcname-" + strconv.Itoa(i) clusterId := "test-clusterid-" + strconv.Itoa(i) derivedService := DerivedName(namespace, name, clusterId) assert.NotContains(t, derivedServiceMap, derivedService, "derived service already exists") derivedServiceMap[derivedService] = true } assert.Equal(t, numTests, len(derivedServiceMap)) assert.True(t, DerivedName(test.HttpNsName, test.SvcName, test.ClusterId1) != DerivedName(test.HttpNsName, test.SvcName, test.ClusterId2)) } func TestCreateServiceImportStruct(t *testing.T) { type args struct { servicePorts []*model.Port endpoints []*model.Endpoint clusterIds []string } tests := []struct { name string args args want multiclusterv1alpha1.ServiceImport }{ { name: "happy case", args: args{ clusterIds: []string{test.ClusterId1, test.ClusterId2}, servicePorts: []*model.Port{ {Name: test.PortName1, Protocol: test.Protocol1, Port: test.Port1}, {Name: test.PortName2, Protocol: test.Protocol2, Port: test.Port2}, }, endpoints: []*model.Endpoint{ { ServiceType: model.ClusterSetIPType, }, }, }, want: multiclusterv1alpha1.ServiceImport{ ObjectMeta: metav1.ObjectMeta{ Namespace: test.HttpNsName, Name: test.SvcName, Annotations: map[string]string{ DerivedServiceAnnotation: CreateDerivedServiceAnnotation(test.HttpNsName, test.SvcName, []string{test.ClusterId1, test.ClusterId2}), }, }, Spec: multiclusterv1alpha1.ServiceImportSpec{ IPs: []string{}, Type: multiclusterv1alpha1.ClusterSetIP, Ports: []multiclusterv1alpha1.ServicePort{ {Name: test.PortName1, Protocol: v1.ProtocolTCP, Port: test.Port1}, {Name: test.PortName2, Protocol: v1.ProtocolUDP, Port: test.Port2}, }, }, Status: multiclusterv1alpha1.ServiceImportStatus{ Clusters: []multiclusterv1alpha1.ClusterStatus{ { Cluster: test.ClusterId1, }, { Cluster: test.ClusterId2, }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := CreateServiceImportStruct(test.GetTestServiceWithEndpoint(tt.args.endpoints), tt.args.clusterIds, tt.args.servicePorts); !reflect.DeepEqual(*got, tt.want) { t.Errorf("CreateServiceImportStruct() = %v, want %v", *got, tt.want) } }) } } func TestExtractServiceType(t *testing.T) { tests := []struct { name string svc *v1.Service want model.ServiceType }{ { name: "cluster ip type", svc: &v1.Service{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: test.SvcName, Namespace: test.HttpNsName, }, Spec: v1.ServiceSpec{ Ports: []v1.ServicePort{{ Name: test.PortName1, Protocol: test.Protocol1, Port: test.ServicePort1, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: test.Port1}, }}, ClusterIP: "10.108.89.43", }, Status: v1.ServiceStatus{}, }, want: model.ClusterSetIPType, }, { name: "headless type", svc: &v1.Service{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{ Name: test.SvcName, Namespace: test.HttpNsName, }, Spec: v1.ServiceSpec{ Ports: []v1.ServicePort{{ Name: test.PortName1, Protocol: test.Protocol1, Port: test.ServicePort1, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: test.Port1}, }}, ClusterIP: "None", }, Status: v1.ServiceStatus{}, }, want: model.HeadlessType, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ExtractServiceType(tt.svc); got != tt.want { t.Errorf("ExtractServiceType() = %v, want %v", got, tt.want) } }) } } func TestServiceTypetoServiceImportType(t *testing.T) { tests := []struct { name string svcType model.ServiceType want multiclusterv1alpha1.ServiceImportType }{ { name: "cluster ip type", svcType: model.ClusterSetIPType, want: multiclusterv1alpha1.ClusterSetIP, }, { name: "headless type", svcType: model.HeadlessType, want: multiclusterv1alpha1.Headless, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ServiceTypetoServiceImportType(tt.svcType); got != tt.want { t.Errorf("ServiceTypetoServiceImportType() = %v, want %v", got, tt.want) } }) } } func TestCreateDerivedServiceStruct(t *testing.T) { type args struct { servicePorts []*model.Port svcImport *multiclusterv1alpha1.ServiceImport } tests := []struct { name string args args want *v1.ServiceSpec }{ { name: "cluster ip case", args: args{ servicePorts: []*model.Port{ {Name: test.PortName1, Protocol: test.Protocol1, Port: test.Port1, TargetPort: "8080"}, {Name: test.PortName2, Protocol: test.Protocol2, Port: test.Port2, TargetPort: "8080"}, }, svcImport: &multiclusterv1alpha1.ServiceImport{ ObjectMeta: metav1.ObjectMeta{ Namespace: test.HttpNsName, Name: test.SvcName, Annotations: map[string]string{DerivedServiceAnnotation: DerivedName(test.HttpNsName, test.SvcName, test.ClusterId1)}, }, Spec: multiclusterv1alpha1.ServiceImportSpec{ IPs: []string{}, Type: multiclusterv1alpha1.ClusterSetIP, }, }, }, want: &v1.ServiceSpec{ Type: v1.ServiceTypeClusterIP, Ports: []v1.ServicePort{ {Name: test.PortName1, Protocol: test.Protocol1, Port: test.Port1, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: 8080}}, {Name: test.PortName2, Protocol: test.Protocol2, Port: test.Port2, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: 8080}}, }, }, }, { name: "headless case", args: args{ servicePorts: []*model.Port{ {Name: test.PortName1, Protocol: test.Protocol1, Port: test.Port1, TargetPort: "8080"}, {Name: test.PortName2, Protocol: test.Protocol2, Port: test.Port2, TargetPort: "8080"}, }, svcImport: &multiclusterv1alpha1.ServiceImport{ ObjectMeta: metav1.ObjectMeta{ Namespace: test.HttpNsName, Name: test.SvcName, Annotations: map[string]string{DerivedServiceAnnotation: DerivedName(test.HttpNsName, test.SvcName, test.ClusterId1)}, }, Spec: multiclusterv1alpha1.ServiceImportSpec{ IPs: []string{}, Type: multiclusterv1alpha1.Headless, }, }, }, want: &v1.ServiceSpec{ Type: v1.ServiceTypeClusterIP, Ports: []v1.ServicePort{ {Name: test.PortName1, Protocol: test.Protocol1, Port: test.Port1, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: 8080}}, {Name: test.PortName2, Protocol: test.Protocol2, Port: test.Port2, TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: 8080}}, }, ClusterIP: "None", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := &CreateDerivedServiceStruct(tt.args.svcImport, tt.args.servicePorts, test.ClusterId1).Spec; !reflect.DeepEqual(got, tt.want) { t.Errorf("CreateDerivedServiceStruct() = %v, want %v", got, tt.want) } }) } } func TestCreateDerivedServiceAnnotation(t *testing.T) { type args struct { namespace string name string clusterIds []string } tests := []struct { name string args args want string }{ { name: "create derived service annotation", args: args{ namespace: test.HttpNsName, name: test.SvcName, clusterIds: []string{test.ClusterId1, test.ClusterId2}, }, want: "[{\"cluster\":\"test-mcs-clusterid-1\",\"derived-service\":\"imported-vm6pdvp7di\"},{\"cluster\":\"test-mcs-clusterid-2\",\"derived-service\":\"imported-i8hm9c3um2\"}]", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := CreateDerivedServiceAnnotation(tt.args.namespace, tt.args.name, tt.args.clusterIds); got != tt.want { t.Errorf("CreateDerivedServiceAnnotation() = %v, want %v", got, tt.want) } }) } }