k8s.io/kubernetes@v1.29.3/pkg/scheduler/internal/cache/node_tree_test.go (about)

     1  /*
     2  Copyright 2018 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 cache
    18  
    19  import (
    20  	"testing"
    21  
    22  	"github.com/google/go-cmp/cmp"
    23  
    24  	v1 "k8s.io/api/core/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/klog/v2/ktesting"
    27  )
    28  
    29  var allNodes = []*v1.Node{
    30  	// Node 0: a node without any region-zone label
    31  	{
    32  		ObjectMeta: metav1.ObjectMeta{
    33  			Name: "node-0",
    34  		},
    35  	},
    36  	// Node 1: a node with region label only
    37  	{
    38  		ObjectMeta: metav1.ObjectMeta{
    39  			Name: "node-1",
    40  			Labels: map[string]string{
    41  				v1.LabelTopologyRegion: "region-1",
    42  			},
    43  		},
    44  	},
    45  	// Node 2: a node with zone label only
    46  	{
    47  		ObjectMeta: metav1.ObjectMeta{
    48  			Name: "node-2",
    49  			Labels: map[string]string{
    50  				v1.LabelTopologyZone: "zone-2",
    51  			},
    52  		},
    53  	},
    54  	// Node 3: a node with proper region and zone labels
    55  	{
    56  		ObjectMeta: metav1.ObjectMeta{
    57  			Name: "node-3",
    58  			Labels: map[string]string{
    59  				v1.LabelTopologyRegion: "region-1",
    60  				v1.LabelTopologyZone:   "zone-2",
    61  			},
    62  		},
    63  	},
    64  	// Node 4: a node with proper region and zone labels
    65  	{
    66  		ObjectMeta: metav1.ObjectMeta{
    67  			Name: "node-4",
    68  			Labels: map[string]string{
    69  				v1.LabelTopologyRegion: "region-1",
    70  				v1.LabelTopologyZone:   "zone-2",
    71  			},
    72  		},
    73  	},
    74  	// Node 5: a node with proper region and zone labels in a different zone, same region as above
    75  	{
    76  		ObjectMeta: metav1.ObjectMeta{
    77  			Name: "node-5",
    78  			Labels: map[string]string{
    79  				v1.LabelTopologyRegion: "region-1",
    80  				v1.LabelTopologyZone:   "zone-3",
    81  			},
    82  		},
    83  	},
    84  	// Node 6: a node with proper region and zone labels in a new region and zone
    85  	{
    86  		ObjectMeta: metav1.ObjectMeta{
    87  			Name: "node-6",
    88  			Labels: map[string]string{
    89  				v1.LabelTopologyRegion: "region-2",
    90  				v1.LabelTopologyZone:   "zone-2",
    91  			},
    92  		},
    93  	},
    94  	// Node 7: a node with proper region and zone labels in a region and zone as node-6
    95  	{
    96  		ObjectMeta: metav1.ObjectMeta{
    97  			Name: "node-7",
    98  			Labels: map[string]string{
    99  				v1.LabelTopologyRegion: "region-2",
   100  				v1.LabelTopologyZone:   "zone-2",
   101  			},
   102  		},
   103  	},
   104  	// Node 8: a node with proper region and zone labels in a region and zone as node-6
   105  	{
   106  		ObjectMeta: metav1.ObjectMeta{
   107  			Name: "node-8",
   108  			Labels: map[string]string{
   109  				v1.LabelTopologyRegion: "region-2",
   110  				v1.LabelTopologyZone:   "zone-2",
   111  			},
   112  		},
   113  	},
   114  	// Node 9: a node with zone + region label and the deprecated zone + region label
   115  	{
   116  		ObjectMeta: metav1.ObjectMeta{
   117  			Name: "node-9",
   118  			Labels: map[string]string{
   119  				v1.LabelTopologyRegion:          "region-2",
   120  				v1.LabelTopologyZone:            "zone-2",
   121  				v1.LabelFailureDomainBetaRegion: "region-2",
   122  				v1.LabelFailureDomainBetaZone:   "zone-2",
   123  			},
   124  		},
   125  	},
   126  	// Node 10: a node with only the deprecated zone + region labels
   127  	{
   128  		ObjectMeta: metav1.ObjectMeta{
   129  			Name: "node-10",
   130  			Labels: map[string]string{
   131  				v1.LabelFailureDomainBetaRegion: "region-2",
   132  				v1.LabelFailureDomainBetaZone:   "zone-3",
   133  			},
   134  		},
   135  	},
   136  }
   137  
   138  func verifyNodeTree(t *testing.T, nt *nodeTree, expectedTree map[string][]string) {
   139  	expectedNumNodes := int(0)
   140  	for _, na := range expectedTree {
   141  		expectedNumNodes += len(na)
   142  	}
   143  	if numNodes := nt.numNodes; numNodes != expectedNumNodes {
   144  		t.Errorf("unexpected nodeTree.numNodes. Expected: %v, Got: %v", expectedNumNodes, numNodes)
   145  	}
   146  	if diff := cmp.Diff(expectedTree, nt.tree); diff != "" {
   147  		t.Errorf("Unexpected node tree (-want, +got):\n%s", diff)
   148  	}
   149  	if len(nt.zones) != len(expectedTree) {
   150  		t.Errorf("Number of zones in nodeTree.zones is not expected. Expected: %v, Got: %v", len(expectedTree), len(nt.zones))
   151  	}
   152  	for _, z := range nt.zones {
   153  		if _, ok := expectedTree[z]; !ok {
   154  			t.Errorf("zone %v is not expected to exist in nodeTree.zones", z)
   155  		}
   156  	}
   157  }
   158  
   159  func TestNodeTree_AddNode(t *testing.T) {
   160  	tests := []struct {
   161  		name         string
   162  		nodesToAdd   []*v1.Node
   163  		expectedTree map[string][]string
   164  	}{
   165  		{
   166  			name:         "single node no labels",
   167  			nodesToAdd:   allNodes[:1],
   168  			expectedTree: map[string][]string{"": {"node-0"}},
   169  		},
   170  		{
   171  			name:         "same node specified twice",
   172  			nodesToAdd:   []*v1.Node{allNodes[0], allNodes[0]},
   173  			expectedTree: map[string][]string{"": {"node-0"}},
   174  		},
   175  		{
   176  			name:       "mix of nodes with and without proper labels",
   177  			nodesToAdd: allNodes[:4],
   178  			expectedTree: map[string][]string{
   179  				"":                     {"node-0"},
   180  				"region-1:\x00:":       {"node-1"},
   181  				":\x00:zone-2":         {"node-2"},
   182  				"region-1:\x00:zone-2": {"node-3"},
   183  			},
   184  		},
   185  		{
   186  			name:       "mix of nodes with and without proper labels and some zones with multiple nodes",
   187  			nodesToAdd: allNodes[:7],
   188  			expectedTree: map[string][]string{
   189  				"":                     {"node-0"},
   190  				"region-1:\x00:":       {"node-1"},
   191  				":\x00:zone-2":         {"node-2"},
   192  				"region-1:\x00:zone-2": {"node-3", "node-4"},
   193  				"region-1:\x00:zone-3": {"node-5"},
   194  				"region-2:\x00:zone-2": {"node-6"},
   195  			},
   196  		},
   197  		{
   198  			name:       "nodes also using deprecated zone/region label",
   199  			nodesToAdd: allNodes[9:],
   200  			expectedTree: map[string][]string{
   201  				"region-2:\x00:zone-2": {"node-9"},
   202  				"region-2:\x00:zone-3": {"node-10"},
   203  			},
   204  		},
   205  	}
   206  
   207  	for _, test := range tests {
   208  		t.Run(test.name, func(t *testing.T) {
   209  			logger, _ := ktesting.NewTestContext(t)
   210  			nt := newNodeTree(logger, nil)
   211  			for _, n := range test.nodesToAdd {
   212  				nt.addNode(logger, n)
   213  			}
   214  			verifyNodeTree(t, nt, test.expectedTree)
   215  		})
   216  	}
   217  }
   218  
   219  func TestNodeTree_RemoveNode(t *testing.T) {
   220  	tests := []struct {
   221  		name          string
   222  		existingNodes []*v1.Node
   223  		nodesToRemove []*v1.Node
   224  		expectedTree  map[string][]string
   225  		expectError   bool
   226  	}{
   227  		{
   228  			name:          "remove a single node with no labels",
   229  			existingNodes: allNodes[:7],
   230  			nodesToRemove: allNodes[:1],
   231  			expectedTree: map[string][]string{
   232  				"region-1:\x00:":       {"node-1"},
   233  				":\x00:zone-2":         {"node-2"},
   234  				"region-1:\x00:zone-2": {"node-3", "node-4"},
   235  				"region-1:\x00:zone-3": {"node-5"},
   236  				"region-2:\x00:zone-2": {"node-6"},
   237  			},
   238  		},
   239  		{
   240  			name:          "remove a few nodes including one from a zone with multiple nodes",
   241  			existingNodes: allNodes[:7],
   242  			nodesToRemove: allNodes[1:4],
   243  			expectedTree: map[string][]string{
   244  				"":                     {"node-0"},
   245  				"region-1:\x00:zone-2": {"node-4"},
   246  				"region-1:\x00:zone-3": {"node-5"},
   247  				"region-2:\x00:zone-2": {"node-6"},
   248  			},
   249  		},
   250  		{
   251  			name:          "remove all nodes",
   252  			existingNodes: allNodes[:7],
   253  			nodesToRemove: allNodes[:7],
   254  			expectedTree:  map[string][]string{},
   255  		},
   256  		{
   257  			name:          "remove non-existing node",
   258  			existingNodes: nil,
   259  			nodesToRemove: allNodes[:5],
   260  			expectedTree:  map[string][]string{},
   261  			expectError:   true,
   262  		},
   263  	}
   264  
   265  	for _, test := range tests {
   266  		t.Run(test.name, func(t *testing.T) {
   267  			logger, _ := ktesting.NewTestContext(t)
   268  			nt := newNodeTree(logger, test.existingNodes)
   269  			for _, n := range test.nodesToRemove {
   270  				err := nt.removeNode(logger, n)
   271  				if test.expectError == (err == nil) {
   272  					t.Errorf("unexpected returned error value: %v", err)
   273  				}
   274  			}
   275  			verifyNodeTree(t, nt, test.expectedTree)
   276  		})
   277  	}
   278  }
   279  
   280  func TestNodeTree_UpdateNode(t *testing.T) {
   281  	tests := []struct {
   282  		name          string
   283  		existingNodes []*v1.Node
   284  		nodeToUpdate  *v1.Node
   285  		expectedTree  map[string][]string
   286  	}{
   287  		{
   288  			name:          "update a node without label",
   289  			existingNodes: allNodes[:7],
   290  			nodeToUpdate: &v1.Node{
   291  				ObjectMeta: metav1.ObjectMeta{
   292  					Name: "node-0",
   293  					Labels: map[string]string{
   294  						v1.LabelTopologyRegion: "region-1",
   295  						v1.LabelTopologyZone:   "zone-2",
   296  					},
   297  				},
   298  			},
   299  			expectedTree: map[string][]string{
   300  				"region-1:\x00:":       {"node-1"},
   301  				":\x00:zone-2":         {"node-2"},
   302  				"region-1:\x00:zone-2": {"node-3", "node-4", "node-0"},
   303  				"region-1:\x00:zone-3": {"node-5"},
   304  				"region-2:\x00:zone-2": {"node-6"},
   305  			},
   306  		},
   307  		{
   308  			name:          "update the only existing node",
   309  			existingNodes: allNodes[:1],
   310  			nodeToUpdate: &v1.Node{
   311  				ObjectMeta: metav1.ObjectMeta{
   312  					Name: "node-0",
   313  					Labels: map[string]string{
   314  						v1.LabelTopologyRegion: "region-1",
   315  						v1.LabelTopologyZone:   "zone-2",
   316  					},
   317  				},
   318  			},
   319  			expectedTree: map[string][]string{
   320  				"region-1:\x00:zone-2": {"node-0"},
   321  			},
   322  		},
   323  		{
   324  			name:          "update non-existing node",
   325  			existingNodes: allNodes[:1],
   326  			nodeToUpdate: &v1.Node{
   327  				ObjectMeta: metav1.ObjectMeta{
   328  					Name: "node-new",
   329  					Labels: map[string]string{
   330  						v1.LabelTopologyRegion: "region-1",
   331  						v1.LabelTopologyZone:   "zone-2",
   332  					},
   333  				},
   334  			},
   335  			expectedTree: map[string][]string{
   336  				"":                     {"node-0"},
   337  				"region-1:\x00:zone-2": {"node-new"},
   338  			},
   339  		},
   340  	}
   341  
   342  	for _, test := range tests {
   343  		t.Run(test.name, func(t *testing.T) {
   344  			logger, _ := ktesting.NewTestContext(t)
   345  			nt := newNodeTree(logger, test.existingNodes)
   346  			var oldNode *v1.Node
   347  			for _, n := range allNodes {
   348  				if n.Name == test.nodeToUpdate.Name {
   349  					oldNode = n
   350  					break
   351  				}
   352  			}
   353  			if oldNode == nil {
   354  				oldNode = &v1.Node{ObjectMeta: metav1.ObjectMeta{Name: "nonexisting-node"}}
   355  			}
   356  			nt.updateNode(logger, oldNode, test.nodeToUpdate)
   357  			verifyNodeTree(t, nt, test.expectedTree)
   358  		})
   359  	}
   360  }
   361  
   362  func TestNodeTree_List(t *testing.T) {
   363  	tests := []struct {
   364  		name           string
   365  		nodesToAdd     []*v1.Node
   366  		expectedOutput []string
   367  	}{
   368  		{
   369  			name:           "empty tree",
   370  			nodesToAdd:     nil,
   371  			expectedOutput: nil,
   372  		},
   373  		{
   374  			name:           "one node",
   375  			nodesToAdd:     allNodes[:1],
   376  			expectedOutput: []string{"node-0"},
   377  		},
   378  		{
   379  			name:           "four nodes",
   380  			nodesToAdd:     allNodes[:4],
   381  			expectedOutput: []string{"node-0", "node-1", "node-2", "node-3"},
   382  		},
   383  		{
   384  			name:           "all nodes",
   385  			nodesToAdd:     allNodes[:9],
   386  			expectedOutput: []string{"node-0", "node-1", "node-2", "node-3", "node-5", "node-6", "node-4", "node-7", "node-8"},
   387  		},
   388  	}
   389  
   390  	for _, test := range tests {
   391  		t.Run(test.name, func(t *testing.T) {
   392  			logger, _ := ktesting.NewTestContext(t)
   393  			nt := newNodeTree(logger, test.nodesToAdd)
   394  
   395  			output, err := nt.list()
   396  			if err != nil {
   397  				t.Fatal(err)
   398  			}
   399  			if diff := cmp.Diff(test.expectedOutput, output); diff != "" {
   400  				t.Errorf("Unexpected output (-want, +got):\n%s", diff)
   401  			}
   402  		})
   403  	}
   404  }
   405  
   406  func TestNodeTree_List_Exhausted(t *testing.T) {
   407  	logger, _ := ktesting.NewTestContext(t)
   408  	nt := newNodeTree(logger, allNodes[:9])
   409  	nt.numNodes++
   410  	_, err := nt.list()
   411  	if err == nil {
   412  		t.Fatal("Expected an error from zone exhaustion")
   413  	}
   414  }
   415  
   416  func TestNodeTreeMultiOperations(t *testing.T) {
   417  	tests := []struct {
   418  		name           string
   419  		nodesToAdd     []*v1.Node
   420  		nodesToRemove  []*v1.Node
   421  		operations     []string
   422  		expectedOutput []string
   423  	}{
   424  		{
   425  			name:           "add and remove all nodes",
   426  			nodesToAdd:     allNodes[2:9],
   427  			nodesToRemove:  allNodes[2:9],
   428  			operations:     []string{"add", "add", "add", "remove", "remove", "remove"},
   429  			expectedOutput: nil,
   430  		},
   431  		{
   432  			name:           "add and remove some nodes",
   433  			nodesToAdd:     allNodes[2:9],
   434  			nodesToRemove:  allNodes[2:9],
   435  			operations:     []string{"add", "add", "add", "remove"},
   436  			expectedOutput: []string{"node-3", "node-4"},
   437  		},
   438  		{
   439  			name:           "remove three nodes",
   440  			nodesToAdd:     allNodes[2:9],
   441  			nodesToRemove:  allNodes[2:9],
   442  			operations:     []string{"add", "add", "add", "remove", "remove", "remove", "add"},
   443  			expectedOutput: []string{"node-5"},
   444  		},
   445  		{
   446  			name:           "add more nodes to an exhausted zone",
   447  			nodesToAdd:     append(allNodes[4:9:9], allNodes[3]),
   448  			nodesToRemove:  nil,
   449  			operations:     []string{"add", "add", "add", "add", "add", "add"},
   450  			expectedOutput: []string{"node-4", "node-5", "node-6", "node-3", "node-7", "node-8"},
   451  		},
   452  		{
   453  			name:           "remove zone and add new",
   454  			nodesToAdd:     append(allNodes[3:5:5], allNodes[6:8]...),
   455  			nodesToRemove:  allNodes[3:5],
   456  			operations:     []string{"add", "add", "remove", "add", "add", "remove"},
   457  			expectedOutput: []string{"node-6", "node-7"},
   458  		},
   459  	}
   460  
   461  	for _, test := range tests {
   462  		t.Run(test.name, func(t *testing.T) {
   463  			logger, _ := ktesting.NewTestContext(t)
   464  			nt := newNodeTree(logger, nil)
   465  			addIndex := 0
   466  			removeIndex := 0
   467  			for _, op := range test.operations {
   468  				switch op {
   469  				case "add":
   470  					if addIndex >= len(test.nodesToAdd) {
   471  						t.Error("more add operations than nodesToAdd")
   472  					} else {
   473  						nt.addNode(logger, test.nodesToAdd[addIndex])
   474  						addIndex++
   475  					}
   476  				case "remove":
   477  					if removeIndex >= len(test.nodesToRemove) {
   478  						t.Error("more remove operations than nodesToRemove")
   479  					} else {
   480  						nt.removeNode(logger, test.nodesToRemove[removeIndex])
   481  						removeIndex++
   482  					}
   483  				default:
   484  					t.Errorf("unknown operation: %v", op)
   485  				}
   486  			}
   487  			output, err := nt.list()
   488  			if err != nil {
   489  				t.Fatal(err)
   490  			}
   491  			if diff := cmp.Diff(test.expectedOutput, output); diff != "" {
   492  				t.Errorf("Unexpected output (-want, +got):\n%s", diff)
   493  			}
   494  		})
   495  	}
   496  }