diff --git a/api/v1alpha1/perconaservermysql_types.go b/api/v1alpha1/perconaservermysql_types.go index 7c6c99e5e..c590ca336 100644 --- a/api/v1alpha1/perconaservermysql_types.go +++ b/api/v1alpha1/perconaservermysql_types.go @@ -502,7 +502,7 @@ type ServiceExpose struct { ExternalTrafficPolicy corev1.ServiceExternalTrafficPolicyType `json:"externalTrafficPolicy,omitempty"` } -// Determines if both annotations and labels of the service expose are empty. +// SaveOldMeta determines if both annotations and labels of the service expose are empty. func (e *ServiceExpose) SaveOldMeta() bool { return len(e.Annotations) == 0 && len(e.Labels) == 0 } diff --git a/e2e-tests/tests/async-ignore-annotations/04-assert.yaml b/e2e-tests/tests/async-ignore-annotations/04-assert.yaml index a2c42a78f..3bfdcdcec 100644 --- a/e2e-tests/tests/async-ignore-annotations/04-assert.yaml +++ b/e2e-tests/tests/async-ignore-annotations/04-assert.yaml @@ -52,8 +52,6 @@ spec: app.kubernetes.io/name: mysql app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22 - rack-test: rack-test-22 sessionAffinity: None type: ClusterIP --- @@ -113,8 +111,6 @@ spec: app.kubernetes.io/name: haproxy app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22 - rack-test: rack-test-22 sessionAffinity: None type: ClusterIP --- @@ -163,7 +159,5 @@ spec: app.kubernetes.io/name: orchestrator app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22 - rack-test: rack-test-22 sessionAffinity: None type: ClusterIP diff --git a/e2e-tests/tests/async-ignore-annotations/05-assert.yaml b/e2e-tests/tests/async-ignore-annotations/05-assert.yaml index 3bee722f2..6bcac4d12 100644 --- a/e2e-tests/tests/async-ignore-annotations/05-assert.yaml +++ b/e2e-tests/tests/async-ignore-annotations/05-assert.yaml @@ -50,7 +50,6 @@ spec: app.kubernetes.io/name: mysql app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22 sessionAffinity: None type: ClusterIP --- @@ -108,7 +107,6 @@ spec: app.kubernetes.io/name: haproxy app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22 sessionAffinity: None type: ClusterIP --- @@ -155,6 +153,5 @@ spec: app.kubernetes.io/name: orchestrator app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22 sessionAffinity: None type: ClusterIP diff --git a/e2e-tests/tests/async-ignore-annotations/06-assert.yaml b/e2e-tests/tests/async-ignore-annotations/06-assert.yaml index b0d960eb2..ab8bb58a6 100644 --- a/e2e-tests/tests/async-ignore-annotations/06-assert.yaml +++ b/e2e-tests/tests/async-ignore-annotations/06-assert.yaml @@ -49,7 +49,6 @@ spec: app.kubernetes.io/managed-by: percona-server-mysql-operator app.kubernetes.io/name: mysql app.kubernetes.io/part-of: percona-server - rack: rack-22-test sessionAffinity: None type: ClusterIP --- @@ -107,7 +106,6 @@ spec: app.kubernetes.io/name: haproxy app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22-test sessionAffinity: None type: ClusterIP --- @@ -154,6 +152,5 @@ spec: app.kubernetes.io/name: orchestrator app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22-test sessionAffinity: None type: ClusterIP diff --git a/e2e-tests/tests/async-ignore-annotations/07-assert.yaml b/e2e-tests/tests/async-ignore-annotations/07-assert.yaml index cd3a0eb45..9fe20b442 100644 --- a/e2e-tests/tests/async-ignore-annotations/07-assert.yaml +++ b/e2e-tests/tests/async-ignore-annotations/07-assert.yaml @@ -48,7 +48,6 @@ spec: app.kubernetes.io/name: mysql app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22-test sessionAffinity: None type: ClusterIP --- @@ -104,7 +103,6 @@ spec: app.kubernetes.io/name: haproxy app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22-test sessionAffinity: None type: ClusterIP --- @@ -149,7 +147,6 @@ spec: app.kubernetes.io/name: orchestrator app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22-test sessionAffinity: None type: ClusterIP diff --git a/e2e-tests/tests/gr-ignore-annotations/04-assert.yaml b/e2e-tests/tests/gr-ignore-annotations/04-assert.yaml index 8f9363961..0aa7d05d9 100644 --- a/e2e-tests/tests/gr-ignore-annotations/04-assert.yaml +++ b/e2e-tests/tests/gr-ignore-annotations/04-assert.yaml @@ -56,8 +56,6 @@ spec: app.kubernetes.io/name: mysql app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack-test: rack-test-22 - rack: rack-22 sessionAffinity: None type: LoadBalancer --- @@ -126,7 +124,5 @@ spec: app.kubernetes.io/name: router app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack-test: rack-test-22 - rack: rack-22 sessionAffinity: None type: LoadBalancer diff --git a/e2e-tests/tests/gr-ignore-annotations/05-assert.yaml b/e2e-tests/tests/gr-ignore-annotations/05-assert.yaml index ade0d1566..73b79d510 100644 --- a/e2e-tests/tests/gr-ignore-annotations/05-assert.yaml +++ b/e2e-tests/tests/gr-ignore-annotations/05-assert.yaml @@ -54,7 +54,6 @@ spec: app.kubernetes.io/name: mysql app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22 sessionAffinity: None type: LoadBalancer --- @@ -121,6 +120,5 @@ spec: app.kubernetes.io/name: router app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22 sessionAffinity: None type: LoadBalancer diff --git a/e2e-tests/tests/gr-ignore-annotations/06-assert.yaml b/e2e-tests/tests/gr-ignore-annotations/06-assert.yaml index 4ee401c0c..54bedc7ac 100644 --- a/e2e-tests/tests/gr-ignore-annotations/06-assert.yaml +++ b/e2e-tests/tests/gr-ignore-annotations/06-assert.yaml @@ -54,7 +54,6 @@ spec: app.kubernetes.io/name: mysql app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22-test sessionAffinity: None type: LoadBalancer --- @@ -121,6 +120,5 @@ spec: app.kubernetes.io/name: router app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22-test sessionAffinity: None type: LoadBalancer diff --git a/e2e-tests/tests/gr-ignore-annotations/07-assert.yaml b/e2e-tests/tests/gr-ignore-annotations/07-assert.yaml index 9a10bda2e..823d41fea 100644 --- a/e2e-tests/tests/gr-ignore-annotations/07-assert.yaml +++ b/e2e-tests/tests/gr-ignore-annotations/07-assert.yaml @@ -52,7 +52,6 @@ spec: app.kubernetes.io/name: mysql app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22-test sessionAffinity: None type: LoadBalancer --- @@ -117,6 +116,5 @@ spec: app.kubernetes.io/name: router app.kubernetes.io/part-of: percona-server app.kubernetes.io/version: v0.12.0 - rack: rack-22-test sessionAffinity: None type: LoadBalancer diff --git a/pkg/haproxy/haproxy.go b/pkg/haproxy/haproxy.go index 38971c4e7..ba38c503b 100644 --- a/pkg/haproxy/haproxy.go +++ b/pkg/haproxy/haproxy.go @@ -61,6 +61,8 @@ func Service(cr *apiv1alpha1.PerconaServerMySQL, secret *corev1.Secret) *corev1. labels := MatchLabels(cr) labels = util.SSMapMerge(expose.Labels, labels) + selector := MatchLabels(cr) + serviceType := cr.Spec.Proxy.HAProxy.Expose.Type var loadBalancerSourceRanges []string @@ -117,7 +119,7 @@ func Service(cr *apiv1alpha1.PerconaServerMySQL, secret *corev1.Secret) *corev1. Spec: corev1.ServiceSpec{ Type: serviceType, Ports: ports, - Selector: labels, + Selector: selector, LoadBalancerSourceRanges: loadBalancerSourceRanges, InternalTrafficPolicy: expose.InternalTrafficPolicy, ExternalTrafficPolicy: externalTrafficPolicy, diff --git a/pkg/haproxy/haproxy_test.go b/pkg/haproxy/haproxy_test.go index 20ebc7e12..7e8a80a3b 100644 --- a/pkg/haproxy/haproxy_test.go +++ b/pkg/haproxy/haproxy_test.go @@ -3,6 +3,7 @@ package haproxy import ( "testing" + apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -233,3 +234,90 @@ func TestStatefulset(t *testing.T) { assert.Equal(t, expectedLivenessProbe, *hContainer.LivenessProbe) }) } + +func TestService(t *testing.T) { + podName := "test-cluster-haproxy" + + cr := &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "test-namespace", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{ + Proxy: apiv1alpha1.ProxySpec{ + HAProxy: &apiv1alpha1.HAProxySpec{ + Expose: apiv1alpha1.ServiceExpose{ + Type: corev1.ServiceTypeLoadBalancer, + Labels: map[string]string{ + "custom-label": "custom-value", + }, + Annotations: map[string]string{ + "custom-annotation": "custom-annotation-value", + }, + LoadBalancerSourceRanges: []string{"10.0.0.0/8"}, + }, + }, + }, + }, + } + + tests := map[string]struct { + serviceType corev1.ServiceType + expectLoadBalancer bool + expectExternalTrafficPolicy bool + }{ + "LoadBalancer service": { + serviceType: corev1.ServiceTypeLoadBalancer, + expectLoadBalancer: true, + expectExternalTrafficPolicy: true, + }, + "NodePort service": { + serviceType: corev1.ServiceTypeNodePort, + expectLoadBalancer: false, + expectExternalTrafficPolicy: true, + }, + "ClusterIP service": { + serviceType: corev1.ServiceTypeClusterIP, + expectLoadBalancer: false, + expectExternalTrafficPolicy: false, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + cr.Spec.Proxy.HAProxy.Expose.Type = tt.serviceType + + service := Service(cr, nil) + + assert.Equal(t, "v1", service.APIVersion) + assert.Equal(t, "Service", service.Kind) + assert.Equal(t, podName, service.Name) + assert.Equal(t, "test-namespace", service.Namespace) + + assert.Equal(t, tt.serviceType, service.Spec.Type) + + expectedLabels := MatchLabels(cr) + expectedLabels["custom-label"] = "custom-value" + assert.Equal(t, expectedLabels, service.Labels) + + expectedSelector := MatchLabels(cr) + assert.Equal(t, expectedSelector, service.Spec.Selector) + + assert.Equal(t, cr.Spec.Proxy.HAProxy.Expose.Annotations, service.Annotations) + + if tt.expectLoadBalancer { + assert.Equal(t, cr.Spec.Proxy.HAProxy.Expose.LoadBalancerSourceRanges, service.Spec.LoadBalancerSourceRanges) + } else { + assert.Empty(t, service.Spec.LoadBalancerSourceRanges) + } + + if tt.expectExternalTrafficPolicy { + assert.Equal(t, cr.Spec.Proxy.HAProxy.Expose.ExternalTrafficPolicy, service.Spec.ExternalTrafficPolicy) + } else { + assert.Empty(t, service.Spec.ExternalTrafficPolicy) + } + + assert.Equal(t, cr.Spec.Proxy.HAProxy.Expose.InternalTrafficPolicy, service.Spec.InternalTrafficPolicy) + }) + } +} diff --git a/pkg/k8s/utils_test.go b/pkg/k8s/utils_test.go new file mode 100644 index 000000000..a84b20c5d --- /dev/null +++ b/pkg/k8s/utils_test.go @@ -0,0 +1,336 @@ +package k8s + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" +) + +func TestEnsureService(t *testing.T) { + scheme := runtime.NewScheme() + err := corev1.AddToScheme(scheme) + require.NoError(t, err) + err = apiv1alpha1.AddToScheme(scheme) + require.NoError(t, err) + + tests := map[string]struct { + cr *apiv1alpha1.PerconaServerMySQL + svc *corev1.Service + existingSvc *corev1.Service + saveOldMeta bool + expectError bool + validate func(t *testing.T, cl client.Client, svc *corev1.Service) + }{ + "no ignore annotations or labels, saveOldMeta false": { + cr: &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr", + Namespace: "default", + UID: "test-uid", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{}, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 3306, Name: "mysql"}, + }, + }, + }, + saveOldMeta: false, + expectError: false, + validate: func(t *testing.T, cl client.Client, svc *corev1.Service) { + result := &corev1.Service{} + err := cl.Get(context.Background(), types.NamespacedName{ + Name: svc.Name, Namespace: svc.Namespace}, result) + assert.NoError(t, err) + assert.Equal(t, "test-service", result.Name) + assert.Equal(t, "default", result.Namespace) + assert.NotEmpty(t, result.GetAnnotations()["percona.com/last-config-hash"]) + }, + }, + "service doesn't exist - creates new service": { + cr: &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr", + Namespace: "default", + UID: "test-uid", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{ + IgnoreAnnotations: []string{"ignore.me"}, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-service", + Namespace: "default", + Annotations: map[string]string{ + "new": "annotation", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 3306, Name: "mysql"}, + }, + }, + }, + saveOldMeta: true, + expectError: false, + validate: func(t *testing.T, cl client.Client, svc *corev1.Service) { + result := &corev1.Service{} + err := cl.Get(context.Background(), types.NamespacedName{ + Name: svc.Name, Namespace: svc.Namespace}, result) + assert.NoError(t, err) + assert.Equal(t, "new-service", result.Name) + assert.Contains(t, result.GetAnnotations(), "new") + }, + }, + "service exists - preserves old metadata when saveOldMeta is true": { + cr: &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr", + Namespace: "default", + UID: "test-uid", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{}, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-service", + Namespace: "default", + Annotations: map[string]string{ + "new": "annotation", + }, + Labels: map[string]string{ + "new": "label", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 3306, Name: "mysql"}, + }, + }, + }, + existingSvc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-service", + Namespace: "default", + Annotations: map[string]string{ + "old": "annotation", + }, + Labels: map[string]string{ + "old": "label", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 80, Name: "http"}, + }, + }, + }, + saveOldMeta: true, + expectError: false, + validate: func(t *testing.T, cl client.Client, svc *corev1.Service) { + result := &corev1.Service{} + err := cl.Get(context.Background(), types.NamespacedName{ + Name: svc.Name, Namespace: svc.Namespace}, result) + assert.NoError(t, err) + assert.Contains(t, result.GetAnnotations(), "old") + assert.Contains(t, result.GetAnnotations(), "new") + assert.Contains(t, result.GetLabels(), "old") + assert.Contains(t, result.GetLabels(), "new") + }, + }, + "service exists - dont preserve old metadata when saveOldMeta is true": { + cr: &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr", + Namespace: "default", + UID: "test-uid", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{}, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-service", + Namespace: "default", + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 3306, Name: "mysql"}, + }, + }, + }, + existingSvc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "existing-service", + Namespace: "default", + Annotations: map[string]string{ + "old": "annotation", + }, + Labels: map[string]string{ + "old": "label", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 80, Name: "http"}, + }, + }, + }, + saveOldMeta: false, + expectError: false, + validate: func(t *testing.T, cl client.Client, svc *corev1.Service) { + result := &corev1.Service{} + err := cl.Get(context.Background(), types.NamespacedName{ + Name: svc.Name, Namespace: svc.Namespace}, result) + assert.NoError(t, err) + assert.NotContains(t, result.GetAnnotations(), "old") + assert.NotContains(t, result.GetLabels(), "old") + }, + }, + "service exists - handles ignored annotations": { + cr: &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr", + Namespace: "default", + UID: "test-uid", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{ + IgnoreAnnotations: []string{"ignore.annotation"}, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-with-ignored", + Namespace: "default", + Annotations: map[string]string{ + "keep": "this", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 3306, Name: "mysql"}, + }, + }, + }, + existingSvc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-with-ignored", + Namespace: "default", + Annotations: map[string]string{ + "ignore.annotation": "should-be-kept", + "remove": "this", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 80, Name: "http"}, + }, + }, + }, + saveOldMeta: false, + expectError: false, + validate: func(t *testing.T, cl client.Client, svc *corev1.Service) { + result := &corev1.Service{} + err := cl.Get(context.Background(), types.NamespacedName{ + Name: svc.Name, Namespace: svc.Namespace}, result) + assert.NoError(t, err) + assert.Contains(t, result.GetAnnotations(), "ignore.annotation") + assert.Contains(t, result.GetAnnotations(), "keep") + assert.NotContains(t, result.GetAnnotations(), "remove") + }, + }, + "service exists - handles ignored labels": { + cr: &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr", + Namespace: "default", + UID: "test-uid", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{ + IgnoreLabels: []string{"ignore.label"}, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-with-ignored-labels", + Namespace: "default", + Labels: map[string]string{ + "keep": "this", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 3306, Name: "mysql"}, + }, + }, + }, + existingSvc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "service-with-ignored-labels", + Namespace: "default", + Labels: map[string]string{ + "ignore.label": "should-be-kept", + "remove": "this", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Port: 80, Name: "http"}, + }, + }, + }, + saveOldMeta: false, + expectError: false, + validate: func(t *testing.T, cl client.Client, svc *corev1.Service) { + result := &corev1.Service{} + err := cl.Get(context.Background(), types.NamespacedName{ + Name: svc.Name, Namespace: svc.Namespace}, result) + assert.NoError(t, err) + assert.Contains(t, result.GetLabels(), "ignore.label") + assert.Contains(t, result.GetLabels(), "keep") + assert.NotContains(t, result.GetLabels(), "remove") + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + var objects []client.Object + if tt.existingSvc != nil { + objects = append(objects, tt.existingSvc) + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(objects...). + Build() + + err := EnsureService(context.Background(), fakeClient, tt.cr, tt.svc, scheme, tt.saveOldMeta) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + if tt.validate != nil { + tt.validate(t, fakeClient, tt.svc) + } + } + }) + } +} diff --git a/pkg/mysql/mysql.go b/pkg/mysql/mysql.go index 1b236235d..12e46e6b2 100644 --- a/pkg/mysql/mysql.go +++ b/pkg/mysql/mysql.go @@ -490,7 +490,6 @@ func PodService(cr *apiv1alpha1.PerconaServerMySQL, t corev1.ServiceType, podNam selector := MatchLabels(cr) selector["statefulset.kubernetes.io/pod-name"] = podName - selector = util.SSMapMerge(expose.Labels, selector) var loadBalancerSourceRanges []string if t == corev1.ServiceTypeLoadBalancer { @@ -534,7 +533,6 @@ func PrimaryService(cr *apiv1alpha1.PerconaServerMySQL) *corev1.Service { selector := MatchLabels(cr) selector[naming.LabelMySQLPrimary] = "true" - selector = util.SSMapMerge(expose.Labels, selector) var loadBalancerSourceRanges []string if expose.Type == corev1.ServiceTypeLoadBalancer { diff --git a/pkg/mysql/mysql_test.go b/pkg/mysql/mysql_test.go index 0fbc15a77..2c5613740 100644 --- a/pkg/mysql/mysql_test.go +++ b/pkg/mysql/mysql_test.go @@ -532,7 +532,6 @@ func TestPrimaryService_GroupReplication(t *testing.T) { assert.Equal(t, "v1", service.APIVersion) assert.Equal(t, "Service", service.Kind) - assert.Equal(t, PrimaryServiceName(cr), service.Name) assert.Equal(t, "test-cluster-mysql-primary", service.Name) assert.Equal(t, "test-namespace", service.Namespace) @@ -544,7 +543,6 @@ func TestPrimaryService_GroupReplication(t *testing.T) { expectedSelector := MatchLabels(cr) expectedSelector[naming.LabelMySQLPrimary] = "true" - expectedSelector["custom-label"] = "custom-value" assert.Equal(t, expectedSelector, service.Spec.Selector) assert.Equal(t, cr.Spec.MySQL.ExposePrimary.Annotations, service.Annotations) @@ -566,6 +564,97 @@ func TestPrimaryService_GroupReplication(t *testing.T) { } } +func TestPodService(t *testing.T) { + podName := "test-pod" + + cr := &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "test-namespace", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{ + MySQL: apiv1alpha1.MySQLSpec{ + ClusterType: apiv1alpha1.ClusterTypeGR, + Expose: apiv1alpha1.ServiceExposeTogglable{ + Enabled: true, + ServiceExpose: apiv1alpha1.ServiceExpose{ + Type: corev1.ServiceTypeLoadBalancer, + Labels: map[string]string{ + "custom-label": "custom-value", + }, + Annotations: map[string]string{ + "custom-annotation": "custom-annotation-value", + }, + LoadBalancerSourceRanges: []string{"10.0.0.0/8"}, + }, + }, + }, + }, + } + + tests := map[string]struct { + serviceType corev1.ServiceType + expectLoadBalancer bool + expectExternalTrafficPolicy bool + }{ + "LoadBalancer service for GR": { + serviceType: corev1.ServiceTypeLoadBalancer, + expectLoadBalancer: true, + expectExternalTrafficPolicy: true, + }, + "NodePort service for GR": { + serviceType: corev1.ServiceTypeNodePort, + expectLoadBalancer: false, + expectExternalTrafficPolicy: true, + }, + "ClusterIP service for GR": { + serviceType: corev1.ServiceTypeClusterIP, + expectLoadBalancer: false, + expectExternalTrafficPolicy: false, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + cr.Spec.MySQL.Expose.Type = tt.serviceType + + service := PodService(cr, tt.serviceType, podName) + + assert.Equal(t, "v1", service.APIVersion) + assert.Equal(t, "Service", service.Kind) + assert.Equal(t, podName, service.Name) + assert.Equal(t, "test-namespace", service.Namespace) + + assert.Equal(t, tt.serviceType, service.Spec.Type) + + expectedLabels := MatchLabels(cr) + expectedLabels["custom-label"] = "custom-value" + expectedLabels[naming.LabelExposed] = "true" + assert.Equal(t, expectedLabels, service.Labels) + + expectedSelector := MatchLabels(cr) + expectedSelector["statefulset.kubernetes.io/pod-name"] = podName + assert.Equal(t, expectedSelector, service.Spec.Selector) + + assert.Equal(t, cr.Spec.MySQL.Expose.Annotations, service.Annotations) + + if tt.expectLoadBalancer { + assert.Equal(t, cr.Spec.MySQL.Expose.LoadBalancerSourceRanges, service.Spec.LoadBalancerSourceRanges) + } else { + assert.Empty(t, service.Spec.LoadBalancerSourceRanges) + } + + if tt.expectExternalTrafficPolicy { + assert.Equal(t, cr.Spec.MySQL.Expose.ExternalTrafficPolicy, service.Spec.ExternalTrafficPolicy) + } else { + assert.Empty(t, service.Spec.ExternalTrafficPolicy) + } + + assert.Equal(t, cr.Spec.MySQL.Expose.InternalTrafficPolicy, service.Spec.InternalTrafficPolicy) + }) + } +} + func TestPrimaryServiceName(t *testing.T) { cr := &apiv1alpha1.PerconaServerMySQL{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/orchestrator/orchestrator.go b/pkg/orchestrator/orchestrator.go index 57e38f6b9..e246ff2f6 100644 --- a/pkg/orchestrator/orchestrator.go +++ b/pkg/orchestrator/orchestrator.go @@ -408,7 +408,6 @@ func PodService(cr *apiv1alpha1.PerconaServerMySQL, t corev1.ServiceType, podNam selector := MatchLabels(cr) selector["statefulset.kubernetes.io/pod-name"] = podName - selector = util.SSMapMerge(expose.Labels, selector) var loadBalancerSourceRanges []string if t == corev1.ServiceTypeLoadBalancer { diff --git a/pkg/orchestrator/orchestrator_test.go b/pkg/orchestrator/orchestrator_test.go index 1b8bf5ac0..78b555bbb 100644 --- a/pkg/orchestrator/orchestrator_test.go +++ b/pkg/orchestrator/orchestrator_test.go @@ -3,8 +3,11 @@ package orchestrator import ( "testing" + apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" + "github.com/percona/percona-server-mysql-operator/pkg/naming" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" "github.com/percona/percona-server-mysql-operator/pkg/platform" @@ -123,3 +126,90 @@ func TestStatefulSet(t *testing.T) { assert.Equal(t, tolerations, sts.Spec.Template.Spec.Tolerations) }) } + +func TestPodService(t *testing.T) { + podName := "test-pod" + + cr := &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "test-namespace", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{ + Orchestrator: apiv1alpha1.OrchestratorSpec{ + Expose: apiv1alpha1.ServiceExpose{ + Type: corev1.ServiceTypeLoadBalancer, + Labels: map[string]string{ + "custom-label": "custom-value", + }, + Annotations: map[string]string{ + "custom-annotation": "custom-annotation-value", + }, + LoadBalancerSourceRanges: []string{"10.0.0.0/8"}, + }, + }, + }, + } + + tests := map[string]struct { + serviceType corev1.ServiceType + expectLoadBalancer bool + expectExternalTrafficPolicy bool + }{ + "LoadBalancer service": { + serviceType: corev1.ServiceTypeLoadBalancer, + expectLoadBalancer: true, + expectExternalTrafficPolicy: true, + }, + "NodePort service": { + serviceType: corev1.ServiceTypeNodePort, + expectLoadBalancer: false, + expectExternalTrafficPolicy: true, + }, + "ClusterIP service": { + serviceType: corev1.ServiceTypeClusterIP, + expectLoadBalancer: false, + expectExternalTrafficPolicy: false, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + cr.Spec.MySQL.Expose.Type = tt.serviceType + + service := PodService(cr, tt.serviceType, podName) + + assert.Equal(t, "v1", service.APIVersion) + assert.Equal(t, "Service", service.Kind) + assert.Equal(t, podName, service.Name) + assert.Equal(t, "test-namespace", service.Namespace) + + assert.Equal(t, tt.serviceType, service.Spec.Type) + + expectedLabels := MatchLabels(cr) + expectedLabels["custom-label"] = "custom-value" + expectedLabels[naming.LabelExposed] = "true" + assert.Equal(t, expectedLabels, service.Labels) + + expectedSelector := MatchLabels(cr) + expectedSelector["statefulset.kubernetes.io/pod-name"] = podName + assert.Equal(t, expectedSelector, service.Spec.Selector) + + assert.Equal(t, cr.Spec.Orchestrator.Expose.Annotations, service.Annotations) + + if tt.expectLoadBalancer { + assert.Equal(t, cr.Spec.Orchestrator.Expose.LoadBalancerSourceRanges, service.Spec.LoadBalancerSourceRanges) + } else { + assert.Empty(t, service.Spec.LoadBalancerSourceRanges) + } + + if tt.expectExternalTrafficPolicy { + assert.Equal(t, cr.Spec.Orchestrator.Expose.ExternalTrafficPolicy, service.Spec.ExternalTrafficPolicy) + } else { + assert.Empty(t, service.Spec.ExternalTrafficPolicy) + } + + assert.Equal(t, cr.Spec.Orchestrator.Expose.InternalTrafficPolicy, service.Spec.InternalTrafficPolicy) + }) + } +} diff --git a/pkg/router/router.go b/pkg/router/router.go index a2c72226f..9a8ac70c9 100644 --- a/pkg/router/router.go +++ b/pkg/router/router.go @@ -60,6 +60,8 @@ func Service(cr *apiv1alpha1.PerconaServerMySQL) *corev1.Service { labels := util.SSMapMerge(expose.Labels, MatchLabels(cr)) + selector := MatchLabels(cr) + var loadBalancerSourceRanges []string if expose.Type == corev1.ServiceTypeLoadBalancer { loadBalancerSourceRanges = expose.LoadBalancerSourceRanges @@ -84,7 +86,7 @@ func Service(cr *apiv1alpha1.PerconaServerMySQL) *corev1.Service { Spec: corev1.ServiceSpec{ Type: expose.Type, Ports: ports(cr.Spec.Proxy.Router.Ports), - Selector: labels, + Selector: selector, LoadBalancerSourceRanges: loadBalancerSourceRanges, InternalTrafficPolicy: expose.InternalTrafficPolicy, ExternalTrafficPolicy: externalTrafficPolicy, diff --git a/pkg/router/router_test.go b/pkg/router/router_test.go index 12182d616..82840a937 100644 --- a/pkg/router/router_test.go +++ b/pkg/router/router_test.go @@ -5,8 +5,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" + apiv1alpha1 "github.com/percona/percona-server-mysql-operator/api/v1alpha1" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" "sigs.k8s.io/yaml" @@ -300,6 +302,93 @@ func TestPorts(t *testing.T) { } } +func TestService(t *testing.T) { + podName := "test-cluster-router" + + cr := &apiv1alpha1.PerconaServerMySQL{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Namespace: "test-namespace", + }, + Spec: apiv1alpha1.PerconaServerMySQLSpec{ + Proxy: apiv1alpha1.ProxySpec{ + Router: &apiv1alpha1.MySQLRouterSpec{ + Expose: apiv1alpha1.ServiceExpose{ + Type: corev1.ServiceTypeLoadBalancer, + Labels: map[string]string{ + "custom-label": "custom-value", + }, + Annotations: map[string]string{ + "custom-annotation": "custom-annotation-value", + }, + LoadBalancerSourceRanges: []string{"10.0.0.0/8"}, + }, + }, + }, + }, + } + + tests := map[string]struct { + serviceType corev1.ServiceType + expectLoadBalancer bool + expectExternalTrafficPolicy bool + }{ + "LoadBalancer service": { + serviceType: corev1.ServiceTypeLoadBalancer, + expectLoadBalancer: true, + expectExternalTrafficPolicy: true, + }, + "NodePort service": { + serviceType: corev1.ServiceTypeNodePort, + expectLoadBalancer: false, + expectExternalTrafficPolicy: true, + }, + "ClusterIP service": { + serviceType: corev1.ServiceTypeClusterIP, + expectLoadBalancer: false, + expectExternalTrafficPolicy: false, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + cr.Spec.Proxy.Router.Expose.Type = tt.serviceType + + service := Service(cr) + + assert.Equal(t, "v1", service.APIVersion) + assert.Equal(t, "Service", service.Kind) + assert.Equal(t, podName, service.Name) + assert.Equal(t, "test-namespace", service.Namespace) + + assert.Equal(t, tt.serviceType, service.Spec.Type) + + expectedLabels := MatchLabels(cr) + expectedLabels["custom-label"] = "custom-value" + assert.Equal(t, expectedLabels, service.Labels) + + expectedSelector := MatchLabels(cr) + assert.Equal(t, expectedSelector, service.Spec.Selector) + + assert.Equal(t, cr.Spec.Proxy.Router.Expose.Annotations, service.Annotations) + + if tt.expectLoadBalancer { + assert.Equal(t, cr.Spec.Proxy.Router.Expose.LoadBalancerSourceRanges, service.Spec.LoadBalancerSourceRanges) + } else { + assert.Empty(t, service.Spec.LoadBalancerSourceRanges) + } + + if tt.expectExternalTrafficPolicy { + assert.Equal(t, cr.Spec.Proxy.Router.Expose.ExternalTrafficPolicy, service.Spec.ExternalTrafficPolicy) + } else { + assert.Empty(t, service.Spec.ExternalTrafficPolicy) + } + + assert.Equal(t, cr.Spec.Proxy.Router.Expose.InternalTrafficPolicy, service.Spec.InternalTrafficPolicy) + }) + } +} + func updateObject[T any](obj T, updateFuncs ...func(obj T) T) T { for _, f := range updateFuncs { obj = f(obj)