github.com/kubewharf/katalyst-core@v0.5.3/pkg/controller/tide/tide_test.go (about)

     1  /*
     2  Copyright 2022 The Katalyst Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tide
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"testing"
    23  
    24  	"github.com/stretchr/testify/assert"
    25  	corev1 "k8s.io/api/core/v1"
    26  	"k8s.io/apimachinery/pkg/api/resource"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/labels"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/types"
    31  	"k8s.io/apimachinery/pkg/util/intstr"
    32  	"k8s.io/client-go/tools/cache"
    33  
    34  	v1alpha12 "github.com/kubewharf/katalyst-api/pkg/apis/tide/v1alpha1"
    35  	katalystbase "github.com/kubewharf/katalyst-core/cmd/base"
    36  )
    37  
    38  func TestTide_RunOnce(t1 *testing.T) {
    39  	t1.Parallel()
    40  
    41  	nodePool := &v1alpha12.TideNodePool{
    42  		ObjectMeta: metav1.ObjectMeta{
    43  			Name: "np1",
    44  		},
    45  		Spec: v1alpha12.TideNodePoolSpec{
    46  			NodeConfigs: v1alpha12.NodeConfigs{
    47  				NodeSelector: map[string]string{"test": "test"},
    48  			},
    49  		},
    50  	}
    51  	type args struct {
    52  		ctx          context.Context
    53  		nodeList     []runtime.Object
    54  		podList      []runtime.Object
    55  		tideNodePool *v1alpha12.TideNodePool
    56  	}
    57  	tests := []struct {
    58  		name                 string
    59  		args                 args
    60  		wantOnlineNodeCount  int
    61  		wantOfflineNodeCount int
    62  	}{
    63  		{
    64  			name: "release empty node to offline",
    65  			args: args{
    66  				ctx: context.Background(),
    67  				nodeList: []runtime.Object{
    68  					buildTideNode(NewNodePoolWrapper(nodePool.DeepCopy()), "n1", 1000, 1000, true),
    69  					buildTideNode(NewNodePoolWrapper(nodePool.DeepCopy()), "n2", 1000, 1000, true),
    70  				},
    71  				podList:      nil,
    72  				tideNodePool: nodePool.DeepCopy(),
    73  			},
    74  			wantOnlineNodeCount:  1,
    75  			wantOfflineNodeCount: 1,
    76  		},
    77  		{
    78  			name: "release empty node to online",
    79  			args: args{
    80  				ctx: context.Background(),
    81  				nodeList: []runtime.Object{
    82  					buildTideNode(NewNodePoolWrapper(nodePool.DeepCopy()), "n3", 1000, 1000, false),
    83  					buildTideNode(NewNodePoolWrapper(nodePool.DeepCopy()), "n4", 1000, 1000, false),
    84  				},
    85  				podList:      []runtime.Object{buildOnlinePod(NewNodePoolWrapper(nodePool.DeepCopy()), "p1", 500, 500)},
    86  				tideNodePool: nodePool.DeepCopy(),
    87  			},
    88  			wantOnlineNodeCount:  1,
    89  			wantOfflineNodeCount: 1,
    90  		},
    91  	}
    92  	for _, tt := range tests {
    93  		tt := tt
    94  		t1.Run(tt.name, func(t1 *testing.T) {
    95  			t1.Parallel()
    96  
    97  			controlCtx, err := katalystbase.GenerateFakeGenericContext(append(tt.args.nodeList, tt.args.podList...))
    98  			if err != nil {
    99  				t1.Error(err)
   100  			}
   101  			t, err := NewTide(tt.args.ctx, controlCtx, nil, nil)
   102  			if err != nil {
   103  				t1.Error(err)
   104  			}
   105  			controlCtx.StartInformer(tt.args.ctx)
   106  			syncd := cache.WaitForCacheSync(tt.args.ctx.Done(), t.nodeListerSynced, t.tideListerSynced, t.podListerSynced)
   107  			assert.True(t1, syncd)
   108  			wrapper := NewNodePoolWrapper(tt.args.tideNodePool)
   109  			checker := func(pod *corev1.Pod) bool {
   110  				return labels.SelectorFromSet(map[string]string{LabelPodTypeKey: LabelOnlinePodValue}).Matches(labels.Set(pod.GetLabels()))
   111  			}
   112  			t.RunOnce(tt.args.ctx, checker, wrapper)
   113  			nodes, err := t.client.KubeClient.CoreV1().Nodes().List(tt.args.ctx, metav1.ListOptions{})
   114  			if err != nil {
   115  				t1.Error(err)
   116  			}
   117  			onlineNodes, offlineNodes := 0, 0
   118  			for _, node := range nodes.Items {
   119  				fmt.Println(node.Labels)
   120  				if wrapper.GetOnlineTideNodeSelector().Matches(labels.Set(node.Labels)) {
   121  					onlineNodes++
   122  				}
   123  				if wrapper.GetOfflineTideNodeSelector().Matches(labels.Set(node.Labels)) {
   124  					offlineNodes++
   125  				}
   126  			}
   127  			fmt.Println(onlineNodes, offlineNodes)
   128  			assert.Equal(t1, onlineNodes, tt.wantOnlineNodeCount)
   129  			assert.Equal(t1, offlineNodes, tt.wantOfflineNodeCount)
   130  		})
   131  	}
   132  }
   133  
   134  func buildNode(nodePool NodePoolWrapper, name string, millicpu int64, mem int64) *corev1.Node {
   135  	node := &corev1.Node{
   136  		ObjectMeta: metav1.ObjectMeta{
   137  			Name:     name,
   138  			SelfLink: fmt.Sprintf("/api/v1/nodes/%s", name),
   139  			Labels:   nodePool.GetNodeSelector(),
   140  		},
   141  		Spec: corev1.NodeSpec{
   142  			ProviderID: name,
   143  		},
   144  		Status: corev1.NodeStatus{
   145  			Capacity: corev1.ResourceList{
   146  				corev1.ResourcePods: *resource.NewQuantity(100, resource.DecimalSI),
   147  			},
   148  		},
   149  	}
   150  
   151  	if millicpu >= 0 {
   152  		node.Status.Capacity[corev1.ResourceCPU] = *resource.NewMilliQuantity(millicpu, resource.DecimalSI)
   153  	}
   154  	if mem >= 0 {
   155  		node.Status.Capacity[corev1.ResourceMemory] = *resource.NewQuantity(mem, resource.DecimalSI)
   156  	}
   157  
   158  	node.Status.Allocatable = corev1.ResourceList{}
   159  	for k, v := range node.Status.Capacity {
   160  		node.Status.Allocatable[k] = v
   161  	}
   162  
   163  	return node
   164  }
   165  
   166  // buildTideNode creates a tide node with specified capacity.
   167  func buildTideNode(nodePool NodePoolWrapper, name string, millicpu int64, mem int64, isOnline bool) *corev1.Node {
   168  	node := buildNode(nodePool, name, millicpu, mem)
   169  	node.Labels[nodePool.GetTideLabel().Key] = nodePool.GetTideLabel().Value
   170  	node.Labels[LabelNodePoolKey] = nodePool.GetName()
   171  	if isOnline {
   172  		node.Labels[nodePool.GetOnlineLabel().Key] = nodePool.GetOnlineLabel().Value
   173  		node.Spec.Taints = []corev1.Taint{
   174  			{
   175  				Key:    nodePool.GetEvictOfflinePodTaint().Key,
   176  				Value:  nodePool.GetEvictOfflinePodTaint().Value,
   177  				Effect: corev1.TaintEffect(nodePool.GetEvictOfflinePodTaint().Effect),
   178  			},
   179  		}
   180  	} else {
   181  		node.Labels[nodePool.GetOfflineLabel().Key] = nodePool.GetOfflineLabel().Value
   182  		node.Spec.Taints = []corev1.Taint{
   183  			{
   184  				Key:    nodePool.GetEvictOnlinePodTaint().Key,
   185  				Value:  nodePool.GetEvictOnlinePodTaint().Value,
   186  				Effect: corev1.TaintEffect(nodePool.GetEvictOnlinePodTaint().Effect),
   187  			},
   188  		}
   189  	}
   190  
   191  	return node
   192  }
   193  
   194  func buildOnlinePod(nodePool NodePoolWrapper, name string, cpu int64, mem int64) *corev1.Pod {
   195  	startTime := metav1.Unix(0, 0)
   196  	pod := &corev1.Pod{
   197  		ObjectMeta: metav1.ObjectMeta{
   198  			UID:         types.UID(name),
   199  			Namespace:   "default",
   200  			Name:        name,
   201  			SelfLink:    fmt.Sprintf("/api/v1/namespaces/default/pods/%s", name),
   202  			Annotations: map[string]string{},
   203  			Labels:      map[string]string{LabelPodTypeKey: LabelOnlinePodValue},
   204  		},
   205  		Spec: corev1.PodSpec{
   206  			Containers: []corev1.Container{
   207  				{
   208  					Resources: corev1.ResourceRequirements{
   209  						Requests: corev1.ResourceList{},
   210  					},
   211  				},
   212  			},
   213  			Tolerations: []corev1.Toleration{
   214  				{
   215  					Key:               nodePool.GetEvictOfflinePodTaint().Key,
   216  					Operator:          corev1.TolerationOpEqual,
   217  					Value:             nodePool.GetEvictOfflinePodTaint().Value,
   218  					Effect:            corev1.TaintEffect(nodePool.GetEvictOfflinePodTaint().Effect),
   219  					TolerationSeconds: nil,
   220  				},
   221  			},
   222  		},
   223  		Status: corev1.PodStatus{
   224  			StartTime: &startTime,
   225  			Conditions: []corev1.PodCondition{
   226  				{
   227  					Type:               corev1.PodScheduled,
   228  					Status:             corev1.ConditionFalse,
   229  					LastProbeTime:      metav1.Time{},
   230  					LastTransitionTime: metav1.Time{},
   231  					Reason:             corev1.PodReasonUnschedulable,
   232  					Message:            "",
   233  				},
   234  			},
   235  		},
   236  	}
   237  
   238  	if cpu >= 0 {
   239  		pod.Spec.Containers[0].Resources.Requests[corev1.ResourceCPU] = *resource.NewMilliQuantity(cpu, resource.DecimalSI)
   240  	}
   241  	if mem >= 0 {
   242  		pod.Spec.Containers[0].Resources.Requests[corev1.ResourceMemory] = *resource.NewQuantity(mem, resource.DecimalSI)
   243  	}
   244  
   245  	return pod
   246  }
   247  
   248  func TestTide_Reconcile(t1 *testing.T) {
   249  	t1.Parallel()
   250  
   251  	nodeReserve := intstr.FromString("30%")
   252  	nodePool := &v1alpha12.TideNodePool{
   253  		ObjectMeta: metav1.ObjectMeta{
   254  			Name: "np1",
   255  		},
   256  		Spec: v1alpha12.TideNodePoolSpec{
   257  			NodeConfigs: v1alpha12.NodeConfigs{
   258  				NodeSelector: map[string]string{"test": "test"},
   259  				Reserve: v1alpha12.ReserveOptions{
   260  					Online:  &nodeReserve,
   261  					Offline: &nodeReserve,
   262  				},
   263  			},
   264  		},
   265  	}
   266  	type args struct {
   267  		ctx          context.Context
   268  		nodeList     []runtime.Object
   269  		podList      []runtime.Object
   270  		tideNodePool *v1alpha12.TideNodePool
   271  	}
   272  	tests := []struct {
   273  		name string
   274  		args args
   275  
   276  		wantReserveOnlineNodeCount  int
   277  		wantReserveOfflineNodeCount int
   278  		wantReserveTideNodeCount    int
   279  	}{
   280  		{
   281  			name: "normal",
   282  			args: args{
   283  				ctx: context.Background(),
   284  				nodeList: []runtime.Object{
   285  					buildNode(NewNodePoolWrapper(nodePool), "n5", 1000, 1000),
   286  					buildNode(NewNodePoolWrapper(nodePool), "n6", 1000, 1000),
   287  					buildNode(NewNodePoolWrapper(nodePool), "n7", 1000, 1000),
   288  					buildNode(NewNodePoolWrapper(nodePool), "n8", 1000, 1000),
   289  				},
   290  				podList:      nil,
   291  				tideNodePool: nodePool,
   292  			},
   293  			wantReserveOnlineNodeCount:  2,
   294  			wantReserveOfflineNodeCount: 1,
   295  			wantReserveTideNodeCount:    1,
   296  		},
   297  	}
   298  	for _, tt := range tests {
   299  		tt := tt
   300  		t1.Run(tt.name, func(t1 *testing.T) {
   301  			t1.Parallel()
   302  
   303  			controlCtx, err := katalystbase.GenerateFakeGenericContext(append(tt.args.nodeList, tt.args.podList...), []runtime.Object{tt.args.tideNodePool})
   304  			if err != nil {
   305  				t1.Error(err)
   306  			}
   307  			t, err := NewTide(tt.args.ctx, controlCtx, nil, nil)
   308  			if err != nil {
   309  				t1.Error(err)
   310  			}
   311  			controlCtx.StartInformer(tt.args.ctx)
   312  			syncd := cache.WaitForCacheSync(tt.args.ctx.Done(), t.nodeListerSynced, t.tideListerSynced, t.podListerSynced)
   313  			assert.True(t1, syncd)
   314  			t.Reconcile(tt.args.ctx, tt.args.tideNodePool)
   315  			syncd = cache.WaitForCacheSync(tt.args.ctx.Done(), t.nodeListerSynced, t.tideListerSynced, t.podListerSynced)
   316  			assert.True(t1, syncd)
   317  			tideNodePool, err := t.client.InternalClient.TideV1alpha1().TideNodePools().Get(tt.args.ctx, tt.args.tideNodePool.Name, metav1.GetOptions{})
   318  			if err != nil {
   319  				t1.Error(err)
   320  			}
   321  			assert.Equal(t1, len(tideNodePool.Status.ReserveNodes.OnlineNodes), tt.wantReserveOnlineNodeCount)
   322  			assert.Equal(t1, len(tideNodePool.Status.ReserveNodes.OfflineNodes), tt.wantReserveOfflineNodeCount)
   323  			assert.Equal(t1, len(tideNodePool.Status.TideNodes.Nodes), tt.wantReserveTideNodeCount)
   324  		})
   325  	}
   326  }