...

Source file src/k8s.io/kubernetes/pkg/scheduler/util/utils_test.go

Documentation: k8s.io/kubernetes/pkg/scheduler/util

     1  /*
     2  Copyright 2017 The Kubernetes 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 util
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"syscall"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/google/go-cmp/cmp"
    28  	v1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/runtime"
    31  	"k8s.io/apimachinery/pkg/util/net"
    32  	clientsetfake "k8s.io/client-go/kubernetes/fake"
    33  	clienttesting "k8s.io/client-go/testing"
    34  	"k8s.io/klog/v2"
    35  	extenderv1 "k8s.io/kube-scheduler/extender/v1"
    36  )
    37  
    38  func TestGetPodFullName(t *testing.T) {
    39  	pod := &v1.Pod{
    40  		ObjectMeta: metav1.ObjectMeta{
    41  			Namespace: "test",
    42  			Name:      "pod",
    43  		},
    44  	}
    45  	got := GetPodFullName(pod)
    46  	expected := fmt.Sprintf("%s_%s", pod.Name, pod.Namespace)
    47  	if got != expected {
    48  		t.Errorf("Got wrong full name, got: %s, expected: %s", got, expected)
    49  	}
    50  }
    51  
    52  func newPriorityPodWithStartTime(name string, priority int32, startTime time.Time) *v1.Pod {
    53  	return &v1.Pod{
    54  		ObjectMeta: metav1.ObjectMeta{
    55  			Name: name,
    56  		},
    57  		Spec: v1.PodSpec{
    58  			Priority: &priority,
    59  		},
    60  		Status: v1.PodStatus{
    61  			StartTime: &metav1.Time{Time: startTime},
    62  		},
    63  	}
    64  }
    65  
    66  func TestGetEarliestPodStartTime(t *testing.T) {
    67  	var priority int32 = 1
    68  	currentTime := time.Now()
    69  	tests := []struct {
    70  		name              string
    71  		pods              []*v1.Pod
    72  		expectedStartTime *metav1.Time
    73  	}{
    74  		{
    75  			name:              "Pods length is 0",
    76  			pods:              []*v1.Pod{},
    77  			expectedStartTime: nil,
    78  		},
    79  		{
    80  			name: "generate new startTime",
    81  			pods: []*v1.Pod{
    82  				newPriorityPodWithStartTime("pod1", 1, currentTime.Add(-time.Second)),
    83  				{
    84  					ObjectMeta: metav1.ObjectMeta{
    85  						Name: "pod2",
    86  					},
    87  					Spec: v1.PodSpec{
    88  						Priority: &priority,
    89  					},
    90  				},
    91  			},
    92  			expectedStartTime: &metav1.Time{Time: currentTime.Add(-time.Second)},
    93  		},
    94  		{
    95  			name: "Pod with earliest start time last in the list",
    96  			pods: []*v1.Pod{
    97  				newPriorityPodWithStartTime("pod1", 1, currentTime.Add(time.Second)),
    98  				newPriorityPodWithStartTime("pod2", 2, currentTime.Add(time.Second)),
    99  				newPriorityPodWithStartTime("pod3", 2, currentTime),
   100  			},
   101  			expectedStartTime: &metav1.Time{Time: currentTime},
   102  		},
   103  		{
   104  			name: "Pod with earliest start time first in the list",
   105  			pods: []*v1.Pod{
   106  				newPriorityPodWithStartTime("pod1", 2, currentTime),
   107  				newPriorityPodWithStartTime("pod2", 2, currentTime.Add(time.Second)),
   108  				newPriorityPodWithStartTime("pod3", 2, currentTime.Add(2*time.Second)),
   109  			},
   110  			expectedStartTime: &metav1.Time{Time: currentTime},
   111  		},
   112  	}
   113  	for _, test := range tests {
   114  		t.Run(test.name, func(t *testing.T) {
   115  			startTime := GetEarliestPodStartTime(&extenderv1.Victims{Pods: test.pods})
   116  			if !startTime.Equal(test.expectedStartTime) {
   117  				t.Errorf("startTime is not the expected result,got %v, expected %v", startTime, test.expectedStartTime)
   118  			}
   119  		})
   120  	}
   121  }
   122  
   123  func TestMoreImportantPod(t *testing.T) {
   124  	currentTime := time.Now()
   125  	pod1 := newPriorityPodWithStartTime("pod1", 1, currentTime)
   126  	pod2 := newPriorityPodWithStartTime("pod2", 2, currentTime.Add(time.Second))
   127  	pod3 := newPriorityPodWithStartTime("pod3", 2, currentTime)
   128  
   129  	tests := map[string]struct {
   130  		p1       *v1.Pod
   131  		p2       *v1.Pod
   132  		expected bool
   133  	}{
   134  		"Pod with higher priority": {
   135  			p1:       pod1,
   136  			p2:       pod2,
   137  			expected: false,
   138  		},
   139  		"Pod with older created time": {
   140  			p1:       pod2,
   141  			p2:       pod3,
   142  			expected: false,
   143  		},
   144  		"Pods with same start time": {
   145  			p1:       pod3,
   146  			p2:       pod1,
   147  			expected: true,
   148  		},
   149  	}
   150  
   151  	for k, v := range tests {
   152  		t.Run(k, func(t *testing.T) {
   153  			got := MoreImportantPod(v.p1, v.p2)
   154  			if got != v.expected {
   155  				t.Errorf("expected %t but got %t", v.expected, got)
   156  			}
   157  		})
   158  	}
   159  }
   160  
   161  func TestRemoveNominatedNodeName(t *testing.T) {
   162  	tests := []struct {
   163  		name                     string
   164  		currentNominatedNodeName string
   165  		newNominatedNodeName     string
   166  		expectedPatchRequests    int
   167  		expectedPatchData        string
   168  	}{
   169  		{
   170  			name:                     "Should make patch request to clear node name",
   171  			currentNominatedNodeName: "node1",
   172  			expectedPatchRequests:    1,
   173  			expectedPatchData:        `{"status":{"nominatedNodeName":null}}`,
   174  		},
   175  		{
   176  			name:                     "Should not make patch request if nominated node is already cleared",
   177  			currentNominatedNodeName: "",
   178  			expectedPatchRequests:    0,
   179  		},
   180  	}
   181  	for _, test := range tests {
   182  		t.Run(test.name, func(t *testing.T) {
   183  			actualPatchRequests := 0
   184  			var actualPatchData string
   185  			cs := &clientsetfake.Clientset{}
   186  			cs.AddReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) {
   187  				actualPatchRequests++
   188  				patch := action.(clienttesting.PatchAction)
   189  				actualPatchData = string(patch.GetPatch())
   190  				// For this test, we don't care about the result of the patched pod, just that we got the expected
   191  				// patch request, so just returning &v1.Pod{} here is OK because scheduler doesn't use the response.
   192  				return true, &v1.Pod{}, nil
   193  			})
   194  
   195  			pod := &v1.Pod{
   196  				ObjectMeta: metav1.ObjectMeta{Name: "foo"},
   197  				Status:     v1.PodStatus{NominatedNodeName: test.currentNominatedNodeName},
   198  			}
   199  
   200  			ctx, cancel := context.WithCancel(context.Background())
   201  			defer cancel()
   202  			if err := ClearNominatedNodeName(ctx, cs, pod); err != nil {
   203  				t.Fatalf("Error calling removeNominatedNodeName: %v", err)
   204  			}
   205  
   206  			if actualPatchRequests != test.expectedPatchRequests {
   207  				t.Fatalf("Actual patch requests (%d) dos not equal expected patch requests (%d)", actualPatchRequests, test.expectedPatchRequests)
   208  			}
   209  
   210  			if test.expectedPatchRequests > 0 && actualPatchData != test.expectedPatchData {
   211  				t.Fatalf("Patch data mismatch: Actual was %v, but expected %v", actualPatchData, test.expectedPatchData)
   212  			}
   213  		})
   214  	}
   215  }
   216  
   217  func TestPatchPodStatus(t *testing.T) {
   218  	tests := []struct {
   219  		name   string
   220  		pod    v1.Pod
   221  		client *clientsetfake.Clientset
   222  		// validateErr checks if error returned from PatchPodStatus is expected one or not.
   223  		// (true means error is expected one.)
   224  		validateErr    func(goterr error) bool
   225  		statusToUpdate v1.PodStatus
   226  	}{
   227  		{
   228  			name:   "Should update pod conditions successfully",
   229  			client: clientsetfake.NewSimpleClientset(),
   230  			pod: v1.Pod{
   231  				ObjectMeta: metav1.ObjectMeta{
   232  					Namespace: "ns",
   233  					Name:      "pod1",
   234  				},
   235  				Spec: v1.PodSpec{
   236  					ImagePullSecrets: []v1.LocalObjectReference{{Name: "foo"}},
   237  				},
   238  			},
   239  			statusToUpdate: v1.PodStatus{
   240  				Conditions: []v1.PodCondition{
   241  					{
   242  						Type:   v1.PodScheduled,
   243  						Status: v1.ConditionFalse,
   244  					},
   245  				},
   246  			},
   247  		},
   248  		{
   249  			// ref: #101697, #94626 - ImagePullSecrets are allowed to have empty secret names
   250  			// which would fail the 2-way merge patch generation on Pod patches
   251  			// due to the mergeKey being the name field
   252  			name:   "Should update pod conditions successfully on a pod Spec with secrets with empty name",
   253  			client: clientsetfake.NewSimpleClientset(),
   254  			pod: v1.Pod{
   255  				ObjectMeta: metav1.ObjectMeta{
   256  					Namespace: "ns",
   257  					Name:      "pod1",
   258  				},
   259  				Spec: v1.PodSpec{
   260  					// this will serialize to imagePullSecrets:[{}]
   261  					ImagePullSecrets: make([]v1.LocalObjectReference, 1),
   262  				},
   263  			},
   264  			statusToUpdate: v1.PodStatus{
   265  				Conditions: []v1.PodCondition{
   266  					{
   267  						Type:   v1.PodScheduled,
   268  						Status: v1.ConditionFalse,
   269  					},
   270  				},
   271  			},
   272  		},
   273  		{
   274  			name: "retry patch request when an 'connection refused' error is returned",
   275  			client: func() *clientsetfake.Clientset {
   276  				client := clientsetfake.NewSimpleClientset()
   277  
   278  				reqcount := 0
   279  				client.PrependReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) {
   280  					defer func() { reqcount++ }()
   281  					if reqcount == 0 {
   282  						// return an connection refused error for the first patch request.
   283  						return true, &v1.Pod{}, fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED)
   284  					}
   285  					if reqcount == 1 {
   286  						// not return error for the second patch request.
   287  						return false, &v1.Pod{}, nil
   288  					}
   289  
   290  					// return error if requests comes in more than three times.
   291  					return true, nil, errors.New("requests comes in more than three times.")
   292  				})
   293  
   294  				return client
   295  			}(),
   296  			pod: v1.Pod{
   297  				ObjectMeta: metav1.ObjectMeta{
   298  					Namespace: "ns",
   299  					Name:      "pod1",
   300  				},
   301  				Spec: v1.PodSpec{
   302  					ImagePullSecrets: []v1.LocalObjectReference{{Name: "foo"}},
   303  				},
   304  			},
   305  			statusToUpdate: v1.PodStatus{
   306  				Conditions: []v1.PodCondition{
   307  					{
   308  						Type:   v1.PodScheduled,
   309  						Status: v1.ConditionFalse,
   310  					},
   311  				},
   312  			},
   313  		},
   314  		{
   315  			name: "only 4 retries at most",
   316  			client: func() *clientsetfake.Clientset {
   317  				client := clientsetfake.NewSimpleClientset()
   318  
   319  				reqcount := 0
   320  				client.PrependReactor("patch", "pods", func(action clienttesting.Action) (bool, runtime.Object, error) {
   321  					defer func() { reqcount++ }()
   322  					if reqcount >= 4 {
   323  						// return error if requests comes in more than four times.
   324  						return true, nil, errors.New("requests comes in more than four times.")
   325  					}
   326  
   327  					// return an connection refused error for the first patch request.
   328  					return true, &v1.Pod{}, fmt.Errorf("connection refused: %w", syscall.ECONNREFUSED)
   329  				})
   330  
   331  				return client
   332  			}(),
   333  			pod: v1.Pod{
   334  				ObjectMeta: metav1.ObjectMeta{
   335  					Namespace: "ns",
   336  					Name:      "pod1",
   337  				},
   338  				Spec: v1.PodSpec{
   339  					ImagePullSecrets: []v1.LocalObjectReference{{Name: "foo"}},
   340  				},
   341  			},
   342  			validateErr: net.IsConnectionRefused,
   343  			statusToUpdate: v1.PodStatus{
   344  				Conditions: []v1.PodCondition{
   345  					{
   346  						Type:   v1.PodScheduled,
   347  						Status: v1.ConditionFalse,
   348  					},
   349  				},
   350  			},
   351  		},
   352  	}
   353  
   354  	for _, tc := range tests {
   355  		t.Run(tc.name, func(t *testing.T) {
   356  			client := tc.client
   357  			_, err := client.CoreV1().Pods(tc.pod.Namespace).Create(context.TODO(), &tc.pod, metav1.CreateOptions{})
   358  			if err != nil {
   359  				t.Fatal(err)
   360  			}
   361  
   362  			ctx, cancel := context.WithCancel(context.Background())
   363  			defer cancel()
   364  			err = PatchPodStatus(ctx, client, &tc.pod, &tc.statusToUpdate)
   365  			if err != nil && tc.validateErr == nil {
   366  				// shouldn't be error
   367  				t.Fatal(err)
   368  			}
   369  			if tc.validateErr != nil {
   370  				if !tc.validateErr(err) {
   371  					t.Fatalf("Returned unexpected error: %v", err)
   372  				}
   373  				return
   374  			}
   375  
   376  			retrievedPod, err := client.CoreV1().Pods(tc.pod.Namespace).Get(ctx, tc.pod.Name, metav1.GetOptions{})
   377  			if err != nil {
   378  				t.Fatal(err)
   379  			}
   380  
   381  			if diff := cmp.Diff(tc.statusToUpdate, retrievedPod.Status); diff != "" {
   382  				t.Errorf("unexpected pod status (-want,+got):\n%s", diff)
   383  			}
   384  		})
   385  	}
   386  }
   387  
   388  // Test_As tests the As function with Pod.
   389  func Test_As_Pod(t *testing.T) {
   390  	tests := []struct {
   391  		name       string
   392  		oldObj     interface{}
   393  		newObj     interface{}
   394  		wantOldObj *v1.Pod
   395  		wantNewObj *v1.Pod
   396  		wantErr    bool
   397  	}{
   398  		{
   399  			name:       "nil old Pod",
   400  			oldObj:     nil,
   401  			newObj:     &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   402  			wantOldObj: nil,
   403  			wantNewObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   404  		},
   405  		{
   406  			name:       "nil new Pod",
   407  			oldObj:     &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   408  			newObj:     nil,
   409  			wantOldObj: &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   410  			wantNewObj: nil,
   411  		},
   412  		{
   413  			name:    "two different kinds of objects",
   414  			oldObj:  &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   415  			newObj:  &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   416  			wantErr: true,
   417  		},
   418  	}
   419  
   420  	for _, tc := range tests {
   421  		t.Run(tc.name, func(t *testing.T) {
   422  			gotOld, gotNew, err := As[*v1.Pod](tc.oldObj, tc.newObj)
   423  			if err != nil && !tc.wantErr {
   424  				t.Fatalf("unexpected error: %v", err)
   425  			}
   426  			if tc.wantErr {
   427  				if err == nil {
   428  					t.Fatalf("expected error, but got nil")
   429  				}
   430  				return
   431  			}
   432  
   433  			if diff := cmp.Diff(tc.wantOldObj, gotOld); diff != "" {
   434  				t.Errorf("unexpected old object (-want,+got):\n%s", diff)
   435  			}
   436  			if diff := cmp.Diff(tc.wantNewObj, gotNew); diff != "" {
   437  				t.Errorf("unexpected new object (-want,+got):\n%s", diff)
   438  			}
   439  		})
   440  	}
   441  }
   442  
   443  // Test_As_Node tests the As function with Node.
   444  func Test_As_Node(t *testing.T) {
   445  	tests := []struct {
   446  		name       string
   447  		oldObj     interface{}
   448  		newObj     interface{}
   449  		wantOldObj *v1.Node
   450  		wantNewObj *v1.Node
   451  		wantErr    bool
   452  	}{
   453  		{
   454  			name:       "nil old Node",
   455  			oldObj:     nil,
   456  			newObj:     &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   457  			wantOldObj: nil,
   458  			wantNewObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   459  		},
   460  		{
   461  			name:       "nil new Node",
   462  			oldObj:     &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   463  			newObj:     nil,
   464  			wantOldObj: &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   465  			wantNewObj: nil,
   466  		},
   467  	}
   468  
   469  	for _, tc := range tests {
   470  		t.Run(tc.name, func(t *testing.T) {
   471  			gotOld, gotNew, err := As[*v1.Node](tc.oldObj, tc.newObj)
   472  			if err != nil && !tc.wantErr {
   473  				t.Fatalf("unexpected error: %v", err)
   474  			}
   475  			if tc.wantErr {
   476  				if err == nil {
   477  					t.Fatalf("expected error, but got nil")
   478  				}
   479  				return
   480  			}
   481  
   482  			if diff := cmp.Diff(tc.wantOldObj, gotOld); diff != "" {
   483  				t.Errorf("unexpected old object (-want,+got):\n%s", diff)
   484  			}
   485  			if diff := cmp.Diff(tc.wantNewObj, gotNew); diff != "" {
   486  				t.Errorf("unexpected new object (-want,+got):\n%s", diff)
   487  			}
   488  		})
   489  	}
   490  }
   491  
   492  // Test_As_KMetadata tests the As function with Pod.
   493  func Test_As_KMetadata(t *testing.T) {
   494  	tests := []struct {
   495  		name    string
   496  		oldObj  interface{}
   497  		newObj  interface{}
   498  		wantErr bool
   499  	}{
   500  		{
   501  			name:    "nil old Pod",
   502  			oldObj:  nil,
   503  			newObj:  &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   504  			wantErr: false,
   505  		},
   506  		{
   507  			name:    "nil new Pod",
   508  			oldObj:  &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   509  			newObj:  nil,
   510  			wantErr: false,
   511  		},
   512  		{
   513  			name:    "two different kinds of objects",
   514  			oldObj:  &v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   515  			newObj:  &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "foo"}},
   516  			wantErr: false,
   517  		},
   518  		{
   519  			name:    "unknown old type",
   520  			oldObj:  "unknown type",
   521  			wantErr: true,
   522  		},
   523  		{
   524  			name:    "unknown new type",
   525  			newObj:  "unknown type",
   526  			wantErr: true,
   527  		},
   528  	}
   529  
   530  	for _, tc := range tests {
   531  		t.Run(tc.name, func(t *testing.T) {
   532  			_, _, err := As[klog.KMetadata](tc.oldObj, tc.newObj)
   533  			if err != nil && !tc.wantErr {
   534  				t.Fatalf("unexpected error: %v", err)
   535  			}
   536  			if tc.wantErr {
   537  				if err == nil {
   538  					t.Fatalf("expected error, but got nil")
   539  				}
   540  				return
   541  			}
   542  		})
   543  	}
   544  }
   545  

View as plain text