karmada/pkg/controllers/cluster/cluster_controller_test.go

474 lines
14 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 cluster
import (
"context"
"fmt"
"reflect"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/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/features"
"github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/gclient"
"github.com/karmada-io/karmada/pkg/util/names"
)
func newClusterController() *Controller {
rbIndexerFunc := func(obj client.Object) []string {
rb, ok := obj.(*workv1alpha2.ResourceBinding)
if !ok {
return nil
}
return util.GetBindingClusterNames(&rb.Spec)
}
crbIndexerFunc := func(obj client.Object) []string {
crb, ok := obj.(*workv1alpha2.ClusterResourceBinding)
if !ok {
return nil
}
return util.GetBindingClusterNames(&crb.Spec)
}
client := fake.NewClientBuilder().WithScheme(gclient.NewSchema()).
WithIndex(&workv1alpha2.ResourceBinding{}, rbClusterKeyIndex, rbIndexerFunc).
WithIndex(&workv1alpha2.ClusterResourceBinding{}, crbClusterKeyIndex, crbIndexerFunc).
WithStatusSubresource(&clusterv1alpha1.Cluster{}).Build()
return &Controller{
Client: client,
EventRecorder: record.NewFakeRecorder(1024),
clusterHealthMap: newClusterHealthMap(),
EnableTaintManager: true,
ClusterMonitorGracePeriod: 40 * time.Second,
}
}
func TestController_Reconcile(t *testing.T) {
req := controllerruntime.Request{NamespacedName: types.NamespacedName{Name: "test-cluster"}}
tests := []struct {
name string
cluster *clusterv1alpha1.Cluster
ns *corev1.Namespace
work *workv1alpha1.Work
del bool
wCluster *clusterv1alpha1.Cluster
want controllerruntime.Result
wantErr bool
}{
{
name: "cluster without status",
cluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
},
wCluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{
Taints: []corev1.Taint{{
Key: clusterv1alpha1.TaintClusterNotReady,
Effect: corev1.TaintEffectNoSchedule,
}},
},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{},
},
},
want: controllerruntime.Result{},
wantErr: false,
},
{
name: "cluster with ready condition",
cluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{
Taints: []corev1.Taint{{
Key: clusterv1alpha1.TaintClusterNotReady,
Effect: corev1.TaintEffectNoSchedule,
}},
},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionTrue,
},
},
},
},
wCluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{Taints: []corev1.Taint{}},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionTrue,
},
},
},
},
want: controllerruntime.Result{},
wantErr: false,
},
{
name: "cluster with unknown condition",
cluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{
Taints: []corev1.Taint{{
Key: clusterv1alpha1.TaintClusterNotReady,
Effect: corev1.TaintEffectNoSchedule,
}},
},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionUnknown,
},
},
},
},
wCluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{Taints: []corev1.Taint{
{
Key: clusterv1alpha1.TaintClusterUnreachable,
Effect: corev1.TaintEffectNoSchedule,
},
}},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionUnknown,
},
},
},
},
want: controllerruntime.Result{},
wantErr: false,
},
{
name: "cluster with false condition",
cluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{
Taints: []corev1.Taint{{
Key: clusterv1alpha1.TaintClusterUnreachable,
Effect: corev1.TaintEffectNoSchedule,
}},
},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionFalse,
},
},
},
},
wCluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{Taints: []corev1.Taint{
{
Key: clusterv1alpha1.TaintClusterNotReady,
Effect: corev1.TaintEffectNoSchedule,
},
}},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionFalse,
},
},
},
},
want: controllerruntime.Result{},
wantErr: false,
},
{
name: "cluster not found",
cluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster-noexist",
Finalizers: []string{util.ClusterControllerFinalizer},
},
},
want: controllerruntime.Result{},
wantErr: false,
},
{
name: "remove cluster failed",
cluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{
SyncMode: clusterv1alpha1.Pull,
},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionFalse,
},
},
},
},
ns: &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: names.GenerateExecutionSpaceName("test-cluster"),
},
},
work: &workv1alpha1.Work{
ObjectMeta: metav1.ObjectMeta{
Name: "test-work",
Namespace: names.GenerateExecutionSpaceName("test-cluster"),
Finalizers: []string{util.ExecutionControllerFinalizer},
},
},
del: true,
want: controllerruntime.Result{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := newClusterController()
if tt.cluster != nil {
if err := c.Create(context.Background(), tt.cluster, &client.CreateOptions{}); err != nil {
t.Fatalf("failed to create cluster %v", err)
}
}
if tt.ns != nil {
if err := c.Create(context.Background(), tt.ns, &client.CreateOptions{}); err != nil {
t.Fatalf("failed to create ns %v", err)
}
}
if tt.work != nil {
if err := c.Create(context.Background(), tt.work, &client.CreateOptions{}); err != nil {
t.Fatalf("failed to create work %v", err)
}
}
if tt.del {
if err := c.Delete(context.Background(), tt.cluster, &client.DeleteOptions{}); err != nil {
t.Fatalf("failed to delete cluster %v", err)
}
}
got, err := c.Reconcile(context.Background(), req)
if (err != nil) != tt.wantErr {
t.Errorf("Controller.Reconcile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Controller.Reconcile() = %v, want %v", got, tt.want)
return
}
if tt.wCluster != nil {
cluster := &clusterv1alpha1.Cluster{}
if err := c.Get(context.Background(), types.NamespacedName{Name: tt.cluster.Name}, cluster, &client.GetOptions{}); err != nil {
t.Errorf("failed to get cluster %v", err)
return
}
cleanUpCluster(cluster)
if !reflect.DeepEqual(cluster, tt.wCluster) {
t.Errorf("Cluster resource reconcile get %v, want %v", *cluster, *tt.wCluster)
}
}
})
}
}
func TestController_monitorClusterHealth(t *testing.T) {
tests := []struct {
name string
cluster *clusterv1alpha1.Cluster
wCluster *clusterv1alpha1.Cluster
wantErr bool
}{
{
name: "cluster without status",
cluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{
SyncMode: clusterv1alpha1.Pull,
},
},
wCluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{
SyncMode: clusterv1alpha1.Pull,
Taints: []corev1.Taint{
{
Key: clusterv1alpha1.TaintClusterUnreachable,
Effect: corev1.TaintEffectNoExecute,
},
},
},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionUnknown,
Reason: "ClusterStatusNeverUpdated",
Message: "Cluster status controller never posted cluster status.",
}},
},
},
wantErr: false,
},
{
name: "cluster with ready condition",
cluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{
SyncMode: clusterv1alpha1.Pull,
},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionTrue,
},
},
},
},
wCluster: &clusterv1alpha1.Cluster{
ObjectMeta: controllerruntime.ObjectMeta{
Name: "test-cluster",
Finalizers: []string{util.ClusterControllerFinalizer},
},
Spec: clusterv1alpha1.ClusterSpec{
SyncMode: clusterv1alpha1.Pull,
Taints: []corev1.Taint{},
},
Status: clusterv1alpha1.ClusterStatus{
Conditions: []metav1.Condition{
{
Type: clusterv1alpha1.ClusterConditionReady,
Status: metav1.ConditionTrue,
},
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := features.FeatureGate.Set(fmt.Sprintf("%s=%t", features.Failover, true))
if err != nil {
t.Fatalf("Failed to enable failover feature gate: %v", err)
}
c := newClusterController()
if tt.cluster != nil {
if err = c.Create(context.Background(), tt.cluster, &client.CreateOptions{}); err != nil {
t.Fatalf("failed to create cluster: %v", err)
}
}
if err = c.monitorClusterHealth(context.Background()); (err != nil) != tt.wantErr {
t.Errorf("Controller.monitorClusterHealth() error = %v, wantErr %v", err, tt.wantErr)
return
}
cluster := &clusterv1alpha1.Cluster{}
if err = c.Get(context.Background(), types.NamespacedName{Name: "test-cluster"}, cluster, &client.GetOptions{}); err != nil {
t.Errorf("failed to get cluster: %v", err)
return
}
cleanUpCluster(cluster)
if !reflect.DeepEqual(cluster, tt.wCluster) {
t.Errorf("Cluster resource get %+v, want %+v", *cluster, *tt.wCluster)
return
}
})
}
}
// cleanUpCluster removes unnecessary fields from Cluster resource for testing purposes.
func cleanUpCluster(c *clusterv1alpha1.Cluster) {
c.ObjectMeta.ResourceVersion = ""
taints := []corev1.Taint{}
for _, taint := range c.Spec.Taints {
taint.TimeAdded = nil
taints = append(taints, taint)
}
c.Spec.Taints = taints
cond := []metav1.Condition{}
for _, condition := range c.Status.Conditions {
condition.LastTransitionTime = metav1.Time{}
cond = append(cond, condition)
}
c.Status.Conditions = cond
}