karmada/pkg/controllers/multiclusterservice/mcs_controller_test.go

1050 lines
34 KiB
Go

/*
Copyright 2024 The Karmada Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package multiclusterservice
import (
"context"
"fmt"
"sort"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
networkingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
"github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/names"
)
func TestHandleMultiClusterServiceDelete(t *testing.T) {
tests := []struct {
name string
mcs *networkingv1alpha1.MultiClusterService
existingService *corev1.Service
existingResourceBinding *workv1alpha2.ResourceBinding
expectedServiceLabels map[string]string
expectedServiceAnnotations map[string]string
expectedRBLabels map[string]string
expectedRBAnnotations map[string]string
}{
{
name: "Delete MCS and clean up Service and ResourceBinding",
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Finalizers: []string{util.MCSControllerFinalizer},
},
},
existingService: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
util.ResourceTemplateClaimedByLabel: util.MultiClusterServiceKind,
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
Annotations: map[string]string{
networkingv1alpha1.MultiClusterServiceNameAnnotation: "test-mcs",
networkingv1alpha1.MultiClusterServiceNamespaceAnnotation: "default",
},
},
},
existingResourceBinding: &workv1alpha2.ResourceBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "service-test-mcs",
Namespace: "default",
Labels: map[string]string{
workv1alpha2.BindingManagedByLabel: util.MultiClusterServiceKind,
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
Annotations: map[string]string{
networkingv1alpha1.MultiClusterServiceNameAnnotation: "test-mcs",
networkingv1alpha1.MultiClusterServiceNamespaceAnnotation: "default",
},
},
},
expectedServiceLabels: nil,
expectedServiceAnnotations: map[string]string{
networkingv1alpha1.MultiClusterServiceNameAnnotation: "test-mcs",
networkingv1alpha1.MultiClusterServiceNamespaceAnnotation: "default",
},
expectedRBLabels: map[string]string{
workv1alpha2.BindingManagedByLabel: util.MultiClusterServiceKind,
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
expectedRBAnnotations: map[string]string{
networkingv1alpha1.MultiClusterServiceNameAnnotation: "test-mcs",
networkingv1alpha1.MultiClusterServiceNamespaceAnnotation: "default",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
controller := newFakeController(tt.mcs, tt.existingService, tt.existingResourceBinding)
_, err := controller.handleMultiClusterServiceDelete(context.Background(), tt.mcs)
assert.NoError(t, err)
updatedService := &corev1.Service{}
err = controller.Client.Get(context.Background(), types.NamespacedName{Namespace: tt.mcs.Namespace, Name: tt.mcs.Name}, updatedService)
assert.NoError(t, err)
updatedRB := &workv1alpha2.ResourceBinding{}
err = controller.Client.Get(context.Background(), types.NamespacedName{Namespace: tt.mcs.Namespace, Name: "service-" + tt.mcs.Name}, updatedRB)
assert.NoError(t, err)
assert.Equal(t, tt.expectedServiceLabels, updatedService.Labels)
assert.Equal(t, tt.expectedServiceAnnotations, updatedService.Annotations)
assert.Equal(t, tt.expectedRBLabels, updatedRB.Labels)
assert.Equal(t, tt.expectedRBAnnotations, updatedRB.Annotations)
updatedMCS := &networkingv1alpha1.MultiClusterService{}
err = controller.Client.Get(context.Background(), types.NamespacedName{Namespace: tt.mcs.Namespace, Name: tt.mcs.Name}, updatedMCS)
assert.NoError(t, err)
assert.NotContains(t, updatedMCS.Finalizers, util.MCSControllerFinalizer)
})
}
}
func TestRetrieveMultiClusterService(t *testing.T) {
tests := []struct {
name string
mcs *networkingv1alpha1.MultiClusterService
existingWorks []*workv1alpha1.Work
providerClusters sets.Set[string]
clusters []*clusterv1alpha1.Cluster
expectedWorks int
}{
{
name: "Remove work for non-provider cluster",
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
existingWorks: []*workv1alpha1.Work{
{
ObjectMeta: metav1.ObjectMeta{
Name: names.GenerateWorkName("MultiClusterService", "test-mcs", "default"),
Namespace: names.GenerateExecutionSpaceName("cluster1"),
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
Spec: workv1alpha1.WorkSpec{
Workload: workv1alpha1.WorkloadTemplate{
Manifests: []workv1alpha1.Manifest{
{
RawExtension: runtime.RawExtension{Raw: []byte(`{"apiVersion":"networking.karmada.io/v1alpha1","kind":"MultiClusterService"}`)},
},
},
},
},
},
},
providerClusters: sets.New("cluster2"),
clusters: []*clusterv1alpha1.Cluster{
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionTrue},
},
},
},
},
expectedWorks: 0,
},
{
name: "Keep work for provider cluster",
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
existingWorks: []*workv1alpha1.Work{
{
ObjectMeta: metav1.ObjectMeta{
Name: names.GenerateWorkName("MultiClusterService", "test-mcs", "default"),
Namespace: names.GenerateExecutionSpaceName("cluster1"),
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
Spec: workv1alpha1.WorkSpec{
Workload: workv1alpha1.WorkloadTemplate{
Manifests: []workv1alpha1.Manifest{
{
RawExtension: runtime.RawExtension{Raw: []byte(`{"apiVersion":"networking.karmada.io/v1alpha1","kind":"MultiClusterService"}`)},
},
},
},
},
},
},
providerClusters: sets.New("cluster1"),
clusters: []*clusterv1alpha1.Cluster{
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionTrue},
},
},
},
},
expectedWorks: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objs := []runtime.Object{tt.mcs}
objs = append(objs, toRuntimeObjects(tt.existingWorks)...)
objs = append(objs, toRuntimeObjects(tt.clusters)...)
controller := newFakeController(objs...)
err := controller.retrieveMultiClusterService(context.Background(), tt.mcs, tt.providerClusters)
assert.NoError(t, err)
workList := &workv1alpha1.WorkList{}
err = controller.Client.List(context.Background(), workList)
assert.NoError(t, err)
assert.Equal(t, tt.expectedWorks, len(workList.Items))
})
}
}
func TestPropagateMultiClusterService(t *testing.T) {
tests := []struct {
name string
mcs *networkingv1alpha1.MultiClusterService
providerClusters sets.Set[string]
clusters []*clusterv1alpha1.Cluster
expectedWorks int
}{
{
name: "Propagate to one ready cluster",
mcs: &networkingv1alpha1.MultiClusterService{
TypeMeta: metav1.TypeMeta{
Kind: "MultiClusterService",
APIVersion: networkingv1alpha1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
providerClusters: sets.New("cluster1"),
clusters: []*clusterv1alpha1.Cluster{
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionTrue},
},
APIEnablements: []clusterv1alpha1.APIEnablement{
{
GroupVersion: "discovery.k8s.io/v1",
Resources: []clusterv1alpha1.APIResource{
{Kind: "EndpointSlice"},
},
},
},
},
},
},
expectedWorks: 1,
},
{
name: "No propagation to unready cluster",
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
providerClusters: sets.New("cluster1"),
clusters: []*clusterv1alpha1.Cluster{
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionFalse},
},
},
},
},
expectedWorks: 0,
},
{
name: "No propagation to cluster without EndpointSlice support",
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
providerClusters: sets.New("cluster1"),
clusters: []*clusterv1alpha1.Cluster{
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionTrue},
},
APIEnablements: []clusterv1alpha1.APIEnablement{
{
GroupVersion: "v1",
Resources: []clusterv1alpha1.APIResource{
{Kind: "Pod"},
},
},
},
},
},
},
expectedWorks: 0,
},
{
name: "Propagate to multiple ready clusters",
mcs: &networkingv1alpha1.MultiClusterService{
TypeMeta: metav1.TypeMeta{
Kind: "MultiClusterService",
APIVersion: networkingv1alpha1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
providerClusters: sets.New("cluster1", "cluster2"),
clusters: []*clusterv1alpha1.Cluster{
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionTrue},
},
APIEnablements: []clusterv1alpha1.APIEnablement{
{
GroupVersion: "discovery.k8s.io/v1",
Resources: []clusterv1alpha1.APIResource{
{Kind: "EndpointSlice"},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster2"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionTrue},
},
APIEnablements: []clusterv1alpha1.APIEnablement{
{
GroupVersion: "discovery.k8s.io/v1",
Resources: []clusterv1alpha1.APIResource{
{Kind: "EndpointSlice"},
},
},
},
},
},
},
expectedWorks: 2,
},
{
name: "Mixed cluster readiness and API support",
mcs: &networkingv1alpha1.MultiClusterService{
TypeMeta: metav1.TypeMeta{
Kind: "MultiClusterService",
APIVersion: networkingv1alpha1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
providerClusters: sets.New("cluster1", "cluster2", "cluster3"),
clusters: []*clusterv1alpha1.Cluster{
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster1"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionTrue},
},
APIEnablements: []clusterv1alpha1.APIEnablement{
{
GroupVersion: "discovery.k8s.io/v1",
Resources: []clusterv1alpha1.APIResource{
{Kind: "EndpointSlice"},
},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster2"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionFalse},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{Name: "cluster3"},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{Type: clusterv1alpha1.ClusterConditionReady, Status: metav1.ConditionTrue},
},
APIEnablements: []clusterv1alpha1.APIEnablement{
{
GroupVersion: "v1",
Resources: []clusterv1alpha1.APIResource{
{Kind: "Pod"},
},
},
},
},
},
},
expectedWorks: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objs := []runtime.Object{tt.mcs}
objs = append(objs, toRuntimeObjects(tt.clusters)...)
controller := newFakeController(objs...)
err := controller.propagateMultiClusterService(context.Background(), tt.mcs, tt.providerClusters)
assert.NoError(t, err)
workList := &workv1alpha1.WorkList{}
err = controller.Client.List(context.Background(), workList)
assert.NoError(t, err)
assert.Equal(t, tt.expectedWorks, len(workList.Items))
if tt.expectedWorks > 0 {
for _, work := range workList.Items {
assert.Equal(t, names.GenerateWorkName(tt.mcs.Kind, tt.mcs.Name, tt.mcs.Namespace), work.Name)
clusterName, err := names.GetClusterName(work.Namespace)
assert.NoError(t, err)
assert.Contains(t, tt.providerClusters, clusterName)
assert.Equal(t, "test-id", work.Labels[networkingv1alpha1.MultiClusterServicePermanentIDLabel])
}
}
})
}
}
func TestBuildResourceBinding(t *testing.T) {
tests := []struct {
name string
svc *corev1.Service
mcs *networkingv1alpha1.MultiClusterService
providerClusters sets.Set[string]
consumerClusters sets.Set[string]
}{
{
name: "Build ResourceBinding with non-overlapping clusters",
svc: &corev1.Service{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Service",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
UID: "test-uid",
ResourceVersion: "1234",
},
},
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
providerClusters: sets.New("cluster1", "cluster2"),
consumerClusters: sets.New("cluster3", "cluster4"),
},
{
name: "Build ResourceBinding with empty consumer clusters",
svc: &corev1.Service{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Service",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
UID: "test-uid",
ResourceVersion: "1234",
},
},
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
providerClusters: sets.New("cluster1", "cluster2"),
consumerClusters: sets.New[string](),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
controller := newFakeController()
rb, err := controller.buildResourceBinding(tt.svc, tt.mcs, tt.providerClusters, tt.consumerClusters)
assert.NoError(t, err)
assert.NotNil(t, rb)
// ObjectMeta Check
assert.Equal(t, names.GenerateBindingName(tt.svc.Kind, tt.svc.Name), rb.Name)
assert.Equal(t, tt.svc.Namespace, rb.Namespace)
// Annotations Check
assert.Equal(t, tt.mcs.Name, rb.Annotations[networkingv1alpha1.MultiClusterServiceNameAnnotation])
assert.Equal(t, tt.mcs.Namespace, rb.Annotations[networkingv1alpha1.MultiClusterServiceNamespaceAnnotation])
// Labels Check
assert.Equal(t, util.MultiClusterServiceKind, rb.Labels[workv1alpha2.BindingManagedByLabel])
assert.Equal(t, "test-id", rb.Labels[networkingv1alpha1.MultiClusterServicePermanentIDLabel])
// OwnerReferences Check
assert.Len(t, rb.OwnerReferences, 1)
assert.Equal(t, tt.svc.APIVersion, rb.OwnerReferences[0].APIVersion)
assert.Equal(t, tt.svc.Kind, rb.OwnerReferences[0].Kind)
assert.Equal(t, tt.svc.Name, rb.OwnerReferences[0].Name)
assert.Equal(t, tt.svc.UID, rb.OwnerReferences[0].UID)
// Finalizers Check
assert.Contains(t, rb.Finalizers, util.BindingControllerFinalizer)
// Spec Check
expectedClusters := tt.providerClusters.Union(tt.consumerClusters).UnsortedList()
actualClusters := rb.Spec.Placement.ClusterAffinity.ClusterNames
// Sort both slices before comparison
sort.Strings(expectedClusters)
sort.Strings(actualClusters)
assert.Equal(t, expectedClusters, actualClusters, "Cluster names should match regardless of order")
// Resource reference Check
assert.Equal(t, tt.svc.APIVersion, rb.Spec.Resource.APIVersion)
assert.Equal(t, tt.svc.Kind, rb.Spec.Resource.Kind)
assert.Equal(t, tt.svc.Namespace, rb.Spec.Resource.Namespace)
assert.Equal(t, tt.svc.Name, rb.Spec.Resource.Name)
assert.Equal(t, tt.svc.UID, rb.Spec.Resource.UID)
assert.Equal(t, tt.svc.ResourceVersion, rb.Spec.Resource.ResourceVersion)
})
}
}
func TestClaimMultiClusterServiceForService(t *testing.T) {
tests := []struct {
name string
svc *corev1.Service
mcs *networkingv1alpha1.MultiClusterService
updateError bool
expectedError bool
}{
{
name: "Claim service for MCS - basic case",
svc: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
},
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
},
{
name: "Claim service for MCS - with existing labels and annotations",
svc: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
Labels: map[string]string{
"existing-label": "value",
policyv1alpha1.PropagationPolicyPermanentIDLabel: "should-be-removed",
},
Annotations: map[string]string{
"existing-annotation": "value",
policyv1alpha1.PropagationPolicyNameAnnotation: "should-be-removed",
},
},
},
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
},
{
name: "Claim service for MCS - update error",
svc: &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
},
},
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mcs",
Namespace: "default",
Labels: map[string]string{
networkingv1alpha1.MultiClusterServicePermanentIDLabel: "test-id",
},
},
},
updateError: true,
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
controller := newFakeController(tt.svc)
if tt.updateError {
controller.Client = newFakeClientWithUpdateError(tt.svc, true)
}
err := controller.claimMultiClusterServiceForService(context.Background(), tt.svc, tt.mcs)
if tt.expectedError {
assert.Error(t, err)
return
}
assert.NoError(t, err)
updatedSvc := &corev1.Service{}
err = controller.Client.Get(context.Background(), types.NamespacedName{Namespace: tt.svc.Namespace, Name: tt.svc.Name}, updatedSvc)
assert.NoError(t, err)
// Added labels and annotations check
assert.Equal(t, util.MultiClusterServiceKind, updatedSvc.Labels[util.ResourceTemplateClaimedByLabel])
assert.Equal(t, "test-id", updatedSvc.Labels[networkingv1alpha1.MultiClusterServicePermanentIDLabel])
assert.Equal(t, tt.mcs.Name, updatedSvc.Annotations[networkingv1alpha1.MultiClusterServiceNameAnnotation])
assert.Equal(t, tt.mcs.Namespace, updatedSvc.Annotations[networkingv1alpha1.MultiClusterServiceNamespaceAnnotation])
// Removed labels and annotations check
assert.NotContains(t, updatedSvc.Labels, policyv1alpha1.PropagationPolicyPermanentIDLabel)
assert.NotContains(t, updatedSvc.Labels, policyv1alpha1.ClusterPropagationPolicyPermanentIDLabel)
assert.NotContains(t, updatedSvc.Annotations, policyv1alpha1.PropagationPolicyNameAnnotation)
assert.NotContains(t, updatedSvc.Annotations, policyv1alpha1.PropagationPolicyNamespaceAnnotation)
assert.NotContains(t, updatedSvc.Annotations, policyv1alpha1.ClusterPropagationPolicyAnnotation)
// Check existing labels and annotations are preserved
if tt.svc.Labels != nil {
assert.Contains(t, updatedSvc.Labels, "existing-label")
}
if tt.svc.Annotations != nil {
assert.Contains(t, updatedSvc.Annotations, "existing-annotation")
}
})
}
}
func TestServiceHasCrossClusterMultiClusterService(t *testing.T) {
tests := []struct {
name string
svc *corev1.Service
mcs *networkingv1alpha1.MultiClusterService
expected bool
}{
{
name: "Service has cross-cluster MCS",
svc: &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: "default"}},
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: "default"},
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("CrossCluster")},
},
},
expected: true,
},
{
name: "Service has no cross-cluster MCS",
svc: &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: "default"}},
mcs: &networkingv1alpha1.MultiClusterService{
ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: "default"},
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("LocalCluster")},
},
},
expected: false,
},
{
name: "Service has no MCS",
svc: &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: "default"}},
mcs: nil,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objs := []runtime.Object{tt.svc}
if tt.mcs != nil {
objs = append(objs, tt.mcs)
}
controller := newFakeController(objs...)
result := controller.serviceHasCrossClusterMultiClusterService(tt.svc)
assert.Equal(t, tt.expected, result)
})
}
}
func TestClusterMapFunc(t *testing.T) {
tests := []struct {
name string
object client.Object
mcsList []*networkingv1alpha1.MultiClusterService
expectedRequests []reconcile.Request
}{
{
name: "Cluster matches MCS provider",
object: &clusterv1alpha1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "cluster1"}},
mcsList: []*networkingv1alpha1.MultiClusterService{
{
ObjectMeta: metav1.ObjectMeta{Name: "mcs1", Namespace: "default"},
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("CrossCluster")},
ProviderClusters: []networkingv1alpha1.ClusterSelector{{Name: "cluster1"}},
},
},
{
ObjectMeta: metav1.ObjectMeta{Name: "mcs2", Namespace: "default"},
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("CrossCluster")},
ProviderClusters: []networkingv1alpha1.ClusterSelector{{Name: "cluster2"}},
},
},
},
expectedRequests: []reconcile.Request{
{NamespacedName: types.NamespacedName{Namespace: "default", Name: "mcs1"}},
{NamespacedName: types.NamespacedName{Namespace: "default", Name: "mcs2"}},
},
},
{
name: "Cluster doesn't match any MCS",
object: &clusterv1alpha1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "cluster3"}},
mcsList: []*networkingv1alpha1.MultiClusterService{
{
ObjectMeta: metav1.ObjectMeta{Name: "mcs1", Namespace: "default"},
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("CrossCluster")},
ProviderClusters: []networkingv1alpha1.ClusterSelector{{Name: "cluster1"}},
},
},
},
expectedRequests: []reconcile.Request{
{NamespacedName: types.NamespacedName{Namespace: "default", Name: "mcs1"}},
},
},
{
name: "Empty MCS list",
object: &clusterv1alpha1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "cluster1"}},
mcsList: []*networkingv1alpha1.MultiClusterService{},
expectedRequests: []reconcile.Request{},
},
{
name: "Non-Cluster object",
object: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1"}},
mcsList: []*networkingv1alpha1.MultiClusterService{},
expectedRequests: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objs := []runtime.Object{tt.object}
objs = append(objs, toRuntimeObjects(tt.mcsList)...)
controller := newFakeController(objs...)
mapFunc := controller.clusterMapFunc()
requests := mapFunc(context.Background(), tt.object)
assert.Equal(t, len(tt.expectedRequests), len(requests), "Number of requests does not match expected")
assert.ElementsMatch(t, tt.expectedRequests, requests, "Requests do not match expected")
if _, ok := tt.object.(*clusterv1alpha1.Cluster); ok {
for _, request := range requests {
found := false
for _, mcs := range tt.mcsList {
if mcs.Name == request.Name && mcs.Namespace == request.Namespace {
found = true
break
}
}
assert.True(t, found, "Generated request does not correspond to any MCS in the list")
}
}
})
}
}
func TestNeedSyncMultiClusterService(t *testing.T) {
tests := []struct {
name string
mcs *networkingv1alpha1.MultiClusterService
clusterName string
expectedNeed bool
expectedErr bool
}{
{
name: "MCS with CrossCluster type and matching provider cluster",
mcs: &networkingv1alpha1.MultiClusterService{
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("CrossCluster")},
ProviderClusters: []networkingv1alpha1.ClusterSelector{{Name: "cluster1"}},
ConsumerClusters: []networkingv1alpha1.ClusterSelector{{Name: "cluster2"}},
},
},
clusterName: "cluster1",
expectedNeed: true,
expectedErr: false,
},
{
name: "MCS with CrossCluster type and matching consumer cluster",
mcs: &networkingv1alpha1.MultiClusterService{
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("CrossCluster")},
ProviderClusters: []networkingv1alpha1.ClusterSelector{{Name: "cluster1"}},
ConsumerClusters: []networkingv1alpha1.ClusterSelector{{Name: "cluster2"}},
},
},
clusterName: "cluster2",
expectedNeed: true,
expectedErr: false,
},
{
name: "MCS without CrossCluster type",
mcs: &networkingv1alpha1.MultiClusterService{
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("LocalCluster")},
},
},
clusterName: "cluster1",
expectedNeed: false,
expectedErr: false,
},
{
name: "MCS with empty ProviderClusters and ConsumerClusters",
mcs: &networkingv1alpha1.MultiClusterService{
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("CrossCluster")},
},
},
clusterName: "cluster1",
expectedNeed: true,
expectedErr: false,
},
{
name: "Cluster doesn't match ProviderClusters or ConsumerClusters",
mcs: &networkingv1alpha1.MultiClusterService{
Spec: networkingv1alpha1.MultiClusterServiceSpec{
Types: []networkingv1alpha1.ExposureType{networkingv1alpha1.ExposureType("CrossCluster")},
ProviderClusters: []networkingv1alpha1.ClusterSelector{{Name: "cluster1"}},
ConsumerClusters: []networkingv1alpha1.ClusterSelector{{Name: "cluster2"}},
},
},
clusterName: "cluster3",
expectedNeed: false,
expectedErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
clusters := createClustersFromMCS(tt.mcs)
objs := append([]runtime.Object{tt.mcs}, toRuntimeObjects(clusters)...)
controller := newFakeController(objs...)
need, err := controller.needSyncMultiClusterService(tt.mcs, tt.clusterName)
assert.Equal(t, tt.expectedNeed, need, "Expected need %v, but got %v", tt.expectedNeed, need)
if tt.expectedErr {
assert.Error(t, err, "Expected an error, but got none")
} else {
assert.NoError(t, err, "Expected no error, but got %v", err)
}
})
}
}
// Helper Functions
// Helper function to create fake Cluster objects based on the MCS spec
func createClustersFromMCS(mcs *networkingv1alpha1.MultiClusterService) []*clusterv1alpha1.Cluster {
var clusters []*clusterv1alpha1.Cluster
for _, pc := range mcs.Spec.ProviderClusters {
clusters = append(clusters, &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: pc.Name},
})
}
for _, cc := range mcs.Spec.ConsumerClusters {
clusters = append(clusters, &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: cc.Name},
})
}
return clusters
}
// Helper function to set up a scheme with all necessary types
func setupScheme() *runtime.Scheme {
s := runtime.NewScheme()
_ = corev1.AddToScheme(s)
_ = networkingv1alpha1.Install(s)
_ = workv1alpha1.Install(s)
_ = workv1alpha2.Install(s)
_ = clusterv1alpha1.Install(s)
_ = scheme.AddToScheme(s)
return s
}
// Helper function to create a new MCSController with a fake client
func newFakeController(objs ...runtime.Object) *MCSController {
s := setupScheme()
fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
return &MCSController{
Client: fakeClient,
EventRecorder: record.NewFakeRecorder(100),
}
}
// Helper function to convert a slice of objects to a slice of runtime.Object
func toRuntimeObjects(objs interface{}) []runtime.Object {
var result []runtime.Object
switch v := objs.(type) {
case []*workv1alpha1.Work:
for _, obj := range v {
result = append(result, obj)
}
case []*clusterv1alpha1.Cluster:
for _, obj := range v {
result = append(result, obj)
}
case []*networkingv1alpha1.MultiClusterService:
for _, obj := range v {
result = append(result, obj)
}
}
return result
}
// Helper function to create a fake client that can simulate update errors
func newFakeClientWithUpdateError(svc *corev1.Service, shouldError bool) client.Client {
s := runtime.NewScheme()
_ = corev1.AddToScheme(s)
_ = networkingv1alpha1.Install(s)
fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(svc).Build()
if shouldError {
return &errorInjectingClient{
Client: fakeClient,
shouldError: shouldError,
}
}
return fakeClient
}
type errorInjectingClient struct {
client.Client
shouldError bool
}
func (c *errorInjectingClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
if c.shouldError {
return fmt.Errorf("simulated update error")
}
return c.Client.Update(ctx, obj, opts...)
}