...

Source file src/k8s.io/kubernetes/pkg/scheduler/framework/plugins/imagelocality/image_locality_test.go

Documentation: k8s.io/kubernetes/pkg/scheduler/framework/plugins/imagelocality

     1  /*
     2  Copyright 2019 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 imagelocality
    18  
    19  import (
    20  	"context"
    21  	"crypto/sha256"
    22  	"encoding/hex"
    23  	"testing"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	v1 "k8s.io/api/core/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/klog/v2/ktesting"
    29  	"k8s.io/kubernetes/pkg/scheduler/framework"
    30  	"k8s.io/kubernetes/pkg/scheduler/framework/runtime"
    31  	"k8s.io/kubernetes/pkg/scheduler/internal/cache"
    32  )
    33  
    34  func TestImageLocalityPriority(t *testing.T) {
    35  	test40250 := v1.PodSpec{
    36  		Containers: []v1.Container{
    37  			{
    38  
    39  				Image: "gcr.io/40",
    40  			},
    41  			{
    42  				Image: "gcr.io/250",
    43  			},
    44  		},
    45  	}
    46  
    47  	test40300 := v1.PodSpec{
    48  		Containers: []v1.Container{
    49  			{
    50  				Image: "gcr.io/40",
    51  			},
    52  			{
    53  				Image: "gcr.io/300",
    54  			},
    55  		},
    56  	}
    57  
    58  	testMinMax := v1.PodSpec{
    59  		Containers: []v1.Container{
    60  			{
    61  				Image: "gcr.io/10",
    62  			},
    63  			{
    64  				Image: "gcr.io/4000",
    65  			},
    66  		},
    67  	}
    68  
    69  	test300600900 := v1.PodSpec{
    70  		Containers: []v1.Container{
    71  			{
    72  				Image: "gcr.io/300",
    73  			},
    74  			{
    75  				Image: "gcr.io/600",
    76  			},
    77  			{
    78  				Image: "gcr.io/900",
    79  			},
    80  		},
    81  	}
    82  
    83  	test3040 := v1.PodSpec{
    84  		Containers: []v1.Container{
    85  			{
    86  				Image: "gcr.io/30",
    87  			},
    88  			{
    89  				Image: "gcr.io/40",
    90  			},
    91  		},
    92  	}
    93  
    94  	test30Init300 := v1.PodSpec{
    95  		Containers: []v1.Container{
    96  			{
    97  				Image: "gcr.io/30",
    98  			},
    99  		},
   100  		InitContainers: []v1.Container{
   101  			{Image: "gcr.io/300"},
   102  		},
   103  	}
   104  
   105  	node403002000 := v1.NodeStatus{
   106  		Images: []v1.ContainerImage{
   107  			{
   108  				Names: []string{
   109  					"gcr.io/40:latest",
   110  					"gcr.io/40:v1",
   111  					"gcr.io/40:v1",
   112  				},
   113  				SizeBytes: int64(40 * mb),
   114  			},
   115  			{
   116  				Names: []string{
   117  					"gcr.io/300:latest",
   118  					"gcr.io/300:v1",
   119  				},
   120  				SizeBytes: int64(300 * mb),
   121  			},
   122  			{
   123  				Names: []string{
   124  					"gcr.io/2000:latest",
   125  				},
   126  				SizeBytes: int64(2000 * mb),
   127  			},
   128  		},
   129  	}
   130  
   131  	node25010 := v1.NodeStatus{
   132  		Images: []v1.ContainerImage{
   133  			{
   134  				Names: []string{
   135  					"gcr.io/250:latest",
   136  				},
   137  				SizeBytes: int64(250 * mb),
   138  			},
   139  			{
   140  				Names: []string{
   141  					"gcr.io/10:latest",
   142  					"gcr.io/10:v1",
   143  				},
   144  				SizeBytes: int64(10 * mb),
   145  			},
   146  		},
   147  	}
   148  
   149  	node60040900 := v1.NodeStatus{
   150  		Images: []v1.ContainerImage{
   151  			{
   152  				Names: []string{
   153  					"gcr.io/600:latest",
   154  				},
   155  				SizeBytes: int64(600 * mb),
   156  			},
   157  			{
   158  				Names: []string{
   159  					"gcr.io/40:latest",
   160  				},
   161  				SizeBytes: int64(40 * mb),
   162  			},
   163  			{
   164  				Names: []string{
   165  					"gcr.io/900:latest",
   166  				},
   167  				SizeBytes: int64(900 * mb),
   168  			},
   169  		},
   170  	}
   171  
   172  	node300600900 := v1.NodeStatus{
   173  		Images: []v1.ContainerImage{
   174  			{
   175  				Names: []string{
   176  					"gcr.io/300:latest",
   177  				},
   178  				SizeBytes: int64(300 * mb),
   179  			},
   180  			{
   181  				Names: []string{
   182  					"gcr.io/600:latest",
   183  				},
   184  				SizeBytes: int64(600 * mb),
   185  			},
   186  			{
   187  				Names: []string{
   188  					"gcr.io/900:latest",
   189  				},
   190  				SizeBytes: int64(900 * mb),
   191  			},
   192  		},
   193  	}
   194  
   195  	node400030 := v1.NodeStatus{
   196  		Images: []v1.ContainerImage{
   197  			{
   198  				Names: []string{
   199  					"gcr.io/4000:latest",
   200  				},
   201  				SizeBytes: int64(4000 * mb),
   202  			},
   203  			{
   204  				Names: []string{
   205  					"gcr.io/30:latest",
   206  				},
   207  				SizeBytes: int64(30 * mb),
   208  			},
   209  		},
   210  	}
   211  
   212  	node203040 := v1.NodeStatus{
   213  		Images: []v1.ContainerImage{
   214  			{
   215  				Names: []string{
   216  					"gcr.io/20:latest",
   217  				},
   218  				SizeBytes: int64(20 * mb),
   219  			},
   220  			{
   221  				Names: []string{
   222  					"gcr.io/30:latest",
   223  				},
   224  				SizeBytes: int64(30 * mb),
   225  			},
   226  			{
   227  				Names: []string{
   228  					"gcr.io/40:latest",
   229  				},
   230  				SizeBytes: int64(40 * mb),
   231  			},
   232  		},
   233  	}
   234  
   235  	nodeWithNoImages := v1.NodeStatus{}
   236  
   237  	tests := []struct {
   238  		pod          *v1.Pod
   239  		pods         []*v1.Pod
   240  		nodes        []*v1.Node
   241  		expectedList framework.NodeScoreList
   242  		name         string
   243  	}{
   244  		{
   245  			// Pod: gcr.io/40 gcr.io/250
   246  
   247  			// Node1
   248  			// Image: gcr.io/40:latest 40MB
   249  			// Score: 0 (40M/2 < 23M, min-threshold)
   250  
   251  			// Node2
   252  			// Image: gcr.io/250:latest 250MB
   253  			// Score: 100 * (250M/2 - 23M)/(1000M * 2 - 23M) = 5
   254  			pod:          &v1.Pod{Spec: test40250},
   255  			nodes:        []*v1.Node{makeImageNode("node1", node403002000), makeImageNode("node2", node25010)},
   256  			expectedList: []framework.NodeScore{{Name: "node1", Score: 0}, {Name: "node2", Score: 5}},
   257  			name:         "two images spread on two nodes, prefer the larger image one",
   258  		},
   259  		{
   260  			// Pod: gcr.io/40 gcr.io/300
   261  
   262  			// Node1
   263  			// Image: gcr.io/40:latest 40MB, gcr.io/300:latest 300MB
   264  			// Score: 100 * ((40M + 300M)/2 - 23M)/(1000M * 2 - 23M) = 7
   265  
   266  			// Node2
   267  			// Image: not present
   268  			// Score: 0
   269  			pod:          &v1.Pod{Spec: test40300},
   270  			nodes:        []*v1.Node{makeImageNode("node1", node403002000), makeImageNode("node2", node25010)},
   271  			expectedList: []framework.NodeScore{{Name: "node1", Score: 7}, {Name: "node2", Score: 0}},
   272  			name:         "two images on one node, prefer this node",
   273  		},
   274  		{
   275  			// Pod: gcr.io/4000 gcr.io/10
   276  
   277  			// Node1
   278  			// Image: gcr.io/4000:latest 2000MB
   279  			// Score: 100 (4000 * 1/2 >= 1000M * 2, max-threshold)
   280  
   281  			// Node2
   282  			// Image: gcr.io/10:latest 10MB
   283  			// Score: 0 (10M/2 < 23M, min-threshold)
   284  			pod:          &v1.Pod{Spec: testMinMax},
   285  			nodes:        []*v1.Node{makeImageNode("node1", node400030), makeImageNode("node2", node25010)},
   286  			expectedList: []framework.NodeScore{{Name: "node1", Score: framework.MaxNodeScore}, {Name: "node2", Score: 0}},
   287  			name:         "if exceed limit, use limit",
   288  		},
   289  		{
   290  			// Pod: gcr.io/4000 gcr.io/10
   291  
   292  			// Node1
   293  			// Image: gcr.io/4000:latest 4000MB
   294  			// Score: 100 * (4000M/3 - 23M)/(1000M * 2 - 23M) = 66
   295  
   296  			// Node2
   297  			// Image: gcr.io/10:latest 10MB
   298  			// Score: 0 (10M*1/3 < 23M, min-threshold)
   299  
   300  			// Node3
   301  			// Image:
   302  			// Score: 0
   303  			pod:          &v1.Pod{Spec: testMinMax},
   304  			nodes:        []*v1.Node{makeImageNode("node1", node400030), makeImageNode("node2", node25010), makeImageNode("node3", nodeWithNoImages)},
   305  			expectedList: []framework.NodeScore{{Name: "node1", Score: 66}, {Name: "node2", Score: 0}, {Name: "node3", Score: 0}},
   306  			name:         "if exceed limit, use limit (with node which has no images present)",
   307  		},
   308  		{
   309  			// Pod: gcr.io/300 gcr.io/600 gcr.io/900
   310  
   311  			// Node1
   312  			// Image: gcr.io/600:latest 600MB, gcr.io/900:latest 900MB
   313  			// Score: 100 * (600M * 2/3 + 900M * 2/3 - 23M) / (1000M * 3 - 23M) = 32
   314  
   315  			// Node2
   316  			// Image: gcr.io/300:latest 300MB, gcr.io/600:latest 600MB, gcr.io/900:latest 900MB
   317  			// Score: 100 * (300M * 1/3 + 600M * 2/3 + 900M * 2/3 - 23M) / (1000M *3 - 23M) = 36
   318  
   319  			// Node3
   320  			// Image:
   321  			// Score: 0
   322  			pod:          &v1.Pod{Spec: test300600900},
   323  			nodes:        []*v1.Node{makeImageNode("node1", node60040900), makeImageNode("node2", node300600900), makeImageNode("node3", nodeWithNoImages)},
   324  			expectedList: []framework.NodeScore{{Name: "node1", Score: 32}, {Name: "node2", Score: 36}, {Name: "node3", Score: 0}},
   325  			name:         "pod with multiple large images, node2 is preferred",
   326  		},
   327  		{
   328  			// Pod: gcr.io/30 gcr.io/40
   329  
   330  			// Node1
   331  			// Image: gcr.io/20:latest 20MB, gcr.io/30:latest 30MB, gcr.io/40:latest 40MB
   332  			// Score: 100 * (30M + 40M * 1/2 - 23M) / (1000M * 2 - 23M) = 1
   333  
   334  			// Node2
   335  			// Image: 100 * (30M - 23M) / (1000M * 2 - 23M) = 0
   336  			// Score: 0
   337  			pod:          &v1.Pod{Spec: test3040},
   338  			nodes:        []*v1.Node{makeImageNode("node1", node203040), makeImageNode("node2", node400030)},
   339  			expectedList: []framework.NodeScore{{Name: "node1", Score: 1}, {Name: "node2", Score: 0}},
   340  			name:         "pod with multiple small images",
   341  		},
   342  		{
   343  			// Pod: gcr.io/30  InitContainers: gcr.io/300
   344  
   345  			// Node1
   346  			// Image: gcr.io/40:latest 40MB, gcr.io/300:latest 300MB, gcr.io/2000:latest 2000MB
   347  			// Score: 100 * (300M * 1/2 - 23M) / (1000M * 2 - 23M) = 6
   348  
   349  			// Node2
   350  			// Image: gcr.io/20:latest 20MB, gcr.io/30:latest 30MB, gcr.io/40:latest 40MB
   351  			// Score: 100 * (30M * 1/2  - 23M) / (1000M * 2 - 23M) = 0
   352  			pod:          &v1.Pod{Spec: test30Init300},
   353  			nodes:        []*v1.Node{makeImageNode("node1", node403002000), makeImageNode("node2", node203040)},
   354  			expectedList: []framework.NodeScore{{Name: "node1", Score: 6}, {Name: "node2", Score: 0}},
   355  			name:         "include InitContainers: two images spread on two nodes, prefer the larger image one",
   356  		},
   357  	}
   358  
   359  	for _, test := range tests {
   360  		t.Run(test.name, func(t *testing.T) {
   361  			_, ctx := ktesting.NewTestContext(t)
   362  			ctx, cancel := context.WithCancel(ctx)
   363  			defer cancel()
   364  
   365  			snapshot := cache.NewSnapshot(nil, test.nodes)
   366  			state := framework.NewCycleState()
   367  			fh, _ := runtime.NewFramework(ctx, nil, nil, runtime.WithSnapshotSharedLister(snapshot))
   368  
   369  			p, err := New(ctx, nil, fh)
   370  			if err != nil {
   371  				t.Fatalf("creating plugin: %v", err)
   372  			}
   373  			var gotList framework.NodeScoreList
   374  			for _, n := range test.nodes {
   375  				nodeName := n.ObjectMeta.Name
   376  				score, status := p.(framework.ScorePlugin).Score(ctx, state, test.pod, nodeName)
   377  				if !status.IsSuccess() {
   378  					t.Errorf("unexpected error: %v", status)
   379  				}
   380  				gotList = append(gotList, framework.NodeScore{Name: nodeName, Score: score})
   381  			}
   382  
   383  			if diff := cmp.Diff(test.expectedList, gotList); diff != "" {
   384  				t.Errorf("Unexpected node score list (-want, +got):\n%s", diff)
   385  			}
   386  		})
   387  	}
   388  }
   389  
   390  func TestNormalizedImageName(t *testing.T) {
   391  	for _, testCase := range []struct {
   392  		Name   string
   393  		Input  string
   394  		Output string
   395  	}{
   396  		{Name: "add :latest postfix 1", Input: "root", Output: "root:latest"},
   397  		{Name: "add :latest postfix 2", Input: "gcr.io:5000/root", Output: "gcr.io:5000/root:latest"},
   398  		{Name: "keep it as is 1", Input: "root:tag", Output: "root:tag"},
   399  		{Name: "keep it as is 2", Input: "root@" + getImageFakeDigest("root"), Output: "root@" + getImageFakeDigest("root")},
   400  	} {
   401  		t.Run(testCase.Name, func(t *testing.T) {
   402  			image := normalizedImageName(testCase.Input)
   403  			if image != testCase.Output {
   404  				t.Errorf("expected image reference: %q, got %q", testCase.Output, image)
   405  			}
   406  		})
   407  	}
   408  }
   409  
   410  func makeImageNode(node string, status v1.NodeStatus) *v1.Node {
   411  	return &v1.Node{
   412  		ObjectMeta: metav1.ObjectMeta{Name: node},
   413  		Status:     status,
   414  	}
   415  }
   416  
   417  func getImageFakeDigest(fakeContent string) string {
   418  	hash := sha256.Sum256([]byte(fakeContent))
   419  	return "sha256:" + hex.EncodeToString(hash[:])
   420  }
   421  

View as plain text