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 }