karmada/pkg/detector/detector_test.go

1183 lines
32 KiB
Go

/*
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 detector
import (
"context"
"fmt"
"regexp"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/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/fedinformer/keys"
)
func BenchmarkEventFilterNoSkipNameSpaces(b *testing.B) {
dt := &ResourceDetector{}
dt.SkippedPropagatingNamespaces = nil
for i := 0; i < b.N; i++ {
dt.EventFilter(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "demo-deployment",
"namespace": "benchmark",
},
"spec": map[string]interface{}{
"replicas": 2,
},
},
})
}
}
func BenchmarkEventFilterNoMatchSkipNameSpaces(b *testing.B) {
dt := &ResourceDetector{}
dt.SkippedPropagatingNamespaces = append(dt.SkippedPropagatingNamespaces, regexp.MustCompile("^benchmark-.*$"))
for i := 0; i < b.N; i++ {
dt.EventFilter(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "demo-deployment",
"namespace": "benchmark",
},
"spec": map[string]interface{}{
"replicas": 2,
},
},
})
}
}
func BenchmarkEventFilterNoWildcards(b *testing.B) {
dt := &ResourceDetector{}
dt.SkippedPropagatingNamespaces = append(dt.SkippedPropagatingNamespaces, regexp.MustCompile("^benchmark$"))
for i := 0; i < b.N; i++ {
dt.EventFilter(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "demo-deployment",
"namespace": "benchmark-1",
},
"spec": map[string]interface{}{
"replicas": 2,
},
},
})
}
}
func BenchmarkEventFilterPrefixMatchSkipNameSpaces(b *testing.B) {
dt := &ResourceDetector{}
dt.SkippedPropagatingNamespaces = append(dt.SkippedPropagatingNamespaces, regexp.MustCompile("^benchmark-.*$"))
for i := 0; i < b.N; i++ {
dt.EventFilter(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "demo-deployment",
"namespace": "benchmark-1",
},
"spec": map[string]interface{}{
"replicas": 2,
},
},
})
}
}
func BenchmarkEventFilterSuffixMatchSkipNameSpaces(b *testing.B) {
dt := &ResourceDetector{}
dt.SkippedPropagatingNamespaces = append(dt.SkippedPropagatingNamespaces, regexp.MustCompile("^.*-benchmark$"))
for i := 0; i < b.N; i++ {
dt.EventFilter(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "demo-deployment",
"namespace": "example-benchmark",
},
"spec": map[string]interface{}{
"replicas": 2,
},
},
})
}
}
func BenchmarkEventFilterMultiSkipNameSpaces(b *testing.B) {
dt := &ResourceDetector{}
dt.SkippedPropagatingNamespaces = append(dt.SkippedPropagatingNamespaces, regexp.MustCompile("^.*-benchmark$"), regexp.MustCompile("^benchmark-.*$"))
for i := 0; i < b.N; i++ {
dt.EventFilter(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "demo-deployment",
"namespace": "benchmark-1",
},
"spec": map[string]interface{}{
"replicas": 2,
},
},
})
}
}
func BenchmarkEventFilterExtensionApiserverAuthentication(b *testing.B) {
dt := &ResourceDetector{}
dt.SkippedPropagatingNamespaces = append(dt.SkippedPropagatingNamespaces, regexp.MustCompile("^kube-.*$"))
for i := 0; i < b.N; i++ {
dt.EventFilter(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "extension-apiserver-authentication",
"namespace": "kube-system",
},
},
})
}
}
func TestGVRDisabled(t *testing.T) {
tests := []struct {
name string
gvr schema.GroupVersionResource
config *util.SkippedResourceConfig
expected bool
}{
{
name: "GVR not disabled",
gvr: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
config: &util.SkippedResourceConfig{},
expected: false,
},
{
name: "GVR disabled by group",
gvr: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
config: &util.SkippedResourceConfig{Groups: map[string]struct{}{"apps": {}}},
expected: true,
},
{
name: "GVR disabled by group version",
gvr: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
config: &util.SkippedResourceConfig{GroupVersions: map[schema.GroupVersion]struct{}{{Group: "apps", Version: "v1"}: {}}},
expected: true,
},
{
name: "SkippedResourceConfig is nil",
gvr: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
config: nil,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &ResourceDetector{
SkippedResourceConfig: tt.config,
RESTMapper: &mockRESTMapper{},
}
result := d.gvrDisabled(tt.gvr)
assert.Equal(t, tt.expected, result)
})
}
}
func TestNeedLeaderElection(t *testing.T) {
tests := []struct {
name string
want bool
}{
{
name: "NeedLeaderElection always returns true",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &ResourceDetector{}
got := d.NeedLeaderElection()
assert.Equal(t, tt.want, got, "NeedLeaderElection() = %v, want %v", got, tt.want)
})
}
}
func TestEventFilter(t *testing.T) {
tests := []struct {
name string
obj *unstructured.Unstructured
skippedPropagatingNamespaces []*regexp.Regexp
expected bool
}{
{
name: "object in karmada-system namespace",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"namespace": "karmada-system",
"name": "test-obj",
},
},
},
expected: false,
},
{
name: "object in karmada-cluster namespace",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"namespace": "karmada-cluster",
"name": "test-obj",
},
},
},
expected: false,
},
{
name: "object in karmada-es-* namespace",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"namespace": "karmada-es-test",
"name": "test-obj",
},
},
},
expected: false,
},
{
name: "object in skipped namespace",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"namespace": "kube-system",
"name": "test-obj",
},
},
},
skippedPropagatingNamespaces: []*regexp.Regexp{regexp.MustCompile("kube-.*")},
expected: false,
},
{
name: "object in non-skipped namespace",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"namespace": "default",
"name": "test-obj",
},
},
},
expected: true,
},
{
name: "extension-apiserver-authentication configmap in kube-system",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"namespace": "kube-system",
"name": "extension-apiserver-authentication",
},
},
},
expected: false,
},
{
name: "cluster-scoped resource",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Node",
"metadata": map[string]interface{}{
"name": "test-node",
},
},
},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := &ResourceDetector{
SkippedPropagatingNamespaces: tt.skippedPropagatingNamespaces,
}
result := d.EventFilter(tt.obj)
assert.Equal(t, tt.expected, result, "For test case: %s", tt.name)
})
}
}
func TestOnAdd(t *testing.T) {
tests := []struct {
name string
obj interface{}
expectedEnqueue bool
}{
{
name: "valid unstructured object",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
},
},
},
expectedEnqueue: true,
},
{
name: "invalid unstructured object",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
expectedEnqueue: true, // The function doesn't check for validity, so it will still enqueue
},
{
name: "non-runtime object",
obj: "not a runtime.Object",
expectedEnqueue: false,
},
{
name: "core v1 object",
obj: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "default",
},
},
expectedEnqueue: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockProcessor := &mockAsyncWorker{}
d := &ResourceDetector{
Processor: mockProcessor,
}
d.OnAdd(tt.obj)
if tt.expectedEnqueue {
assert.Equal(t, 1, mockProcessor.enqueueCount, "Object should be enqueued")
assert.IsType(t, ResourceItem{}, mockProcessor.lastEnqueued, "Enqueued item should be of type ResourceItem")
enqueued := mockProcessor.lastEnqueued.(ResourceItem)
assert.Equal(t, tt.obj, enqueued.Obj, "Enqueued object should match the input object")
} else {
assert.Equal(t, 0, mockProcessor.enqueueCount, "Object should not be enqueued")
}
})
}
}
func TestOnUpdate(t *testing.T) {
tests := []struct {
name string
oldObj interface{}
newObj interface{}
expectedEnqueue bool
expectedChangeByKarmada bool
expectToUnstructuredError bool
}{
{
name: "valid update with changes",
oldObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
},
"spec": map[string]interface{}{
"replicas": int64(1),
},
},
},
newObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
},
"spec": map[string]interface{}{
"replicas": int64(2),
},
},
},
expectedEnqueue: true,
expectedChangeByKarmada: false,
},
{
name: "update without changes",
oldObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
},
"spec": map[string]interface{}{
"replicas": int64(1),
},
},
},
newObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
},
"spec": map[string]interface{}{
"replicas": int64(1),
},
},
},
expectedEnqueue: false,
},
{
name: "invalid object",
oldObj: "not a runtime.Object",
newObj: "not a runtime.Object",
expectedEnqueue: false,
},
{
name: "change by Karmada",
oldObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
},
},
},
newObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
"annotations": map[string]interface{}{
util.PolicyPlacementAnnotation: "test",
},
},
},
},
expectedEnqueue: true,
expectedChangeByKarmada: true,
},
{
name: "core v1 object",
oldObj: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "default",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "container1"}},
},
},
newObj: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "default",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{Name: "container1"}, {Name: "container2"}},
},
},
expectedEnqueue: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockProcessor := &mockAsyncWorker{}
d := &ResourceDetector{
Processor: mockProcessor,
}
d.OnUpdate(tt.oldObj, tt.newObj)
if tt.expectedEnqueue {
assert.Equal(t, 1, mockProcessor.enqueueCount, "Object should be enqueued")
assert.IsType(t, ResourceItem{}, mockProcessor.lastEnqueued, "Enqueued item should be of type ResourceItem")
enqueued := mockProcessor.lastEnqueued.(ResourceItem)
assert.Equal(t, tt.newObj, enqueued.Obj, "Enqueued object should match the new object")
assert.Equal(t, tt.expectedChangeByKarmada, enqueued.ResourceChangeByKarmada, "ResourceChangeByKarmada flag should match expected value")
} else {
assert.Equal(t, 0, mockProcessor.enqueueCount, "Object should not be enqueued")
}
})
}
}
func TestOnDelete(t *testing.T) {
tests := []struct {
name string
obj runtime.Object
expectedEnqueue bool
}{
{
name: "valid object",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
},
},
},
expectedEnqueue: true,
},
{
name: "invalid object",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{},
},
expectedEnqueue: true, // The function doesn't check for validity, so it will still enqueue
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockProcessor := &mockAsyncWorker{}
d := &ResourceDetector{
Processor: mockProcessor,
}
d.OnDelete(tt.obj)
if tt.expectedEnqueue {
assert.Equal(t, 1, mockProcessor.enqueueCount, "Object should be enqueued")
} else {
assert.Equal(t, 0, mockProcessor.enqueueCount, "Object should not be enqueued")
}
})
}
}
func TestLookForMatchedPolicy(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
policies []*policyv1alpha1.PropagationPolicy
expectedPolicy *policyv1alpha1.PropagationPolicy
}{
{
name: "matching policy found",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
},
},
},
policies: []*policyv1alpha1.PropagationPolicy{
{
ObjectMeta: metav1.ObjectMeta{
Name: "policy-1",
Namespace: "default",
},
Spec: policyv1alpha1.PropagationSpec{
ResourceSelectors: []policyv1alpha1.ResourceSelector{
{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
},
},
},
expectedPolicy: &policyv1alpha1.PropagationPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "policy-1",
Namespace: "default",
},
Spec: policyv1alpha1.PropagationSpec{
ResourceSelectors: []policyv1alpha1.ResourceSelector{
{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scheme := setupTestScheme()
fakeClient := dynamicfake.NewSimpleDynamicClient(scheme)
d := &ResourceDetector{
DynamicClient: fakeClient,
propagationPolicyLister: &mockPropagationPolicyLister{
policies: tt.policies,
},
}
objectKey := keys.ClusterWideKey{
Name: tt.object.GetName(),
Namespace: tt.object.GetNamespace(),
Kind: tt.object.GetKind(),
}
policy, err := d.LookForMatchedPolicy(tt.object, objectKey)
if err != nil {
t.Errorf("LookForMatchedPolicy returned an error: %v", err)
}
fmt.Printf("Returned policy: %+v\n", policy)
if tt.expectedPolicy == nil {
assert.Nil(t, policy)
} else {
assert.NotNil(t, policy)
if policy != nil {
assert.Equal(t, tt.expectedPolicy.Name, policy.Name)
assert.Equal(t, tt.expectedPolicy.Namespace, policy.Namespace)
}
}
})
}
}
func TestLookForMatchedClusterPolicy(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
policies []*policyv1alpha1.ClusterPropagationPolicy
expectedPolicy *policyv1alpha1.ClusterPropagationPolicy
}{
{
name: "matching cluster policy found",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
},
},
},
policies: []*policyv1alpha1.ClusterPropagationPolicy{
{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-policy-1",
},
Spec: policyv1alpha1.PropagationSpec{
ResourceSelectors: []policyv1alpha1.ResourceSelector{
{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
},
},
},
expectedPolicy: &policyv1alpha1.ClusterPropagationPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-policy-1",
},
Spec: policyv1alpha1.PropagationSpec{
ResourceSelectors: []policyv1alpha1.ResourceSelector{
{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scheme := setupTestScheme()
fakeClient := dynamicfake.NewSimpleDynamicClient(scheme)
d := &ResourceDetector{
DynamicClient: fakeClient,
clusterPropagationPolicyLister: &mockClusterPropagationPolicyLister{
policies: tt.policies,
},
}
objectKey := keys.ClusterWideKey{
Name: tt.object.GetName(),
Namespace: tt.object.GetNamespace(),
Kind: tt.object.GetKind(),
}
policy, err := d.LookForMatchedClusterPolicy(tt.object, objectKey)
if err != nil {
t.Errorf("LookForMatchedClusterPolicy returned an error: %v", err)
}
fmt.Printf("Returned cluster policy: %+v\n", policy)
if tt.expectedPolicy == nil {
assert.Nil(t, policy)
} else {
assert.NotNil(t, policy)
if policy != nil {
assert.Equal(t, tt.expectedPolicy.Name, policy.Name)
}
}
})
}
}
func TestApplyPolicy(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
policy *policyv1alpha1.PropagationPolicy
resourceChangeByKarmada bool
expectError bool
}{
{
name: "basic apply policy",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
"uid": "test-uid",
},
},
},
policy: &policyv1alpha1.PropagationPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-policy",
Namespace: "default",
},
Spec: policyv1alpha1.PropagationSpec{},
},
resourceChangeByKarmada: false,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scheme := setupTestScheme()
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(tt.object).Build()
fakeRecorder := record.NewFakeRecorder(10)
fakeDynamicClient := dynamicfake.NewSimpleDynamicClient(scheme)
d := &ResourceDetector{
Client: fakeClient,
DynamicClient: fakeDynamicClient,
EventRecorder: fakeRecorder,
ResourceInterpreter: &mockResourceInterpreter{},
RESTMapper: &mockRESTMapper{},
}
err := d.ApplyPolicy(tt.object, keys.ClusterWideKey{}, tt.resourceChangeByKarmada, tt.policy)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
// Verify that the ResourceBinding was created
binding := &workv1alpha2.ResourceBinding{}
err = fakeClient.Get(context.TODO(), client.ObjectKey{
Namespace: tt.object.GetNamespace(),
Name: tt.object.GetName() + "-" + strings.ToLower(tt.object.GetKind()),
}, binding)
assert.NoError(t, err)
assert.Equal(t, tt.object.GetName(), binding.Spec.Resource.Name)
}
})
}
}
func TestApplyClusterPolicy(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
policy *policyv1alpha1.ClusterPropagationPolicy
resourceChangeByKarmada bool
expectError bool
}{
{
name: "apply cluster policy for namespaced resource",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-deployment",
"namespace": "default",
"uid": "test-uid",
},
},
},
policy: &policyv1alpha1.ClusterPropagationPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cluster-policy",
},
Spec: policyv1alpha1.PropagationSpec{},
},
resourceChangeByKarmada: false,
expectError: false,
},
{
name: "apply cluster policy for cluster-scoped resource",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole",
"metadata": map[string]interface{}{
"name": "test-cluster-role",
"uid": "test-uid",
},
},
},
policy: &policyv1alpha1.ClusterPropagationPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test-cluster-policy",
},
Spec: policyv1alpha1.PropagationSpec{},
},
resourceChangeByKarmada: false,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
scheme := setupTestScheme()
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
fakeRecorder := record.NewFakeRecorder(10)
fakeDynamicClient := dynamicfake.NewSimpleDynamicClient(scheme)
d := &ResourceDetector{
Client: fakeClient,
DynamicClient: fakeDynamicClient,
EventRecorder: fakeRecorder,
ResourceInterpreter: &mockResourceInterpreter{},
RESTMapper: &mockRESTMapper{},
}
err := d.ApplyClusterPolicy(tt.object, keys.ClusterWideKey{}, tt.resourceChangeByKarmada, tt.policy)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
// Check if ResourceBinding or ClusterResourceBinding was created
if tt.object.GetNamespace() != "" {
binding := &workv1alpha2.ResourceBinding{}
err = fakeClient.Get(context.TODO(), client.ObjectKey{
Namespace: tt.object.GetNamespace(),
Name: tt.object.GetName() + "-" + strings.ToLower(tt.object.GetKind()),
}, binding)
assert.NoError(t, err)
assert.Equal(t, tt.object.GetName(), binding.Spec.Resource.Name)
} else {
binding := &workv1alpha2.ClusterResourceBinding{}
err = fakeClient.Get(context.TODO(), client.ObjectKey{
Name: tt.object.GetName() + "-" + strings.ToLower(tt.object.GetKind()),
}, binding)
assert.NoError(t, err)
assert.Equal(t, tt.object.GetName(), binding.Spec.Resource.Name)
}
}
})
}
}
// Helper Functions
// setupTestScheme creates a runtime scheme with necessary types for testing
func setupTestScheme() *runtime.Scheme {
scheme := runtime.NewScheme()
_ = workv1alpha2.Install(scheme)
_ = corev1.AddToScheme(scheme)
return scheme
}
// Mock implementations
// mockAsyncWorker is a mock implementation of util.AsyncWorker
type mockAsyncWorker struct {
enqueueCount int
lastEnqueued interface{}
}
func (m *mockAsyncWorker) Enqueue(item interface{}) {
m.enqueueCount++
m.lastEnqueued = item
}
func (m *mockAsyncWorker) Add(_ interface{}) {
m.enqueueCount++
}
func (m *mockAsyncWorker) AddAfter(_ interface{}, _ time.Duration) {}
func (m *mockAsyncWorker) Run(_ int, _ <-chan struct{}) {}
// mockRESTMapper is a simple mock that satisfies the meta.RESTMapper interface
type mockRESTMapper struct{}
func (m *mockRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
return schema.GroupVersionKind{Group: resource.Group, Version: resource.Version, Kind: resource.Resource}, nil
}
func (m *mockRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
gvk, err := m.KindFor(resource)
if err != nil {
return nil, err
}
return []schema.GroupVersionKind{gvk}, nil
}
func (m *mockRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
return input, nil
}
func (m *mockRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
return []schema.GroupVersionResource{input}, nil
}
func (m *mockRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
return &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: gk.Group, Version: versions[0], Resource: gk.Kind},
GroupVersionKind: schema.GroupVersionKind{Group: gk.Group, Version: versions[0], Kind: gk.Kind},
Scope: meta.RESTScopeNamespace,
}, nil
}
func (m *mockRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
mapping, err := m.RESTMapping(gk, versions...)
if err != nil {
return nil, err
}
return []*meta.RESTMapping{mapping}, nil
}
func (m *mockRESTMapper) ResourceSingularizer(resource string) (string, error) {
return resource, nil
}
// mockPropagationPolicyLister is a mock implementation of the PropagationPolicyLister
type mockPropagationPolicyLister struct {
policies []*policyv1alpha1.PropagationPolicy
}
func (m *mockPropagationPolicyLister) List(_ labels.Selector) (ret []runtime.Object, err error) {
var result []runtime.Object
for _, p := range m.policies {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(p)
if err != nil {
return nil, err
}
result = append(result, &unstructured.Unstructured{Object: u})
}
return result, nil
}
func (m *mockPropagationPolicyLister) Get(name string) (runtime.Object, error) {
for _, p := range m.policies {
if p.Name == name {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(p)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: u}, nil
}
}
return nil, nil
}
func (m *mockPropagationPolicyLister) ByNamespace(namespace string) cache.GenericNamespaceLister {
return &mockGenericNamespaceLister{
policies: m.policies,
namespace: namespace,
}
}
// mockGenericNamespaceLister is a mock implementation of cache.GenericNamespaceLister
type mockGenericNamespaceLister struct {
policies []*policyv1alpha1.PropagationPolicy
namespace string
}
func (m *mockGenericNamespaceLister) List(_ labels.Selector) (ret []runtime.Object, err error) {
var result []runtime.Object
for _, p := range m.policies {
if p.Namespace == m.namespace {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(p)
if err != nil {
return nil, err
}
result = append(result, &unstructured.Unstructured{Object: u})
}
}
return result, nil
}
func (m *mockGenericNamespaceLister) Get(name string) (runtime.Object, error) {
for _, p := range m.policies {
if p.Name == name && p.Namespace == m.namespace {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(p)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: u}, nil
}
}
return nil, nil
}
// mockClusterPropagationPolicyLister is a mock implementation of the ClusterPropagationPolicyLister
type mockClusterPropagationPolicyLister struct {
policies []*policyv1alpha1.ClusterPropagationPolicy
}
func (m *mockClusterPropagationPolicyLister) List(_ labels.Selector) (ret []runtime.Object, err error) {
var result []runtime.Object
for _, p := range m.policies {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(p)
if err != nil {
return nil, err
}
result = append(result, &unstructured.Unstructured{Object: u})
}
return result, nil
}
func (m *mockClusterPropagationPolicyLister) Get(name string) (runtime.Object, error) {
for _, p := range m.policies {
if p.Name == name {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(p)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: u}, nil
}
}
return nil, nil
}
func (m *mockClusterPropagationPolicyLister) ByNamespace(_ string) cache.GenericNamespaceLister {
return nil // ClusterPropagationPolicies are not namespaced
}
// mockResourceInterpreter is a mock implementation of the ResourceInterpreter interface
type mockResourceInterpreter struct{}
func (m *mockResourceInterpreter) Start(_ context.Context) error {
return nil
}
func (m *mockResourceInterpreter) HookEnabled(_ schema.GroupVersionKind, _ configv1alpha1.InterpreterOperation) bool {
return false
}
func (m *mockResourceInterpreter) GetReplicas(_ *unstructured.Unstructured) (int32, *workv1alpha2.ReplicaRequirements, error) {
return 0, nil, nil
}
func (m *mockResourceInterpreter) ReviseReplica(object *unstructured.Unstructured, _ int64) (*unstructured.Unstructured, error) {
return object, nil
}
func (m *mockResourceInterpreter) Retain(desired *unstructured.Unstructured, _ *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return desired, nil
}
func (m *mockResourceInterpreter) AggregateStatus(object *unstructured.Unstructured, _ []workv1alpha2.AggregatedStatusItem) (*unstructured.Unstructured, error) {
return object, nil
}
func (m *mockResourceInterpreter) GetDependencies(_ *unstructured.Unstructured) ([]configv1alpha1.DependentObjectReference, error) {
return nil, nil
}
func (m *mockResourceInterpreter) ReflectStatus(_ *unstructured.Unstructured) (*runtime.RawExtension, error) {
return nil, nil
}
func (m *mockResourceInterpreter) InterpretHealth(_ *unstructured.Unstructured) (bool, error) {
return true, nil
}