diff --git a/pkg/controllers/namespace/namespace_sync_controller_test.go b/pkg/controllers/namespace/namespace_sync_controller_test.go new file mode 100644 index 000000000..843a5852e --- /dev/null +++ b/pkg/controllers/namespace/namespace_sync_controller_test.go @@ -0,0 +1,436 @@ +/* +Copyright 2023 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 namespace + +import ( + "context" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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/tools/record" + "k8s.io/klog/v2" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" + policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" + workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" + "github.com/karmada-io/karmada/pkg/util/names" + "github.com/karmada-io/karmada/pkg/util/overridemanager" +) + +func TestController_namespaceShouldBeSynced(t *testing.T) { + tests := []struct { + name string + namespace string + skipped []*regexp.Regexp + want bool + }{ + { + name: "Reserved namespace", + namespace: "kube-system", + want: true, + }, + { + name: "Default namespace", + namespace: "default", + want: false, + }, + { + name: "Regular namespace", + namespace: "test-namespace", + want: true, + }, + { + name: "Skipped namespace", + namespace: "skip-me", + skipped: []*regexp.Regexp{regexp.MustCompile("skip-.*")}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Controller{ + SkippedPropagatingNamespaces: tt.skipped, + } + got := c.namespaceShouldBeSynced(tt.namespace) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestController_Reconcile(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = clusterv1alpha1.Install(scheme) + _ = policyv1alpha1.Install(scheme) + _ = workv1alpha1.Install(scheme) + + tests := []struct { + name string + namespace *corev1.Namespace + clusters []clusterv1alpha1.Cluster + expectedResult controllerruntime.Result + expectedError bool + }{ + { + name: "Namespace should be synced", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + }, + }, + clusters: []clusterv1alpha1.Cluster{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + }, + }, + }, + expectedResult: controllerruntime.Result{}, + expectedError: false, + }, + { + name: "Namespace should not be synced", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-system", + }, + }, + expectedResult: controllerruntime.Result{}, + expectedError: false, + }, + { + name: "Namespace with skip auto propagation label", + namespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "skip-namespace", + Labels: map[string]string{ + policyv1alpha1.NamespaceSkipAutoPropagationLabel: "true", + }, + }, + }, + expectedResult: controllerruntime.Result{}, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.namespace).WithLists(&clusterv1alpha1.ClusterList{Items: tt.clusters}).Build() + fakeRecorder := record.NewFakeRecorder(100) + + c := &Controller{ + Client: fakeClient, + EventRecorder: fakeRecorder, + OverrideManager: overridemanager.New(fakeClient, fakeRecorder), + } + + req := controllerruntime.Request{ + NamespacedName: types.NamespacedName{ + Name: tt.namespace.Name, + }, + } + + result, err := c.Reconcile(context.Background(), req) + assert.Equal(t, tt.expectedResult, result) + assert.Equal(t, tt.expectedError, err != nil) + + if tt.name == "Namespace should be synced" { + work := &workv1alpha1.Work{} + err = fakeClient.Get(context.Background(), types.NamespacedName{ + Namespace: names.GenerateExecutionSpaceName(tt.clusters[0].Name), + Name: names.GenerateWorkName(tt.namespace.Kind, tt.namespace.Name, tt.namespace.Namespace), + }, work) + assert.NoError(t, err) + assert.NotNil(t, work) + } + }) + } +} + +func TestController_buildWorks(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = clusterv1alpha1.Install(scheme) + _ = workv1alpha1.Install(scheme) + _ = policyv1alpha1.Install(scheme) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + }, + } + + clusters := []clusterv1alpha1.Cluster{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster2", + }, + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(namespace, &clusters[0], &clusters[1]).Build() + fakeRecorder := record.NewFakeRecorder(100) + + c := &Controller{ + Client: fakeClient, + EventRecorder: fakeRecorder, + OverrideManager: overridemanager.New(fakeClient, fakeRecorder), + } + + err := c.buildWorks(context.Background(), namespace, clusters) + assert.NoError(t, err) + + for _, cluster := range clusters { + work := &workv1alpha1.Work{} + err = fakeClient.Get(context.Background(), types.NamespacedName{ + Namespace: names.GenerateExecutionSpaceName(cluster.Name), + Name: names.GenerateWorkName(namespace.Kind, namespace.Name, namespace.Namespace), + }, work) + assert.NoError(t, err) + assert.NotNil(t, work) + assert.Equal(t, namespace.Name, work.OwnerReferences[0].Name) + } +} + +func TestController_buildWorksWithOverridePolicy(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = clusterv1alpha1.Install(scheme) + _ = workv1alpha1.Install(scheme) + _ = policyv1alpha1.Install(scheme) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + }, + } + + clusters := []clusterv1alpha1.Cluster{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster1", + }, + }, + } + + overridePolicy := &policyv1alpha1.ClusterOverridePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-override", + }, + Spec: policyv1alpha1.OverrideSpec{ + ResourceSelectors: []policyv1alpha1.ResourceSelector{ + { + APIVersion: "v1", + Kind: "Namespace", + Name: "test-namespace", + }, + }, + OverrideRules: []policyv1alpha1.RuleWithCluster{ + { + TargetCluster: &policyv1alpha1.ClusterAffinity{ + ClusterNames: []string{"cluster1"}, + }, + Overriders: policyv1alpha1.Overriders{ + Plaintext: []policyv1alpha1.PlaintextOverrider{ + { + Path: "/metadata/labels/overridden", + Operator: "add", + Value: apiextensionsv1.JSON{ + Raw: []byte(`"true"`), + }, + }, + }, + }, + }, + }, + }, + } + + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(namespace, &clusters[0], overridePolicy).Build() + fakeRecorder := record.NewFakeRecorder(100) + + c := &Controller{ + Client: fakeClient, + EventRecorder: fakeRecorder, + OverrideManager: overridemanager.New(fakeClient, fakeRecorder), + } + + err := c.buildWorks(context.Background(), namespace, clusters) + assert.NoError(t, err) + + work := &workv1alpha1.Work{} + err = fakeClient.Get(context.Background(), types.NamespacedName{ + Namespace: names.GenerateExecutionSpaceName(clusters[0].Name), + Name: names.GenerateWorkName(namespace.Kind, namespace.Name, namespace.Namespace), + }, work) + assert.NoError(t, err) + assert.NotNil(t, work) + + t.Logf("Work found: %+v", work) + t.Logf("Work annotations: %v", work.Annotations) + t.Logf("Work spec: %+v", work.Spec) + + assert.NotNil(t, work) +} + +func TestController_SetupWithManager(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = clusterv1alpha1.Install(scheme) + _ = workv1alpha1.Install(scheme) + + mgr, err := controllerruntime.NewManager(controllerruntime.GetConfigOrDie(), controllerruntime.Options{Scheme: scheme}) + assert.NoError(t, err) + + c := &Controller{ + Client: mgr.GetClient(), + EventRecorder: mgr.GetEventRecorderFor("test-controller"), + OverrideManager: overridemanager.New(mgr.GetClient(), mgr.GetEventRecorderFor("test-controller")), + } + + err = c.SetupWithManager(mgr) + assert.NoError(t, err) +} + +func TestClusterNamespaceFn(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + namespace1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "namespace1"}} + namespace2 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "namespace2"}} + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(namespace1, namespace2). + Build() + + c := &Controller{Client: fakeClient} + + clusterNamespaceFn := handler.MapFunc( + func(ctx context.Context, _ client.Object) []reconcile.Request { + var requests []reconcile.Request + namespaceList := &corev1.NamespaceList{} + if err := c.Client.List(ctx, namespaceList); err != nil { + klog.Errorf("Failed to list namespace, error: %v", err) + return nil + } + + for _, namespace := range namespaceList.Items { + requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ + Name: namespace.Name, + }}) + } + return requests + }) + + requests := clusterNamespaceFn(context.Background(), nil) + assert.Len(t, requests, 2) + assert.Contains(t, requests, reconcile.Request{NamespacedName: types.NamespacedName{Name: "namespace1"}}) + assert.Contains(t, requests, reconcile.Request{NamespacedName: types.NamespacedName{Name: "namespace2"}}) +} +func TestClusterOverridePolicyNamespaceFn(t *testing.T) { + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = policyv1alpha1.Install(scheme) + + cop := &policyv1alpha1.ClusterOverridePolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "test-policy"}, + Spec: policyv1alpha1.OverrideSpec{ + ResourceSelectors: []policyv1alpha1.ResourceSelector{ + {APIVersion: "v1", Kind: "Namespace", Name: "test-namespace"}, + }, + }, + } + + namespace1 := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace"}} + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(namespace1, cop). + Build() + + c := &Controller{Client: fakeClient} + + clusterOverridePolicyNamespaceFn := handler.MapFunc( + func(ctx context.Context, obj client.Object) []reconcile.Request { + var requests []reconcile.Request + cop, ok := obj.(*policyv1alpha1.ClusterOverridePolicy) + if !ok { + return requests + } + + selectedNamespaces := sets.NewString() + containsAllNamespace := false + for _, rs := range cop.Spec.ResourceSelectors { + if rs.APIVersion != "v1" || rs.Kind != "Namespace" { + continue + } + + if rs.Name == "" { + containsAllNamespace = true + break + } + + selectedNamespaces.Insert(rs.Name) + } + + if containsAllNamespace { + namespaceList := &corev1.NamespaceList{} + if err := c.Client.List(ctx, namespaceList); err != nil { + klog.Errorf("Failed to list namespace, error: %v", err) + return nil + } + + for _, namespace := range namespaceList.Items { + requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ + Name: namespace.Name, + }}) + } + + return requests + } + + for _, ns := range selectedNamespaces.UnsortedList() { + requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ + Name: ns, + }}) + } + + return requests + }) + + requests := clusterOverridePolicyNamespaceFn(context.Background(), cop) + assert.Len(t, requests, 1) + assert.Contains(t, requests, reconcile.Request{NamespacedName: types.NamespacedName{Name: "test-namespace"}}) +}