karmada/pkg/controllers/status/cluster_status_controller_t...

1154 lines
34 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 status
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
kubernetesfake "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/cache"
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"
"github.com/karmada-io/karmada/pkg/sharedcli/ratelimiterflag"
"github.com/karmada-io/karmada/pkg/util"
"github.com/karmada-io/karmada/pkg/util/fedinformer/genericmanager"
"github.com/karmada-io/karmada/pkg/util/fedinformer/typedmanager"
"github.com/karmada-io/karmada/pkg/util/gclient"
"github.com/karmada-io/karmada/pkg/util/helper"
)
// copy from go/src/net/http/internal/testcert/testcert.go
var testCA = []byte(`-----BEGIN CERTIFICATE-----
MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r
bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U
aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P
YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk
POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu
h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE
AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud
DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv
bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI
5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv
cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2
+tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B
grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK
5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/
WkBKOclmOV2xlTVuPw==
-----END CERTIFICATE-----`)
func TestClusterStatusController_Reconcile(t *testing.T) {
tests := []struct {
name string
cluster *clusterv1alpha1.Cluster
clusterName string
expectedResult controllerruntime.Result
expectedError bool
}{
{
name: "Cluster not found",
clusterName: "test-cluster",
expectedResult: controllerruntime.Result{},
expectedError: false,
},
{
name: "Cluster found with finalizer",
clusterName: "test-cluster",
cluster: &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{
Finalizers: []string{
util.ClusterControllerFinalizer,
},
},
},
expectedResult: controllerruntime.Result{},
expectedError: false,
},
{
name: "Cluster found without finalizer",
clusterName: "test-cluster",
cluster: &clusterv1alpha1.Cluster{},
expectedResult: controllerruntime.Result{Requeue: true},
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up the test environment.
c := &ClusterStatusController{
Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).Build(),
GenericInformerManager: genericmanager.GetInstance(),
TypedInformerManager: typedmanager.GetInstance(),
ClusterClientOption: &util.ClientOption{
QPS: 5,
Burst: 10,
},
ClusterClientSetFunc: util.NewClusterClientSet,
}
if tt.cluster != nil {
// Add a cluster to the fake client.
tt.cluster.ObjectMeta.Name = tt.clusterName
c.Client = fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithStatusSubresource(tt.cluster).Build()
if err := c.Client.Create(context.Background(), tt.cluster); err != nil {
t.Fatalf("Failed to create cluster: %v", err)
}
}
// Run the reconcile function.
req := controllerruntime.Request{
NamespacedName: types.NamespacedName{
Name: tt.clusterName,
},
}
result, err := c.Reconcile(context.Background(), req)
// Check the results.
if tt.expectedError && err == nil {
t.Errorf("Expected an error but got nil")
} else if !tt.expectedError && err != nil {
t.Errorf("Expected no error but got %v", err)
}
if !reflect.DeepEqual(tt.expectedResult, result) {
t.Errorf("Expected result %v but got %v", tt.expectedResult, result)
}
})
}
}
func generateClusterClient(APIEndpoint string) *util.ClusterClient {
clusterClient := &util.ClusterClient{
ClusterName: "test",
}
hostClient := fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithObjects(
&clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: clusterv1alpha1.ClusterSpec{
APIEndpoint: APIEndpoint,
SecretRef: &clusterv1alpha1.LocalSecretReference{Namespace: "ns1", Name: "secret1"},
},
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns1", Name: "secret1"},
Data: map[string][]byte{clusterv1alpha1.SecretTokenKey: []byte("token"), clusterv1alpha1.SecretCADataKey: testCA},
}).Build()
clusterClientSet, _ := util.NewClusterClientSet("test", hostClient, nil)
clusterClient.KubeClient = clusterClientSet.KubeClient
return clusterClient
}
func clusterClientSetFunc(string, client.Client, *util.ClientOption) (*util.ClusterClient, error) {
clusterClient := generateClusterClient(serverAddress)
// cannot mock "/readyz"'s response, so create an error
return clusterClient, nil
}
func clusterClientSetFuncWithError(string, client.Client, *util.ClientOption) (*util.ClusterClient, error) {
clusterClient := generateClusterClient(serverAddress)
return clusterClient, fmt.Errorf("err")
}
var serverAddress string
func TestClusterStatusController_syncClusterStatus(t *testing.T) {
t.Run("ClusterClientSetFunc returns error", func(t *testing.T) {
server := mockServer(http.StatusOK, false)
defer server.Close()
serverAddress = server.URL
cluster := &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: clusterv1alpha1.ClusterSpec{
APIEndpoint: server.URL,
SecretRef: &clusterv1alpha1.LocalSecretReference{Namespace: "ns1", Name: "secret1"},
ProxyURL: "http://1.1.1.1",
},
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns1", Name: "secret1"},
Data: map[string][]byte{clusterv1alpha1.SecretTokenKey: []byte("token"), clusterv1alpha1.SecretCADataKey: testCA},
}
c := &ClusterStatusController{
Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithStatusSubresource(cluster, secret).Build(),
GenericInformerManager: genericmanager.GetInstance(),
TypedInformerManager: typedmanager.GetInstance(),
ClusterSuccessThreshold: metav1.Duration{
Duration: time.Duration(1000),
},
ClusterFailureThreshold: metav1.Duration{
Duration: time.Duration(1000),
},
clusterConditionCache: clusterConditionStore{},
PredicateFunc: helper.NewClusterPredicateOnAgent("test"),
RateLimiterOptions: ratelimiterflag.Options{
RateLimiterBaseDelay: time.Duration(1000),
RateLimiterMaxDelay: time.Duration(1000),
RateLimiterQPS: 10,
RateLimiterBucketSize: 10,
},
ClusterClientSetFunc: clusterClientSetFuncWithError,
ClusterDynamicClientSetFunc: util.NewClusterDynamicClientSetForAgent,
}
if err := c.Client.Create(context.Background(), cluster); err != nil {
t.Fatalf("Failed to create cluster: %v", err)
}
res, err := c.syncClusterStatus(cluster)
expect := controllerruntime.Result{}
assert.Equal(t, expect, res)
assert.Empty(t, err)
})
t.Run("online is false, readyCondition.Status isn't true", func(t *testing.T) {
server := mockServer(http.StatusNotFound, true)
defer server.Close()
serverAddress = server.URL
cluster := &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: clusterv1alpha1.ClusterSpec{
APIEndpoint: server.URL,
SecretRef: &clusterv1alpha1.LocalSecretReference{Namespace: "ns1", Name: "secret1"},
ProxyURL: "http://1.1.1.2",
},
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns1", Name: "secret1"},
Data: map[string][]byte{clusterv1alpha1.SecretTokenKey: []byte("token"), clusterv1alpha1.SecretCADataKey: testCA},
}
c := &ClusterStatusController{
Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithStatusSubresource(cluster, secret).Build(),
GenericInformerManager: genericmanager.GetInstance(),
TypedInformerManager: typedmanager.GetInstance(),
ClusterSuccessThreshold: metav1.Duration{
Duration: time.Duration(1000),
},
ClusterFailureThreshold: metav1.Duration{
Duration: time.Duration(1000),
},
clusterConditionCache: clusterConditionStore{},
PredicateFunc: helper.NewClusterPredicateOnAgent("test"),
RateLimiterOptions: ratelimiterflag.Options{
RateLimiterBaseDelay: time.Duration(1000),
RateLimiterMaxDelay: time.Duration(1000),
RateLimiterQPS: 10,
RateLimiterBucketSize: 10,
},
ClusterClientSetFunc: clusterClientSetFunc,
ClusterDynamicClientSetFunc: util.NewClusterDynamicClientSetForAgent,
}
if err := c.Client.Create(context.Background(), cluster); err != nil {
t.Fatalf("Failed to create cluster: %v", err)
}
res, err := c.syncClusterStatus(cluster)
expect := controllerruntime.Result{}
assert.Equal(t, expect, res)
assert.Empty(t, err)
})
t.Run("online and healthy is true", func(t *testing.T) {
server := mockServer(http.StatusOK, false)
defer server.Close()
serverAddress = server.URL
cluster := &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: clusterv1alpha1.ClusterSpec{
APIEndpoint: server.URL,
SecretRef: &clusterv1alpha1.LocalSecretReference{Namespace: "ns1", Name: "secret1"},
ProxyURL: "http://1.1.1.1",
},
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Namespace: "ns1", Name: "secret1"},
Data: map[string][]byte{clusterv1alpha1.SecretTokenKey: []byte("token"), clusterv1alpha1.SecretCADataKey: testCA},
}
c := &ClusterStatusController{
Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithStatusSubresource(cluster, secret).Build(),
GenericInformerManager: genericmanager.GetInstance(),
TypedInformerManager: typedmanager.GetInstance(),
ClusterSuccessThreshold: metav1.Duration{
Duration: time.Duration(1000),
},
ClusterFailureThreshold: metav1.Duration{
Duration: time.Duration(1000),
},
clusterConditionCache: clusterConditionStore{},
PredicateFunc: helper.NewClusterPredicateOnAgent("test"),
RateLimiterOptions: ratelimiterflag.Options{
RateLimiterBaseDelay: time.Duration(1000),
RateLimiterMaxDelay: time.Duration(1000),
RateLimiterQPS: 10,
RateLimiterBucketSize: 10,
},
ClusterClientSetFunc: clusterClientSetFunc,
ClusterDynamicClientSetFunc: util.NewClusterDynamicClientSetForAgent,
}
if err := c.Client.Create(context.Background(), cluster); err != nil {
t.Fatalf("Failed to create cluster: %v", err)
}
res, err := c.syncClusterStatus(cluster)
expect := controllerruntime.Result{}
assert.Equal(t, expect, res)
assert.Empty(t, err)
})
}
func TestGetNodeSummary(t *testing.T) {
nodes := []*corev1.Node{
{
Status: corev1.NodeStatus{
Conditions: []corev1.NodeCondition{
{
Type: corev1.NodeReady,
Status: corev1.ConditionTrue,
},
},
},
},
{
Status: corev1.NodeStatus{
Conditions: []corev1.NodeCondition{
{
Type: corev1.NodeMemoryPressure,
},
},
},
},
}
res := getNodeSummary(nodes)
if res.TotalNum != 2 {
t.Errorf("TotalNum is %v, expect 2", res.TotalNum)
}
if res.ReadyNum != 1 {
t.Errorf("ReadyNum is %v, expect 1", res.ReadyNum)
}
}
func TestGenerateReadyCondition(t *testing.T) {
online := true
healthy := true
con := generateReadyCondition(online, healthy)
expect := util.NewCondition(clusterv1alpha1.ClusterConditionReady, clusterReady, clusterHealthy, metav1.ConditionTrue)
assert.Equal(t, expect, con)
healthy = false
con = generateReadyCondition(online, healthy)
expect = util.NewCondition(clusterv1alpha1.ClusterConditionReady, clusterNotReady, clusterUnhealthy, metav1.ConditionFalse)
assert.Equal(t, expect, con)
online = false
con = generateReadyCondition(online, healthy)
expect = util.NewCondition(clusterv1alpha1.ClusterConditionReady, clusterNotReachableReason, clusterNotReachableMsg, metav1.ConditionFalse)
assert.Equal(t, expect, con)
}
func TransformFunc(interface{}) (interface{}, error) {
return nil, nil
}
func TestListPods(t *testing.T) {
stopCh := make(<-chan struct{})
clientset := kubernetesfake.NewSimpleClientset()
transformFuncs := map[schema.GroupVersionResource]cache.TransformFunc{
{}: TransformFunc,
}
m := typedmanager.NewSingleClusterInformerManager(clientset, 0, stopCh, transformFuncs)
pods, err := listPods(m)
assert.Equal(t, []*corev1.Pod(nil), pods)
assert.Empty(t, err, "listPods returns error")
}
func TestListNodes(t *testing.T) {
stopCh := make(<-chan struct{})
clientset := kubernetesfake.NewSimpleClientset()
transformFuncs := map[schema.GroupVersionResource]cache.TransformFunc{
{}: TransformFunc,
}
m := typedmanager.NewSingleClusterInformerManager(clientset, 0, stopCh, transformFuncs)
nodes, err := listNodes(m)
assert.Equal(t, []*corev1.Node(nil), nodes)
assert.Empty(t, err, "listNodes returns error")
}
func TestGetResourceSummary(t *testing.T) {
nodes := []*corev1.Node{
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
}
pods := []*corev1.Pod{
{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"test": resource.Quantity{},
}}}},
}}}
resourceSummary := getResourceSummary(nodes, pods)
if _, ok := resourceSummary.Allocatable["test"]; !ok {
t.Errorf("key test isn't in resourceSummary.Allocatable")
}
if _, ok := resourceSummary.Allocating["pods"]; !ok {
t.Errorf("key pods isn't in resourceSummary.Allocating")
}
if _, ok := resourceSummary.Allocated["test"]; ok {
t.Errorf("key test is in resourceSummary.Allocated")
}
if len(resourceSummary.AllocatableModelings) != 0 {
t.Errorf("resourceSummary.AllocatableModelings isn't 0")
}
}
func TestGetClusterAllocatable(t *testing.T) {
nodes := []*corev1.Node{
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
}
expect := make(corev1.ResourceList)
expect["test"] = resource.Quantity{}
actual := getClusterAllocatable(nodes)
assert.Equal(t, expect, actual)
}
func TestGetAllocatingResource(t *testing.T) {
podList := []*corev1.Pod{
{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"test": resource.Quantity{},
}}}},
}},
}
actual := getAllocatingResource(podList)
assert.NotEmpty(t, actual["pods"])
}
func TestGetAllocatedResource(t *testing.T) {
podList := []*corev1.Pod{
{
Status: corev1.PodStatus{
Phase: corev1.PodPending,
},
Spec: corev1.PodSpec{
NodeName: "node1",
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"test": resource.Quantity{},
}}}},
}},
}
actual := getAllocatedResource(podList)
assert.NotEmpty(t, actual["pods"])
}
func TestGetNodeAvailable(t *testing.T) {
tests := []struct {
name string
allocatable corev1.ResourceList
podResources *util.Resource
expect corev1.ResourceList
}{
{
name: "resource isn't enough",
allocatable: corev1.ResourceList{
corev1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI),
},
podResources: &util.Resource{
AllowedPodNumber: 11,
},
expect: corev1.ResourceList(nil),
},
{
name: "resource is enough",
allocatable: corev1.ResourceList{
corev1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI),
},
podResources: &util.Resource{
AllowedPodNumber: 9,
},
expect: corev1.ResourceList{
corev1.ResourcePods: *resource.NewQuantity(1, resource.DecimalSI),
},
},
{
name: "podResources is nil",
allocatable: corev1.ResourceList{
corev1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI),
},
podResources: nil,
expect: corev1.ResourceList{
corev1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI),
},
},
{
name: "allocatedResourceList is nil",
allocatable: corev1.ResourceList{
corev1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI),
},
podResources: &util.Resource{
AllowedPodNumber: 0,
},
expect: corev1.ResourceList{
corev1.ResourcePods: *resource.NewQuantity(10, resource.DecimalSI),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := getNodeAvailable(tt.allocatable, tt.podResources)
assert.Equal(t, tt.expect, actual)
})
}
}
func TestGetAllocatableModelings(t *testing.T) {
tests := []struct {
name string
pods []*corev1.Pod
nodes []*corev1.Node
cluster *clusterv1alpha1.Cluster
expect []clusterv1alpha1.AllocatableModeling
}{
{
name: "suc to get AllocatableModeling",
pods: []*corev1.Pod{
{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"test": resource.Quantity{},
}}}},
}},
},
nodes: []*corev1.Node{
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
},
cluster: &clusterv1alpha1.Cluster{
Spec: clusterv1alpha1.ClusterSpec{
ResourceModels: []clusterv1alpha1.ResourceModel{
{
Grade: 0,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1024, resource.DecimalSI),
},
},
},
{
Grade: 1,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(1, resource.DecimalSI),
Max: *resource.NewQuantity(2, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(1024, resource.DecimalSI),
Max: *resource.NewQuantity(1024*2, resource.DecimalSI),
},
},
},
},
},
},
expect: []clusterv1alpha1.AllocatableModeling{
{
Grade: 0,
Count: 2,
},
{
Grade: 1,
Count: 0,
},
},
},
{
name: "cluster.Spec.ResourceModels is empty",
pods: []*corev1.Pod{
{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"test": resource.Quantity{},
}}}},
}},
},
nodes: []*corev1.Node{
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
},
cluster: &clusterv1alpha1.Cluster{
Spec: clusterv1alpha1.ClusterSpec{
ResourceModels: []clusterv1alpha1.ResourceModel{},
},
},
expect: nil,
},
{
name: "InitSummary occurs error",
pods: []*corev1.Pod{
{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"test": resource.Quantity{},
}}}},
}},
},
nodes: []*corev1.Node{
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
},
cluster: &clusterv1alpha1.Cluster{
Spec: clusterv1alpha1.ClusterSpec{
ResourceModels: []clusterv1alpha1.ResourceModel{
{
Grade: 0,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1024, resource.DecimalSI),
},
},
},
{
Grade: 1,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(1, resource.DecimalSI),
Max: *resource.NewQuantity(2, resource.DecimalSI),
},
},
},
},
},
},
expect: nil,
},
{
name: "pod.Spec.NodeName isn't 0",
pods: []*corev1.Pod{
{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"test": resource.Quantity{},
}}}},
NodeName: "node1",
},
Status: corev1.PodStatus{
Phase: corev1.PodRunning,
},
},
},
nodes: []*corev1.Node{
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
{
Status: corev1.NodeStatus{
Allocatable: corev1.ResourceList{
"test": resource.Quantity{},
},
},
},
},
cluster: &clusterv1alpha1.Cluster{
Spec: clusterv1alpha1.ClusterSpec{
ResourceModels: []clusterv1alpha1.ResourceModel{
{
Grade: 0,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1024, resource.DecimalSI),
},
},
},
{
Grade: 1,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(1, resource.DecimalSI),
Max: *resource.NewQuantity(2, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(1024, resource.DecimalSI),
Max: *resource.NewQuantity(1024*2, resource.DecimalSI),
},
},
},
},
},
},
expect: []clusterv1alpha1.AllocatableModeling{
{
Grade: 0,
Count: 2,
},
{
Grade: 1,
Count: 0,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := getAllocatableModelings(tt.cluster, tt.nodes, tt.pods)
assert.Equal(t, tt.expect, actual)
})
}
}
func TestClusterStatusController_updateStatusIfNeeded(t *testing.T) {
t.Run("cluster is in client", func(t *testing.T) {
cluster := &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "karmada",
},
Status: clusterv1alpha1.ClusterStatus{
KubernetesVersion: "v1",
},
Spec: clusterv1alpha1.ClusterSpec{
ResourceModels: []clusterv1alpha1.ResourceModel{
{
Grade: 0,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1024, resource.DecimalSI),
},
},
},
{
Grade: 1,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(1, resource.DecimalSI),
Max: *resource.NewQuantity(2, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(1024, resource.DecimalSI),
Max: *resource.NewQuantity(1024*2, resource.DecimalSI),
},
},
},
},
},
}
currentClusterStatus := clusterv1alpha1.ClusterStatus{
KubernetesVersion: "v2",
}
c := &ClusterStatusController{
Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).WithObjects(
cluster,
).WithStatusSubresource(cluster).Build(),
GenericInformerManager: genericmanager.GetInstance(),
TypedInformerManager: typedmanager.GetInstance(),
ClusterClientOption: &util.ClientOption{
QPS: 5,
Burst: 10,
},
ClusterClientSetFunc: util.NewClusterClientSet,
}
actual, err := c.updateStatusIfNeeded(cluster, currentClusterStatus)
assert.Equal(t, controllerruntime.Result{}, actual)
assert.Empty(t, err, "updateStatusIfNeeded returns error")
})
t.Run("cluster isn't in client", func(t *testing.T) {
cluster := &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "karmada",
},
Status: clusterv1alpha1.ClusterStatus{
KubernetesVersion: "v1",
},
Spec: clusterv1alpha1.ClusterSpec{
ResourceModels: []clusterv1alpha1.ResourceModel{
{
Grade: 0,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1024, resource.DecimalSI),
},
},
},
{
Grade: 1,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(1, resource.DecimalSI),
Max: *resource.NewQuantity(2, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(1024, resource.DecimalSI),
Max: *resource.NewQuantity(1024*2, resource.DecimalSI),
},
},
},
},
},
}
currentClusterStatus := clusterv1alpha1.ClusterStatus{
KubernetesVersion: "v2",
}
c := &ClusterStatusController{
Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).Build(),
GenericInformerManager: genericmanager.GetInstance(),
TypedInformerManager: typedmanager.GetInstance(),
ClusterClientOption: &util.ClientOption{
QPS: 5,
Burst: 10,
},
ClusterClientSetFunc: util.NewClusterClientSet,
}
actual, err := c.updateStatusIfNeeded(cluster, currentClusterStatus)
expect := controllerruntime.Result{Requeue: true}
assert.Equal(t, expect, actual)
assert.NotEmpty(t, err, "updateStatusIfNeeded doesn't return error")
})
}
func NewClusterDynamicClientSetForAgentWithError(_ string, _ client.Client) (*util.DynamicClusterClient, error) {
return nil, fmt.Errorf("err")
}
func TestClusterStatusController_initializeGenericInformerManagerForCluster(t *testing.T) {
t.Run("failed to create dynamicClient", func(t *testing.T) {
c := &ClusterStatusController{
Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).Build(),
GenericInformerManager: genericmanager.GetInstance(),
TypedInformerManager: typedmanager.GetInstance(),
ClusterClientOption: &util.ClientOption{
QPS: 5,
Burst: 10,
},
ClusterClientSetFunc: util.NewClusterClientSet,
ClusterDynamicClientSetFunc: NewClusterDynamicClientSetForAgentWithError,
}
clusterClientSet := &util.ClusterClient{
ClusterName: "test",
}
c.initializeGenericInformerManagerForCluster(clusterClientSet)
})
t.Run("suc to create dynamicClient", func(t *testing.T) {
c := &ClusterStatusController{
Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).Build(),
GenericInformerManager: genericmanager.GetInstance(),
TypedInformerManager: typedmanager.GetInstance(),
ClusterClientOption: &util.ClientOption{
QPS: 5,
Burst: 10,
},
ClusterClientSetFunc: util.NewClusterClientSet,
ClusterDynamicClientSetFunc: util.NewClusterDynamicClientSetForAgent,
}
clusterClientSet := &util.ClusterClient{
ClusterName: "test",
}
c.initializeGenericInformerManagerForCluster(clusterClientSet)
})
}
func TestClusterStatusController_initLeaseController(_ *testing.T) {
c := &ClusterStatusController{
Client: fake.NewClientBuilder().WithScheme(gclient.NewSchema()).Build(),
GenericInformerManager: genericmanager.GetInstance(),
TypedInformerManager: typedmanager.GetInstance(),
ClusterClientOption: &util.ClientOption{
QPS: 5,
Burst: 10,
},
ClusterClientSetFunc: util.NewClusterClientSet,
ClusterDynamicClientSetFunc: NewClusterDynamicClientSetForAgentWithError,
}
cluster := &clusterv1alpha1.Cluster{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster1",
Namespace: "karmada",
},
Status: clusterv1alpha1.ClusterStatus{
KubernetesVersion: "v1",
},
Spec: clusterv1alpha1.ClusterSpec{
ResourceModels: []clusterv1alpha1.ResourceModel{
{
Grade: 0,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(0, resource.DecimalSI),
Max: *resource.NewQuantity(1024, resource.DecimalSI),
},
},
},
{
Grade: 1,
Ranges: []clusterv1alpha1.ResourceModelRange{
{
Name: corev1.ResourceCPU,
Min: *resource.NewMilliQuantity(1, resource.DecimalSI),
Max: *resource.NewQuantity(2, resource.DecimalSI),
},
{
Name: corev1.ResourceMemory,
Min: *resource.NewMilliQuantity(1024, resource.DecimalSI),
Max: *resource.NewQuantity(1024*2, resource.DecimalSI),
},
},
},
},
},
}
c.initLeaseController(cluster)
}
func mockServer(statusCode int, existError bool) *httptest.Server {
respBody := "test"
resp := &http.Response{
StatusCode: statusCode,
Body: io.NopCloser(bytes.NewBufferString(respBody)),
}
// Create an HTTP test server to handle the request
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Write the response to the client
if existError {
statusCode := statusCode
errorMessage := "An error occurred"
http.Error(w, errorMessage, statusCode)
} else {
w.WriteHeader(resp.StatusCode)
_, err := io.Copy(w, resp.Body)
if err != nil {
fmt.Printf("failed to copy, err: %v", err)
}
}
}))
return server
}
func TestHealthEndpointCheck(t *testing.T) {
server := mockServer(http.StatusOK, false)
defer server.Close()
clusterClient := generateClusterClient(server.URL)
actual, err := healthEndpointCheck(clusterClient.KubeClient, "/readyz")
assert.Equal(t, http.StatusOK, actual)
assert.Empty(t, err)
}
func TestGetClusterHealthStatus(t *testing.T) {
t.Run("healthz return error and StatusNotFound", func(t *testing.T) {
server := mockServer(http.StatusNotFound, true)
defer server.Close()
clusterClient := generateClusterClient(server.URL)
online, healthy := getClusterHealthStatus(clusterClient)
assert.Equal(t, false, online)
assert.Equal(t, false, healthy)
})
t.Run("healthz return http.StatusOK", func(t *testing.T) {
server := mockServer(http.StatusOK, false)
defer server.Close()
clusterClient := generateClusterClient(server.URL)
online, healthy := getClusterHealthStatus(clusterClient)
assert.Equal(t, true, online)
assert.Equal(t, true, healthy)
})
t.Run("healthz return http.StatusAccepted", func(t *testing.T) {
server := mockServer(http.StatusAccepted, false)
defer server.Close()
clusterClient := generateClusterClient(server.URL)
online, healthy := getClusterHealthStatus(clusterClient)
assert.Equal(t, true, online)
assert.Equal(t, false, healthy)
})
}