k8s.io/kubernetes@v1.29.3/pkg/apis/core/v1/validation/validation_test.go (about) 1 /* 2 Copyright 2017 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 "strings" 21 "testing" 22 23 v1 "k8s.io/api/core/v1" 24 "k8s.io/apimachinery/pkg/api/resource" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/util/sets" 27 "k8s.io/apimachinery/pkg/util/validation/field" 28 "k8s.io/kubernetes/pkg/apis/core" 29 ) 30 31 func TestValidateResourceRequirements(t *testing.T) { 32 successCase := []struct { 33 name string 34 requirements v1.ResourceRequirements 35 }{{ 36 name: "Resources with Requests equal to Limits", 37 requirements: v1.ResourceRequirements{ 38 Requests: v1.ResourceList{ 39 v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), 40 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10G"), 41 }, 42 Limits: v1.ResourceList{ 43 v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), 44 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10G"), 45 }, 46 }, 47 }, { 48 name: "Resources with only Limits", 49 requirements: v1.ResourceRequirements{ 50 Limits: v1.ResourceList{ 51 v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), 52 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10G"), 53 v1.ResourceName("my.org/resource"): resource.MustParse("10"), 54 }, 55 }, 56 }, { 57 name: "Resources with only Requests", 58 requirements: v1.ResourceRequirements{ 59 Requests: v1.ResourceList{ 60 v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), 61 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10G"), 62 v1.ResourceName("my.org/resource"): resource.MustParse("10"), 63 }, 64 }, 65 }, { 66 name: "Resources with Requests Less Than Limits", 67 requirements: v1.ResourceRequirements{ 68 Requests: v1.ResourceList{ 69 v1.ResourceName(v1.ResourceCPU): resource.MustParse("9"), 70 v1.ResourceName(v1.ResourceMemory): resource.MustParse("9G"), 71 v1.ResourceName("my.org/resource"): resource.MustParse("9"), 72 }, 73 Limits: v1.ResourceList{ 74 v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), 75 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10G"), 76 v1.ResourceName("my.org/resource"): resource.MustParse("9"), 77 }, 78 }, 79 }} 80 for _, tc := range successCase { 81 t.Run(tc.name, func(t *testing.T) { 82 if errs := ValidateResourceRequirements(&tc.requirements, field.NewPath("resources")); len(errs) != 0 { 83 t.Errorf("unexpected error: %v", errs) 84 } 85 }) 86 } 87 88 errorCase := []struct { 89 name string 90 requirements v1.ResourceRequirements 91 skipLimitValueCheck bool 92 skipRequestValueCheck bool 93 }{{ 94 name: "Resources with Requests Larger Than Limits", 95 requirements: v1.ResourceRequirements{ 96 Requests: v1.ResourceList{ 97 v1.ResourceName(v1.ResourceCPU): resource.MustParse("10"), 98 v1.ResourceName(v1.ResourceMemory): resource.MustParse("10G"), 99 v1.ResourceName("my.org/resource"): resource.MustParse("10m"), 100 }, 101 Limits: v1.ResourceList{ 102 v1.ResourceName(v1.ResourceCPU): resource.MustParse("9"), 103 v1.ResourceName(v1.ResourceMemory): resource.MustParse("9G"), 104 v1.ResourceName("my.org/resource"): resource.MustParse("9m"), 105 }, 106 }, 107 }, { 108 name: "Invalid Resources with Requests", 109 requirements: v1.ResourceRequirements{ 110 Requests: v1.ResourceList{ 111 v1.ResourceName("my.org"): resource.MustParse("10m"), 112 }, 113 }, 114 skipRequestValueCheck: true, 115 }, { 116 name: "Invalid Resources with Limits", 117 requirements: v1.ResourceRequirements{ 118 Limits: v1.ResourceList{ 119 v1.ResourceName("my.org"): resource.MustParse("9m"), 120 }, 121 }, 122 skipLimitValueCheck: true, 123 }} 124 for _, tc := range errorCase { 125 t.Run(tc.name, func(t *testing.T) { 126 errs := ValidateResourceRequirements(&tc.requirements, field.NewPath("resources")) 127 if len(errs) == 0 { 128 t.Errorf("expected error") 129 } 130 validateNamesAndValuesInDescription(t, tc.requirements.Limits, errs, tc.skipLimitValueCheck, "limit") 131 validateNamesAndValuesInDescription(t, tc.requirements.Requests, errs, tc.skipRequestValueCheck, "request") 132 }) 133 } 134 } 135 136 func validateNamesAndValuesInDescription(t *testing.T, r v1.ResourceList, errs field.ErrorList, skipValueTest bool, rl string) { 137 for name, value := range r { 138 containsName := false 139 containsValue := false 140 141 for _, e := range errs { 142 if strings.Contains(e.Error(), name.String()) { 143 containsName = true 144 } 145 146 if strings.Contains(e.Error(), value.String()) { 147 containsValue = true 148 } 149 } 150 if !containsName { 151 t.Errorf("error must contain %s name", rl) 152 } 153 if !containsValue && !skipValueTest { 154 t.Errorf("error must contain %s value", rl) 155 } 156 } 157 } 158 159 func TestValidateContainerResourceName(t *testing.T) { 160 successCase := []struct { 161 name string 162 ResourceName core.ResourceName 163 }{{ 164 name: "CPU resource", 165 ResourceName: "cpu", 166 }, { 167 name: "Memory resource", 168 ResourceName: "memory", 169 }, { 170 name: "Hugepages resource", 171 ResourceName: "hugepages-2Mi", 172 }, { 173 name: "Namespaced resource", 174 ResourceName: "kubernetes.io/resource-foo", 175 }, { 176 name: "Extended Resource", 177 ResourceName: "my.org/resource-bar", 178 }} 179 for _, tc := range successCase { 180 t.Run(tc.name, func(t *testing.T) { 181 if errs := ValidateContainerResourceName(tc.ResourceName, field.NewPath(string(tc.ResourceName))); len(errs) != 0 { 182 t.Errorf("unexpected error: %v", errs) 183 } 184 }) 185 } 186 187 errorCase := []struct { 188 name string 189 ResourceName core.ResourceName 190 }{{ 191 name: "Invalid standard resource", 192 ResourceName: "cpu-core", 193 }, { 194 name: "Invalid namespaced resource", 195 ResourceName: "kubernetes.io/", 196 }, { 197 name: "Invalid extended resource", 198 ResourceName: "my.org-foo-resource", 199 }} 200 for _, tc := range errorCase { 201 t.Run(tc.name, func(t *testing.T) { 202 if errs := ValidateContainerResourceName(tc.ResourceName, field.NewPath(string(tc.ResourceName))); len(errs) == 0 { 203 t.Errorf("expected error") 204 } 205 }) 206 } 207 } 208 209 func TestValidatePodLogOptions(t *testing.T) { 210 211 var ( 212 positiveLine = int64(8) 213 negativeLine = int64(-8) 214 limitBytesGreaterThan1 = int64(12) 215 limitBytesLessThan1 = int64(0) 216 sinceSecondsGreaterThan1 = int64(10) 217 sinceSecondsLessThan1 = int64(0) 218 timestamp = metav1.Now() 219 ) 220 221 successCase := []struct { 222 name string 223 podLogOptions v1.PodLogOptions 224 }{{ 225 name: "Empty PodLogOptions", 226 podLogOptions: v1.PodLogOptions{}, 227 }, { 228 name: "PodLogOptions with TailLines", 229 podLogOptions: v1.PodLogOptions{ 230 TailLines: &positiveLine, 231 }, 232 }, { 233 name: "PodLogOptions with LimitBytes", 234 podLogOptions: v1.PodLogOptions{ 235 LimitBytes: &limitBytesGreaterThan1, 236 }, 237 }, { 238 name: "PodLogOptions with only sinceSeconds", 239 podLogOptions: v1.PodLogOptions{ 240 SinceSeconds: &sinceSecondsGreaterThan1, 241 }, 242 }, { 243 name: "PodLogOptions with LimitBytes with TailLines", 244 podLogOptions: v1.PodLogOptions{ 245 LimitBytes: &limitBytesGreaterThan1, 246 TailLines: &positiveLine, 247 }, 248 }, { 249 name: "PodLogOptions with LimitBytes with TailLines with SinceSeconds", 250 podLogOptions: v1.PodLogOptions{ 251 LimitBytes: &limitBytesGreaterThan1, 252 TailLines: &positiveLine, 253 SinceSeconds: &sinceSecondsGreaterThan1, 254 }, 255 }} 256 for _, tc := range successCase { 257 t.Run(tc.name, func(t *testing.T) { 258 if errs := ValidatePodLogOptions(&tc.podLogOptions); len(errs) != 0 { 259 t.Errorf("unexpected error: %v", errs) 260 } 261 }) 262 } 263 264 errorCase := []struct { 265 name string 266 podLogOptions v1.PodLogOptions 267 }{{ 268 name: "Invalid podLogOptions with Negative TailLines", 269 podLogOptions: v1.PodLogOptions{ 270 TailLines: &negativeLine, 271 LimitBytes: &limitBytesGreaterThan1, 272 SinceSeconds: &sinceSecondsGreaterThan1, 273 }, 274 }, { 275 name: "Invalid podLogOptions with zero or negative LimitBytes", 276 podLogOptions: v1.PodLogOptions{ 277 TailLines: &positiveLine, 278 LimitBytes: &limitBytesLessThan1, 279 SinceSeconds: &sinceSecondsGreaterThan1, 280 }, 281 }, { 282 name: "Invalid podLogOptions with zero or negative SinceSeconds", 283 podLogOptions: v1.PodLogOptions{ 284 TailLines: &negativeLine, 285 LimitBytes: &limitBytesGreaterThan1, 286 SinceSeconds: &sinceSecondsLessThan1, 287 }, 288 }, { 289 name: "Invalid podLogOptions with both SinceSeconds and SinceTime set", 290 podLogOptions: v1.PodLogOptions{ 291 TailLines: &negativeLine, 292 LimitBytes: &limitBytesGreaterThan1, 293 SinceSeconds: &sinceSecondsGreaterThan1, 294 SinceTime: ×tamp, 295 }, 296 }} 297 for _, tc := range errorCase { 298 t.Run(tc.name, func(t *testing.T) { 299 if errs := ValidatePodLogOptions(&tc.podLogOptions); len(errs) == 0 { 300 t.Errorf("expected error") 301 } 302 }) 303 } 304 } 305 306 func TestAccumulateUniqueHostPorts(t *testing.T) { 307 successCase := []struct { 308 name string 309 containers []v1.Container 310 accumulator *sets.String 311 fldPath *field.Path 312 }{{ 313 name: "HostPort is not allocated while containers use the same port with different protocol", 314 containers: []v1.Container{{ 315 Ports: []v1.ContainerPort{{ 316 HostPort: 8080, 317 Protocol: v1.ProtocolUDP, 318 }}, 319 }, { 320 Ports: []v1.ContainerPort{{ 321 HostPort: 8080, 322 Protocol: v1.ProtocolTCP, 323 }}, 324 }}, 325 accumulator: &sets.String{}, 326 fldPath: field.NewPath("spec", "containers"), 327 }, { 328 name: "HostPort is not allocated while containers use different ports", 329 containers: []v1.Container{{ 330 Ports: []v1.ContainerPort{{ 331 HostPort: 8080, 332 Protocol: v1.ProtocolUDP, 333 }}, 334 }, { 335 Ports: []v1.ContainerPort{{ 336 HostPort: 8081, 337 Protocol: v1.ProtocolUDP, 338 }}, 339 }}, 340 accumulator: &sets.String{}, 341 fldPath: field.NewPath("spec", "containers"), 342 }} 343 for _, tc := range successCase { 344 t.Run(tc.name, func(t *testing.T) { 345 if errs := AccumulateUniqueHostPorts(tc.containers, tc.accumulator, tc.fldPath); len(errs) != 0 { 346 t.Errorf("unexpected error: %v", errs) 347 } 348 }) 349 } 350 errorCase := []struct { 351 name string 352 containers []v1.Container 353 accumulator *sets.String 354 fldPath *field.Path 355 }{{ 356 name: "HostPort is already allocated while containers use the same port with UDP", 357 containers: []v1.Container{{ 358 Ports: []v1.ContainerPort{{ 359 HostPort: 8080, 360 Protocol: v1.ProtocolUDP, 361 }}, 362 }, { 363 Ports: []v1.ContainerPort{{ 364 HostPort: 8080, 365 Protocol: v1.ProtocolUDP, 366 }}, 367 }}, 368 accumulator: &sets.String{}, 369 fldPath: field.NewPath("spec", "containers"), 370 }, { 371 name: "HostPort is already allocated", 372 containers: []v1.Container{{ 373 Ports: []v1.ContainerPort{{ 374 HostPort: 8080, 375 Protocol: v1.ProtocolUDP, 376 }}, 377 }, { 378 Ports: []v1.ContainerPort{{ 379 HostPort: 8081, 380 Protocol: v1.ProtocolUDP, 381 }}, 382 }}, 383 accumulator: &sets.String{"8080/UDP": sets.Empty{}}, 384 fldPath: field.NewPath("spec", "containers"), 385 }} 386 for _, tc := range errorCase { 387 t.Run(tc.name, func(t *testing.T) { 388 if errs := AccumulateUniqueHostPorts(tc.containers, tc.accumulator, tc.fldPath); len(errs) == 0 { 389 t.Errorf("expected error, but get nil") 390 } 391 }) 392 } 393 }