1191 lines
33 KiB
Go
1191 lines
33 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{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "Pod",
|
|
APIVersion: corev1.SchemeGroupVersion.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-pod",
|
|
Namespace: "default",
|
|
},
|
|
Spec: corev1.PodSpec{
|
|
Containers: []corev1.Container{{Name: "container1"}},
|
|
},
|
|
},
|
|
newObj: &corev1.Pod{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "Pod",
|
|
APIVersion: corev1.SchemeGroupVersion.String(),
|
|
},
|
|
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
|
|
}
|