karmada/pkg/resourceinterpreter/default/native/default_test.go

711 lines
18 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 native
import (
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
"github.com/karmada-io/karmada/pkg/util/helper"
)
var e = NewDefaultInterpreter()
func TestHookEnabled(t *testing.T) {
tests := []struct {
name string
kind schema.GroupVersionKind
operationType configv1alpha1.InterpreterOperation
want bool
}{
{
name: "deployment with interpretreplica operation enabled",
kind: schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
},
operationType: configv1alpha1.InterpreterOperationInterpretReplica,
want: true,
}, {
name: "statefulset with revisereplica operation enabled",
kind: schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "StatefulSet",
},
operationType: configv1alpha1.InterpreterOperationReviseReplica,
want: true,
}, {
name: "persistentvolumeclaim with retain operation enabled",
kind: schema.GroupVersionKind{
Group: "",
Version: "v1",
Kind: "PersistentVolumeClaim",
},
operationType: configv1alpha1.InterpreterOperationRetain,
want: true,
}, {
name: "ingress with aggregatestatus operation enabled",
kind: schema.GroupVersionKind{
Group: "networking.k8s.io",
Version: "v1",
Kind: "Ingress",
},
operationType: configv1alpha1.InterpreterOperationAggregateStatus,
want: true,
}, {
name: "serviceimport with interpretdependency operation enabled",
kind: schema.GroupVersionKind{
Group: "multicluster.x-k8s.io",
Version: "v1alpha1",
Kind: "ServiceImport",
},
operationType: configv1alpha1.InterpreterOperationInterpretDependency,
want: true,
}, {
name: "deployment with interpretstatus operation enabled",
kind: schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
},
operationType: configv1alpha1.InterpreterOperationInterpretStatus,
want: true,
}, {
name: "poddisruptionbudget with interprethealth operation enabled",
kind: schema.GroupVersionKind{
Group: "policy",
Version: "v1",
Kind: "PodDisruptionBudget",
},
operationType: configv1alpha1.InterpreterOperationInterpretHealth,
want: true,
}, {
name: "foot5zmh with prune operation enabled",
kind: schema.GroupVersionKind{
Group: "example-stgzr.karmada.io",
Version: "v1alpha1",
Kind: "Foot5zmh",
},
operationType: configv1alpha1.InterpreterOperationPrune,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := e.HookEnabled(tt.kind, tt.operationType)
if got != tt.want {
t.Errorf("HookEnabled() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetReplicas(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
wantReplica int32
wantRequirement *workv1alpha2.ReplicaRequirements
wantErr bool
}{
{
name: "desired replica exists",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "fake-deployment",
"namespace": "default",
"labels": map[string]interface{}{"app": "my-app"},
},
"spec": map[string]interface{}{
"replicas": int64(2),
"selector": map[string]interface{}{
"matchLabels": map[string]interface{}{"app": "my-app"},
},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{"app": "my-app"},
},
"spec": map[string]interface{}{
"nodeSelector": map[string]interface{}{
"foo": "foo1",
},
"tolerations": []interface{}{
map[string]interface{}{
"key": "foo",
"operator": "Exists",
"effect": "NoSchedule",
},
},
"affinity": map[string]interface{}{
"nodeAffinity": map[string]interface{}{
"requiredDuringSchedulingIgnoredDuringExecution": map[string]interface{}{
"nodeSelectorTerms": []interface{}{
map[string]interface{}{
"matchFields": []interface{}{
map[string]interface{}{
"key": "foo",
"operator": "Exists",
},
},
},
},
},
},
},
},
},
},
},
},
wantReplica: 2,
wantRequirement: &workv1alpha2.ReplicaRequirements{
NodeClaim: &workv1alpha2.NodeClaim{
NodeSelector: map[string]string{
"foo": "foo1",
},
Tolerations: []corev1.Toleration{
{
Key: "foo",
Operator: corev1.TolerationOpExists,
Effect: corev1.TaintEffectNoSchedule,
},
},
HardNodeAffinity: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{
{
MatchFields: []corev1.NodeSelectorRequirement{
{
Key: "foo",
Operator: corev1.NodeSelectorOpExists,
},
},
},
},
},
},
},
wantErr: false,
}, {
name: "cannot get desired replica of given kind and apiversion",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "fake-pod",
"namespace": "default",
"labels": map[string]interface{}{"app": "my-app"},
},
},
},
wantReplica: 0,
wantRequirement: &workv1alpha2.ReplicaRequirements{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotReplica, gotRequirement, err := e.GetReplicas(tt.object)
if (err != nil) != tt.wantErr {
t.Errorf("e.GetReplicas() error = %v, want %v", err, tt.wantErr)
}
assert.Equalf(t, tt.wantReplica, gotReplica, "e.GetReplicas(%v)", tt.object)
assert.Equalf(t, tt.wantRequirement, gotRequirement, "e.GetReplicas(%v)", tt.object)
})
}
}
func TestReviseReplica(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
replica int64
want *unstructured.Unstructured
wantErr bool
}{
{
name: "revise deployment replica",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "fake-deployment",
},
"spec": map[string]interface{}{
"replicas": int64(1),
},
},
},
replica: 3,
want: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "fake-deployment",
},
"spec": map[string]interface{}{
"replicas": int64(3),
},
},
},
wantErr: false,
},
{
name: "cannot revise deployment replica of given kind and apiversion",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Job",
"metadata": map[string]interface{}{
"name": "fake-deployment",
},
"spec": map[string]interface{}{
"replicas": int64(1),
},
},
},
replica: 3,
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := e.ReviseReplica(tt.object, tt.replica)
if (err != nil) != tt.wantErr {
t.Errorf("e.ReviseReplica() error = %v, want %v", err, tt.wantErr)
}
assert.Equalf(t, tt.want, got, "e.ReviseReplica(%v,%v)", tt.object, tt.replica)
})
}
}
func TestRetain(t *testing.T) {
tests := []struct {
name string
desired *unstructured.Unstructured
observed *unstructured.Unstructured
want *unstructured.Unstructured
wantErr bool
}{
{
name: "retain observed replica",
desired: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "nginx",
},
"spec": map[string]interface{}{
"replicas": int32(2),
},
},
},
observed: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "nginx",
},
"spec": map[string]interface{}{
"replicas": int32(4),
},
},
},
want: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "nginx",
},
"spec": map[string]interface{}{
"replicas": int32(2),
},
},
},
wantErr: false,
},
{
name: "cannot retain observed replica of given kind and apiversion",
desired: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "nginx",
},
"spec": map[string]interface{}{
"replicas": int32(2),
},
},
},
observed: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "nginx",
},
"spec": map[string]interface{}{
"replicas": int32(4),
},
},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := e.Retain(tt.desired, tt.observed)
if (err != nil) != tt.wantErr {
t.Errorf("e.Retain() error = %v, want %v", err, tt.wantErr)
}
assert.Equalf(t, tt.want, got, "e.Retain(%v,%v)", tt.desired, tt.observed)
})
}
}
func TestAggregateStatus(t *testing.T) {
statusMap := map[string]interface{}{
"replicas": 1,
"readyReplicas": 1,
"updatedReplicas": 1,
"availableReplicas": 1,
"unavailableReplicas": 0,
}
raw, err := helper.BuildStatusRawExtension(statusMap)
if err != nil {
klog.Errorf("Failed to build raw, error: %v", err)
return
}
tests := []struct {
name string
object *unstructured.Unstructured
aggregatedStatusItems []workv1alpha2.AggregatedStatusItem
want *unstructured.Unstructured
wantErr bool
}{
{
name: "update deployment status",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
},
},
aggregatedStatusItems: []workv1alpha2.AggregatedStatusItem{
{
ClusterName: "member1",
Status: raw,
Applied: true,
},
{
ClusterName: "member2",
Status: raw,
Applied: true,
},
},
want: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
},
"spec": map[string]interface{}{
"selector": nil,
"strategy": map[string]interface{}{},
"template": map[string]interface{}{
"metadata": map[string]interface{}{
"creationTimestamp": nil,
},
"spec": map[string]interface{}{
"containers": nil,
},
},
},
"status": map[string]interface{}{
"availableReplicas": int64(2),
"readyReplicas": int64(2),
"replicas": int64(2),
"updatedReplicas": int64(2),
},
},
},
wantErr: false,
}, {
name: "cannot update deployment status for given kind and apiversion",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Job",
},
},
aggregatedStatusItems: []workv1alpha2.AggregatedStatusItem{
{
ClusterName: "member1",
Status: raw,
Applied: true,
},
{
ClusterName: "member2",
Status: raw,
Applied: true,
},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := e.AggregateStatus(tt.object, tt.aggregatedStatusItems)
if (err != nil) != tt.wantErr {
t.Errorf("e.AggregateStatus() error = %v, want %v", err, tt.wantErr)
}
assert.Equalf(t, tt.want, got, "e.AggregateStatus(%v,%v)", tt.object, tt.aggregatedStatusItems)
})
}
}
func TestGetDependencies(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
want []configv1alpha1.DependentObjectReference
wantErr bool
}{
{
name: "deployment with dependencies 2",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "fake-deployment",
"namespace": namespace,
},
"spec": map[string]interface{}{
"replicas": 3,
"template": map[string]interface{}{
"spec": testPairs[1].podSpecsWithDependencies.Object,
},
},
},
},
want: []configv1alpha1.DependentObjectReference{
{
APIVersion: "v1",
Kind: "ConfigMap",
Namespace: namespace,
Name: "initcontainer-envfrom-configmap",
},
{
APIVersion: "v1",
Kind: "Secret",
Namespace: namespace,
Name: "test-secret",
},
{
APIVersion: "v1",
Kind: "ServiceAccount",
Namespace: namespace,
Name: "test-serviceaccount",
},
},
wantErr: false,
},
{
name: "can not get dependencies of given kind and apiversion",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "CronJob",
"metadata": map[string]interface{}{
"name": "fake-cronjob",
"namespace": namespace,
},
"spec": map[string]interface{}{
"replicas": 3,
"template": map[string]interface{}{
"spec": testPairs[1].podSpecsWithDependencies.Object,
},
},
},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := e.GetDependencies(tt.object)
if (err != nil) != tt.wantErr {
t.Errorf("e.GetDependencies() error = %v, want %v", err, tt.wantErr)
}
assert.Equalf(t, tt.want, got, "e.GetDependencies(%v)", tt.object)
})
}
}
func TestReflectStatus(t *testing.T) {
currStatus := policyv1.PodDisruptionBudgetStatus{
CurrentHealthy: 1,
DesiredHealthy: 1,
DisruptionsAllowed: 1,
ExpectedPods: 1,
}
wantRawExtension, err := helper.BuildStatusRawExtension(&currStatus)
if err != nil {
klog.Errorf("Failed to build wantRawExtension, error: %v", err)
return
}
testRawExtension, err := helper.BuildStatusRawExtension(map[string]interface{}{"key": "value"})
if err != nil {
klog.Errorf("Failed to build testRawExtension, error: %v", err)
return
}
tests := []struct {
name string
object *unstructured.Unstructured
want *runtime.RawExtension
wantErr bool
}{
{
name: "object have correct format status",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "policy/v1",
"kind": "PodDisruptionBudget",
"status": map[string]interface{}{
"currentHealthy": int64(1),
"desiredHealthy": int64(1),
"disruptionsAllowed": int64(1),
"expectedPods": int64(1),
},
},
},
want: wantRawExtension,
wantErr: false,
},
{
name: "missing build-in handler",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"status": map[string]interface{}{"key": "value"},
},
},
want: testRawExtension,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := e.ReflectStatus(tt.object)
if (err != nil) != tt.wantErr {
t.Errorf("e.ReflectStatus() error = %v, want %v", err, tt.wantErr)
}
assert.Equalf(t, tt.want, got, "e.ReflectStatus(%v)", tt.object)
})
}
}
func TestInterpretHealth(t *testing.T) {
tests := []struct {
name string
object *unstructured.Unstructured
want bool
wantErr bool
}{
{
name: "deployment healthy",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "fake-deployment",
"generation": 1,
},
"spec": map[string]interface{}{
"replicas": 3,
},
"status": map[string]interface{}{
"availableReplicas": 3,
"updatedReplicas": 3,
"observedGeneration": 1,
},
},
},
want: true,
wantErr: false,
},
{
name: "cannot get health state for given kind and apiversion",
object: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "policy/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "fake-deployment",
"generation": 1,
},
"spec": map[string]interface{}{
"replicas": 3,
},
"status": map[string]interface{}{
"availableReplicas": 3,
"updatedReplicas": 3,
"observedGeneration": 1,
},
},
},
want: false,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := e.InterpretHealth(tt.object)
if (err != nil) != tt.wantErr {
t.Errorf("e.InterpretHealth() error = %v, want %v", err, tt.wantErr)
}
assert.Equalf(t, tt.want, got, "e.InterpretHealth(%v)", tt.object)
})
}
}