k8s.io/kubernetes@v1.29.3/pkg/apis/resource/validation/validation_podschedulingcontext_test.go (about)

     1  /*
     2  Copyright 2022 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 validation
    18  
    19  import (
    20  	"fmt"
    21  	"testing"
    22  
    23  	"github.com/stretchr/testify/assert"
    24  
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/util/validation/field"
    27  	"k8s.io/kubernetes/pkg/apis/resource"
    28  	"k8s.io/utils/pointer"
    29  )
    30  
    31  func testPodSchedulingContexts(name, namespace string, spec resource.PodSchedulingContextSpec) *resource.PodSchedulingContext {
    32  	return &resource.PodSchedulingContext{
    33  		ObjectMeta: metav1.ObjectMeta{
    34  			Name:      name,
    35  			Namespace: namespace,
    36  		},
    37  		Spec: spec,
    38  	}
    39  }
    40  
    41  func TestValidatePodSchedulingContexts(t *testing.T) {
    42  	goodName := "foo"
    43  	goodNS := "ns"
    44  	goodPodSchedulingSpec := resource.PodSchedulingContextSpec{}
    45  	now := metav1.Now()
    46  	badName := "!@#$%^"
    47  	badValue := "spaces not allowed"
    48  
    49  	scenarios := map[string]struct {
    50  		schedulingCtx *resource.PodSchedulingContext
    51  		wantFailures  field.ErrorList
    52  	}{
    53  		"good-schedulingCtx": {
    54  			schedulingCtx: testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec),
    55  		},
    56  		"missing-name": {
    57  			wantFailures:  field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")},
    58  			schedulingCtx: testPodSchedulingContexts("", goodNS, goodPodSchedulingSpec),
    59  		},
    60  		"bad-name": {
    61  			wantFailures:  field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
    62  			schedulingCtx: testPodSchedulingContexts(badName, goodNS, goodPodSchedulingSpec),
    63  		},
    64  		"missing-namespace": {
    65  			wantFailures:  field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")},
    66  			schedulingCtx: testPodSchedulingContexts(goodName, "", goodPodSchedulingSpec),
    67  		},
    68  		"generate-name": {
    69  			schedulingCtx: func() *resource.PodSchedulingContext {
    70  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
    71  				schedulingCtx.GenerateName = "pvc-"
    72  				return schedulingCtx
    73  			}(),
    74  		},
    75  		"uid": {
    76  			schedulingCtx: func() *resource.PodSchedulingContext {
    77  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
    78  				schedulingCtx.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d"
    79  				return schedulingCtx
    80  			}(),
    81  		},
    82  		"resource-version": {
    83  			schedulingCtx: func() *resource.PodSchedulingContext {
    84  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
    85  				schedulingCtx.ResourceVersion = "1"
    86  				return schedulingCtx
    87  			}(),
    88  		},
    89  		"generation": {
    90  			schedulingCtx: func() *resource.PodSchedulingContext {
    91  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
    92  				schedulingCtx.Generation = 100
    93  				return schedulingCtx
    94  			}(),
    95  		},
    96  		"creation-timestamp": {
    97  			schedulingCtx: func() *resource.PodSchedulingContext {
    98  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
    99  				schedulingCtx.CreationTimestamp = now
   100  				return schedulingCtx
   101  			}(),
   102  		},
   103  		"deletion-grace-period-seconds": {
   104  			schedulingCtx: func() *resource.PodSchedulingContext {
   105  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
   106  				schedulingCtx.DeletionGracePeriodSeconds = pointer.Int64(10)
   107  				return schedulingCtx
   108  			}(),
   109  		},
   110  		"owner-references": {
   111  			schedulingCtx: func() *resource.PodSchedulingContext {
   112  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
   113  				schedulingCtx.OwnerReferences = []metav1.OwnerReference{
   114  					{
   115  						APIVersion: "v1",
   116  						Kind:       "pod",
   117  						Name:       "foo",
   118  						UID:        "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d",
   119  					},
   120  				}
   121  				return schedulingCtx
   122  			}(),
   123  		},
   124  		"finalizers": {
   125  			schedulingCtx: func() *resource.PodSchedulingContext {
   126  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
   127  				schedulingCtx.Finalizers = []string{
   128  					"example.com/foo",
   129  				}
   130  				return schedulingCtx
   131  			}(),
   132  		},
   133  		"managed-fields": {
   134  			schedulingCtx: func() *resource.PodSchedulingContext {
   135  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
   136  				schedulingCtx.ManagedFields = []metav1.ManagedFieldsEntry{
   137  					{
   138  						FieldsType: "FieldsV1",
   139  						Operation:  "Apply",
   140  						APIVersion: "apps/v1",
   141  						Manager:    "foo",
   142  					},
   143  				}
   144  				return schedulingCtx
   145  			}(),
   146  		},
   147  		"good-labels": {
   148  			schedulingCtx: func() *resource.PodSchedulingContext {
   149  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
   150  				schedulingCtx.Labels = map[string]string{
   151  					"apps.kubernetes.io/name": "test",
   152  				}
   153  				return schedulingCtx
   154  			}(),
   155  		},
   156  		"bad-labels": {
   157  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue',  or 'my_value',  or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")},
   158  			schedulingCtx: func() *resource.PodSchedulingContext {
   159  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
   160  				schedulingCtx.Labels = map[string]string{
   161  					"hello-world": badValue,
   162  				}
   163  				return schedulingCtx
   164  			}(),
   165  		},
   166  		"good-annotations": {
   167  			schedulingCtx: func() *resource.PodSchedulingContext {
   168  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
   169  				schedulingCtx.Annotations = map[string]string{
   170  					"foo": "bar",
   171  				}
   172  				return schedulingCtx
   173  			}(),
   174  		},
   175  		"bad-annotations": {
   176  			wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName',  or 'my.name',  or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")},
   177  			schedulingCtx: func() *resource.PodSchedulingContext {
   178  				schedulingCtx := testPodSchedulingContexts(goodName, goodNS, goodPodSchedulingSpec)
   179  				schedulingCtx.Annotations = map[string]string{
   180  					badName: "hello world",
   181  				}
   182  				return schedulingCtx
   183  			}(),
   184  		},
   185  	}
   186  
   187  	for name, scenario := range scenarios {
   188  		t.Run(name, func(t *testing.T) {
   189  			errs := ValidatePodSchedulingContexts(scenario.schedulingCtx)
   190  			assert.Equal(t, scenario.wantFailures, errs)
   191  		})
   192  	}
   193  }
   194  
   195  func TestValidatePodSchedulingUpdate(t *testing.T) {
   196  	validScheduling := testPodSchedulingContexts("foo", "ns", resource.PodSchedulingContextSpec{})
   197  	badName := "!@#$%^"
   198  
   199  	scenarios := map[string]struct {
   200  		oldScheduling *resource.PodSchedulingContext
   201  		update        func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext
   202  		wantFailures  field.ErrorList
   203  	}{
   204  		"valid-no-op-update": {
   205  			oldScheduling: validScheduling,
   206  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   207  				return schedulingCtx
   208  			},
   209  		},
   210  		"add-selected-node": {
   211  			oldScheduling: validScheduling,
   212  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   213  				schedulingCtx.Spec.SelectedNode = "worker1"
   214  				return schedulingCtx
   215  			},
   216  		},
   217  		"add-potential-nodes": {
   218  			oldScheduling: validScheduling,
   219  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   220  				for i := 0; i < resource.PodSchedulingNodeListMaxSize; i++ {
   221  					schedulingCtx.Spec.PotentialNodes = append(schedulingCtx.Spec.PotentialNodes, fmt.Sprintf("worker%d", i))
   222  				}
   223  				return schedulingCtx
   224  			},
   225  		},
   226  		"invalid-potential-nodes-too-long": {
   227  			wantFailures:  field.ErrorList{field.TooLongMaxLength(field.NewPath("spec", "potentialNodes"), 129, resource.PodSchedulingNodeListMaxSize)},
   228  			oldScheduling: validScheduling,
   229  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   230  				for i := 0; i < resource.PodSchedulingNodeListMaxSize+1; i++ {
   231  					schedulingCtx.Spec.PotentialNodes = append(schedulingCtx.Spec.PotentialNodes, fmt.Sprintf("worker%d", i))
   232  				}
   233  				return schedulingCtx
   234  			},
   235  		},
   236  		"invalid-potential-nodes-name": {
   237  			wantFailures:  field.ErrorList{field.Invalid(field.NewPath("spec", "potentialNodes").Index(0), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
   238  			oldScheduling: validScheduling,
   239  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   240  				schedulingCtx.Spec.PotentialNodes = append(schedulingCtx.Spec.PotentialNodes, badName)
   241  				return schedulingCtx
   242  			},
   243  		},
   244  	}
   245  
   246  	for name, scenario := range scenarios {
   247  		t.Run(name, func(t *testing.T) {
   248  			scenario.oldScheduling.ResourceVersion = "1"
   249  			errs := ValidatePodSchedulingContextUpdate(scenario.update(scenario.oldScheduling.DeepCopy()), scenario.oldScheduling)
   250  			assert.Equal(t, scenario.wantFailures, errs)
   251  		})
   252  	}
   253  }
   254  
   255  func TestValidatePodSchedulingStatusUpdate(t *testing.T) {
   256  	validScheduling := testPodSchedulingContexts("foo", "ns", resource.PodSchedulingContextSpec{})
   257  	badName := "!@#$%^"
   258  
   259  	scenarios := map[string]struct {
   260  		oldScheduling *resource.PodSchedulingContext
   261  		update        func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext
   262  		wantFailures  field.ErrorList
   263  	}{
   264  		"valid-no-op-update": {
   265  			oldScheduling: validScheduling,
   266  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   267  				return schedulingCtx
   268  			},
   269  		},
   270  		"add-claim-status": {
   271  			oldScheduling: validScheduling,
   272  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   273  				schedulingCtx.Status.ResourceClaims = append(schedulingCtx.Status.ResourceClaims,
   274  					resource.ResourceClaimSchedulingStatus{
   275  						Name: "my-claim",
   276  					},
   277  				)
   278  				for i := 0; i < resource.PodSchedulingNodeListMaxSize; i++ {
   279  					schedulingCtx.Status.ResourceClaims[0].UnsuitableNodes = append(
   280  						schedulingCtx.Status.ResourceClaims[0].UnsuitableNodes,
   281  						fmt.Sprintf("worker%d", i),
   282  					)
   283  				}
   284  				return schedulingCtx
   285  			},
   286  		},
   287  		"invalid-duplicated-claim-status": {
   288  			wantFailures:  field.ErrorList{field.Duplicate(field.NewPath("status", "claims").Index(1), "my-claim")},
   289  			oldScheduling: validScheduling,
   290  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   291  				for i := 0; i < 2; i++ {
   292  					schedulingCtx.Status.ResourceClaims = append(schedulingCtx.Status.ResourceClaims,
   293  						resource.ResourceClaimSchedulingStatus{Name: "my-claim"},
   294  					)
   295  				}
   296  				return schedulingCtx
   297  			},
   298  		},
   299  		"invalid-too-long-claim-status": {
   300  			wantFailures:  field.ErrorList{field.TooLongMaxLength(field.NewPath("status", "claims").Index(0).Child("unsuitableNodes"), 129, resource.PodSchedulingNodeListMaxSize)},
   301  			oldScheduling: validScheduling,
   302  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   303  				schedulingCtx.Status.ResourceClaims = append(schedulingCtx.Status.ResourceClaims,
   304  					resource.ResourceClaimSchedulingStatus{
   305  						Name: "my-claim",
   306  					},
   307  				)
   308  				for i := 0; i < resource.PodSchedulingNodeListMaxSize+1; i++ {
   309  					schedulingCtx.Status.ResourceClaims[0].UnsuitableNodes = append(
   310  						schedulingCtx.Status.ResourceClaims[0].UnsuitableNodes,
   311  						fmt.Sprintf("worker%d", i),
   312  					)
   313  				}
   314  				return schedulingCtx
   315  			},
   316  		},
   317  		"invalid-node-name": {
   318  			wantFailures:  field.ErrorList{field.Invalid(field.NewPath("status", "claims").Index(0).Child("unsuitableNodes").Index(0), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")},
   319  			oldScheduling: validScheduling,
   320  			update: func(schedulingCtx *resource.PodSchedulingContext) *resource.PodSchedulingContext {
   321  				schedulingCtx.Status.ResourceClaims = append(schedulingCtx.Status.ResourceClaims,
   322  					resource.ResourceClaimSchedulingStatus{
   323  						Name: "my-claim",
   324  					},
   325  				)
   326  				schedulingCtx.Status.ResourceClaims[0].UnsuitableNodes = append(
   327  					schedulingCtx.Status.ResourceClaims[0].UnsuitableNodes,
   328  					badName,
   329  				)
   330  				return schedulingCtx
   331  			},
   332  		},
   333  	}
   334  
   335  	for name, scenario := range scenarios {
   336  		t.Run(name, func(t *testing.T) {
   337  			scenario.oldScheduling.ResourceVersion = "1"
   338  			errs := ValidatePodSchedulingContextStatusUpdate(scenario.update(scenario.oldScheduling.DeepCopy()), scenario.oldScheduling)
   339  			assert.Equal(t, scenario.wantFailures, errs)
   340  		})
   341  	}
   342  }