sigs.k8s.io/kueue@v0.6.2/pkg/podset/podset_test.go (about)

     1  /*
     2  Copyright 2023 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  package podset
    17  
    18  import (
    19  	"context"
    20  	"testing"
    21  
    22  	"github.com/google/go-cmp/cmp"
    23  	"github.com/google/go-cmp/cmp/cmpopts"
    24  	corev1 "k8s.io/api/core/v1"
    25  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    26  	"k8s.io/apimachinery/pkg/runtime/schema"
    27  	"k8s.io/utils/ptr"
    28  
    29  	kueue "sigs.k8s.io/kueue/apis/kueue/v1beta1"
    30  	utiltesting "sigs.k8s.io/kueue/pkg/util/testing"
    31  )
    32  
    33  func TestFromAssignment(t *testing.T) {
    34  	toleration1 := corev1.Toleration{
    35  		Key:      "t1k",
    36  		Operator: corev1.TolerationOpEqual,
    37  		Value:    "t1v",
    38  		Effect:   corev1.TaintEffectNoExecute,
    39  	}
    40  	toleration2 := corev1.Toleration{
    41  		Key:      "t2k",
    42  		Operator: corev1.TolerationOpExists,
    43  		Effect:   corev1.TaintEffectNoSchedule,
    44  	}
    45  	toleration3 := corev1.Toleration{
    46  		Key:      "t3k",
    47  		Operator: corev1.TolerationOpEqual,
    48  		Value:    "t3v",
    49  		Effect:   corev1.TaintEffectPreferNoSchedule,
    50  	}
    51  
    52  	flavor1 := utiltesting.MakeResourceFlavor("flavor1").
    53  		Label("f1l1", "f1v1").
    54  		Label("f1l2", "f1v2").
    55  		Toleration(*toleration1.DeepCopy()).
    56  		Toleration(*toleration2.DeepCopy()).
    57  		Obj()
    58  
    59  	flavor2 := utiltesting.MakeResourceFlavor("flavor2").
    60  		Label("f2l1", "f2v1").
    61  		Label("f2l2", "f2v2").
    62  		Toleration(*toleration3.DeepCopy()).
    63  		Obj()
    64  
    65  	cases := map[string]struct {
    66  		assignment   *kueue.PodSetAssignment
    67  		defaultCount int32
    68  		flavors      []kueue.ResourceFlavor
    69  		wantError    error
    70  		wantInfo     PodSetInfo
    71  	}{
    72  		"single flavor": {
    73  			assignment: &kueue.PodSetAssignment{
    74  				Name: "name",
    75  				Flavors: map[corev1.ResourceName]kueue.ResourceFlavorReference{
    76  					corev1.ResourceCPU: kueue.ResourceFlavorReference(flavor1.Name),
    77  				},
    78  				Count: ptr.To[int32](2),
    79  			},
    80  			defaultCount: 4,
    81  			flavors:      []kueue.ResourceFlavor{*flavor1.DeepCopy()},
    82  			wantInfo: PodSetInfo{
    83  				Name:  "name",
    84  				Count: 2,
    85  				NodeSelector: map[string]string{
    86  					"f1l1": "f1v1",
    87  					"f1l2": "f1v2",
    88  				},
    89  				Tolerations: []corev1.Toleration{*toleration1.DeepCopy(), *toleration2.DeepCopy()},
    90  			},
    91  		},
    92  		"multiple flavors": {
    93  			assignment: &kueue.PodSetAssignment{
    94  				Name: "name",
    95  				Flavors: map[corev1.ResourceName]kueue.ResourceFlavorReference{
    96  					corev1.ResourceCPU:    kueue.ResourceFlavorReference(flavor1.Name),
    97  					corev1.ResourceMemory: kueue.ResourceFlavorReference(flavor2.Name),
    98  				},
    99  				Count: ptr.To[int32](2),
   100  			},
   101  			defaultCount: 4,
   102  			flavors:      []kueue.ResourceFlavor{*flavor1.DeepCopy(), *flavor2.DeepCopy()},
   103  			wantInfo: PodSetInfo{
   104  				Name:  "name",
   105  				Count: 2,
   106  				NodeSelector: map[string]string{
   107  					"f1l1": "f1v1",
   108  					"f1l2": "f1v2",
   109  					"f2l1": "f2v1",
   110  					"f2l2": "f2v2",
   111  				},
   112  				Tolerations: []corev1.Toleration{*toleration1.DeepCopy(), *toleration2.DeepCopy(), *toleration3.DeepCopy()},
   113  			},
   114  		},
   115  		"duplicate flavor": {
   116  			assignment: &kueue.PodSetAssignment{
   117  				Name: "name",
   118  				Flavors: map[corev1.ResourceName]kueue.ResourceFlavorReference{
   119  					corev1.ResourceCPU:    kueue.ResourceFlavorReference(flavor1.Name),
   120  					corev1.ResourceMemory: kueue.ResourceFlavorReference(flavor1.Name),
   121  				},
   122  				Count: ptr.To[int32](2),
   123  			},
   124  			defaultCount: 4,
   125  			flavors:      []kueue.ResourceFlavor{*flavor1.DeepCopy(), *flavor2.DeepCopy()},
   126  			wantInfo: PodSetInfo{
   127  				Name:  "name",
   128  				Count: 2,
   129  				NodeSelector: map[string]string{
   130  					"f1l1": "f1v1",
   131  					"f1l2": "f1v2",
   132  				},
   133  				Tolerations: []corev1.Toleration{*toleration1.DeepCopy(), *toleration2.DeepCopy()},
   134  			},
   135  		},
   136  		"flavor not found": {
   137  			assignment: &kueue.PodSetAssignment{
   138  				Name: "name",
   139  				Flavors: map[corev1.ResourceName]kueue.ResourceFlavorReference{
   140  					corev1.ResourceCPU: kueue.ResourceFlavorReference(flavor1.Name),
   141  				},
   142  				Count: ptr.To[int32](2),
   143  			},
   144  			defaultCount: 4,
   145  			wantError:    apierrors.NewNotFound(schema.GroupResource{Group: kueue.GroupVersion.Group, Resource: "resourceflavors"}, "flavor1"),
   146  		},
   147  		"default count": {
   148  			assignment: &kueue.PodSetAssignment{
   149  				Name: "name",
   150  				Flavors: map[corev1.ResourceName]kueue.ResourceFlavorReference{
   151  					corev1.ResourceCPU: kueue.ResourceFlavorReference(flavor1.Name),
   152  				},
   153  			},
   154  			defaultCount: 4,
   155  			flavors:      []kueue.ResourceFlavor{*flavor1.DeepCopy()},
   156  			wantInfo: PodSetInfo{
   157  				Name:  "name",
   158  				Count: 4,
   159  				NodeSelector: map[string]string{
   160  					"f1l1": "f1v1",
   161  					"f1l2": "f1v2",
   162  				},
   163  				Tolerations: []corev1.Toleration{*toleration1.DeepCopy(), *toleration2.DeepCopy()},
   164  			},
   165  		},
   166  	}
   167  	for name, tc := range cases {
   168  		t.Run(name, func(t *testing.T) {
   169  			ctx := context.TODO()
   170  			client := utiltesting.NewClientBuilder().WithLists(&kueue.ResourceFlavorList{Items: tc.flavors}).Build()
   171  
   172  			gotInfo, gotError := FromAssignment(ctx, client, tc.assignment, tc.defaultCount)
   173  
   174  			if diff := cmp.Diff(tc.wantError, gotError); diff != "" {
   175  				t.Errorf("Unexpected error (-want/+got):\n%s", diff)
   176  			}
   177  
   178  			if tc.wantError == nil {
   179  				if diff := cmp.Diff(tc.wantInfo, gotInfo, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(a, b corev1.Toleration) bool { return a.Key < b.Key })); diff != "" {
   180  					t.Errorf("Unexpected info (-want/+got):\n%s", diff)
   181  				}
   182  			}
   183  		})
   184  	}
   185  }
   186  
   187  func TestMergeRestore(t *testing.T) {
   188  
   189  	basePodSet := utiltesting.MakePodSet("", 1).
   190  		NodeSelector(map[string]string{"ns0": "ns0v"}).
   191  		Labels(map[string]string{"l0": "l0v"}).
   192  		Annotations(map[string]string{"a0": "a0v"}).
   193  		Toleration(corev1.Toleration{
   194  			Key:      "t0",
   195  			Operator: corev1.TolerationOpEqual,
   196  			Value:    "t0v",
   197  			Effect:   corev1.TaintEffectNoSchedule,
   198  		}).
   199  		Obj()
   200  
   201  	cases := map[string]struct {
   202  		podSet             *kueue.PodSet
   203  		info               PodSetInfo
   204  		wantError          bool
   205  		wantPodSet         *kueue.PodSet
   206  		wantRestoreChanges bool
   207  	}{
   208  		"empty info": {
   209  			podSet:     basePodSet.DeepCopy(),
   210  			wantPodSet: basePodSet.DeepCopy(),
   211  		},
   212  		"no conflicts": {
   213  			podSet: basePodSet.DeepCopy(),
   214  			info: PodSetInfo{
   215  				Annotations: map[string]string{
   216  					"a1": "a1v",
   217  				},
   218  				Labels: map[string]string{
   219  					"l1": "l1v",
   220  				},
   221  				NodeSelector: map[string]string{
   222  					"ns1": "ns1v",
   223  				},
   224  				Tolerations: []corev1.Toleration{
   225  					{
   226  						Key:      "t1",
   227  						Operator: corev1.TolerationOpEqual,
   228  						Value:    "t1v",
   229  						Effect:   corev1.TaintEffectNoSchedule,
   230  					},
   231  				},
   232  			},
   233  			wantPodSet: utiltesting.MakePodSet("", 1).
   234  				NodeSelector(map[string]string{"ns0": "ns0v", "ns1": "ns1v"}).
   235  				Labels(map[string]string{"l0": "l0v", "l1": "l1v"}).
   236  				Annotations(map[string]string{"a0": "a0v", "a1": "a1v"}).
   237  				Toleration(corev1.Toleration{
   238  					Key:      "t0",
   239  					Operator: corev1.TolerationOpEqual,
   240  					Value:    "t0v",
   241  					Effect:   corev1.TaintEffectNoSchedule,
   242  				}).
   243  				Toleration(corev1.Toleration{
   244  					Key:      "t1",
   245  					Operator: corev1.TolerationOpEqual,
   246  					Value:    "t1v",
   247  					Effect:   corev1.TaintEffectNoSchedule,
   248  				}).
   249  				Obj(),
   250  			wantRestoreChanges: true,
   251  		},
   252  		"conflicting label": {
   253  			podSet: basePodSet.DeepCopy(),
   254  			info: PodSetInfo{
   255  				Labels: map[string]string{
   256  					"l0": "l0v1",
   257  				},
   258  			},
   259  			wantError: true,
   260  		},
   261  		"conflicting annotation": {
   262  			podSet: basePodSet.DeepCopy(),
   263  			info: PodSetInfo{
   264  				Annotations: map[string]string{
   265  					"a0": "a0v1",
   266  				},
   267  			},
   268  			wantError: true,
   269  		},
   270  		"conflicting node selector": {
   271  			podSet: basePodSet.DeepCopy(),
   272  			info: PodSetInfo{
   273  				NodeSelector: map[string]string{
   274  					"ns0": "ns0v1",
   275  				},
   276  			},
   277  			wantError: true,
   278  		},
   279  	}
   280  
   281  	for name, tc := range cases {
   282  		t.Run(name, func(t *testing.T) {
   283  			orig := tc.podSet.DeepCopy()
   284  
   285  			gotError := Merge(&tc.podSet.Template.ObjectMeta, &tc.podSet.Template.Spec, tc.info)
   286  
   287  			if tc.wantError != (gotError != nil) {
   288  				t.Errorf("Unexpected error status want: %v", tc.wantError)
   289  			}
   290  
   291  			if !tc.wantError {
   292  				if diff := cmp.Diff(tc.wantPodSet.Template, tc.podSet.Template, cmpopts.EquateEmpty()); diff != "" {
   293  					t.Errorf("Unexpected template (-want/+got):\n%s", diff)
   294  				}
   295  
   296  				restoreInfo := FromPodSet(orig)
   297  				gotRestoreChage := RestorePodSpec(&tc.podSet.Template.ObjectMeta, &tc.podSet.Template.Spec, restoreInfo)
   298  				if gotRestoreChage != tc.wantRestoreChanges {
   299  					t.Errorf("Unexpected restore change status want:%v", tc.wantRestoreChanges)
   300  				}
   301  				if diff := cmp.Diff(orig.Template, tc.podSet.Template, cmpopts.EquateEmpty()); diff != "" {
   302  					t.Errorf("Unexpected template (-want/+got):\n%s", diff)
   303  				}
   304  
   305  			}
   306  		})
   307  	}
   308  }
   309  
   310  func TestAddOrUpdateLabel(t *testing.T) {
   311  	cases := map[string]struct {
   312  		info     PodSetInfo
   313  		k, v     string
   314  		wantInfo PodSetInfo
   315  	}{
   316  		"add to nil labels": {
   317  			info: PodSetInfo{},
   318  			k:    "key",
   319  			v:    "value",
   320  			wantInfo: PodSetInfo{
   321  				Labels: map[string]string{"key": "value"},
   322  			},
   323  		},
   324  		"add": {
   325  			info: PodSetInfo{
   326  				Labels: map[string]string{"other-key": "other-value"},
   327  			},
   328  			k: "key",
   329  			v: "value",
   330  			wantInfo: PodSetInfo{
   331  				Labels: map[string]string{"other-key": "other-value", "key": "value"},
   332  			},
   333  		},
   334  		"update": {
   335  			info: PodSetInfo{
   336  				Labels: map[string]string{"key": "value"},
   337  			},
   338  			k: "key",
   339  			v: "updated-value",
   340  			wantInfo: PodSetInfo{
   341  				Labels: map[string]string{"key": "updated-value"},
   342  			},
   343  		},
   344  	}
   345  	for name, tc := range cases {
   346  		t.Run(name, func(t *testing.T) {
   347  			tc.info.AddOrUpdateLabel(tc.k, tc.v)
   348  			if diff := cmp.Diff(tc.wantInfo, tc.info, cmpopts.EquateEmpty()); diff != "" {
   349  				t.Errorf("Unexpected info (-want/+got):\n%s", diff)
   350  			}
   351  		})
   352  	}
   353  }