1050 lines
34 KiB
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...)
|
|
}
|