k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/util/proto/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_test 18 19 import ( 20 "fmt" 21 "path/filepath" 22 23 . "github.com/onsi/ginkgo/v2" 24 . "github.com/onsi/gomega" 25 "sigs.k8s.io/yaml" 26 27 "k8s.io/kube-openapi/pkg/util/proto" 28 "k8s.io/kube-openapi/pkg/util/proto/testing" 29 "k8s.io/kube-openapi/pkg/util/proto/validation" 30 ) 31 32 var fakeSchema = testing.Fake{Path: filepath.Join("..", "testdata", "swagger.json")} 33 34 func Validate(models proto.Models, model string, data string) []error { 35 var obj interface{} 36 if err := yaml.Unmarshal([]byte(data), &obj); err != nil { 37 return []error{fmt.Errorf("pre-validation: failed to parse yaml: %v", err)} 38 } 39 return ValidateObj(models, model, obj) 40 } 41 42 // ValidateObj validates an object produced by decoding json or yaml. 43 // Numbers may be int64 or float64. 44 func ValidateObj(models proto.Models, model string, obj interface{}) []error { 45 schema := models.LookupModel(model) 46 if schema == nil { 47 return []error{fmt.Errorf("pre-validation: couldn't find model %s", model)} 48 } 49 50 return validation.ValidateModel(obj, schema, model) 51 } 52 53 var _ = Describe("resource validation using OpenAPI Schema", func() { 54 var models proto.Models 55 BeforeEach(func() { 56 s, err := fakeSchema.OpenAPISchema() 57 Expect(err).To(BeNil()) 58 models, err = proto.NewOpenAPIData(s) 59 Expect(err).To(BeNil()) 60 }) 61 62 It("finds Deployment in Schema and validates it", func() { 63 err := Validate(models, "io.k8s.api.apps.v1beta1.Deployment", ` 64 apiVersion: extensions/v1beta1 65 kind: Deployment 66 metadata: 67 labels: 68 name: redis-master 69 name: name 70 spec: 71 replicas: 1 72 template: 73 metadata: 74 labels: 75 app: redis 76 spec: 77 containers: 78 - image: redis 79 name: redis 80 `) 81 Expect(err).To(BeNil()) 82 }) 83 84 It("validates a valid pod", func() { 85 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 86 apiVersion: v1 87 kind: Pod 88 metadata: 89 labels: 90 name: redis-master 91 name: name 92 spec: 93 containers: 94 - args: 95 - this 96 - is 97 - an 98 - ok 99 - command 100 image: gcr.io/fake_project/fake_image:fake_tag 101 name: master 102 `) 103 Expect(err).To(BeNil()) 104 }) 105 106 It("finds invalid command (string instead of []string) in Json Pod", func() { 107 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 108 { 109 "kind": "Pod", 110 "apiVersion": "v1", 111 "metadata": { 112 "name": "name", 113 "labels": { 114 "name": "redis-master" 115 } 116 }, 117 "spec": { 118 "containers": [ 119 { 120 "name": "master", 121 "image": "gcr.io/fake_project/fake_image:fake_tag", 122 "args": "this is a bad command" 123 } 124 ] 125 } 126 } 127 `) 128 Expect(err).To(Equal([]error{ 129 validation.ValidationError{ 130 Path: "io.k8s.api.core.v1.Pod.spec.containers[0].args", 131 Err: validation.InvalidTypeError{ 132 Path: "io.k8s.api.core.v1.Container.args", 133 Expected: "array", 134 Actual: "string", 135 }, 136 }, 137 })) 138 }) 139 140 It("fails because hostPort is string instead of int", func() { 141 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 142 { 143 "kind": "Pod", 144 "apiVersion": "v1", 145 "metadata": { 146 "name": "apache-php", 147 "labels": { 148 "name": "apache-php" 149 } 150 }, 151 "spec": { 152 "volumes": [{ 153 "name": "shared-disk" 154 }], 155 "containers": [ 156 { 157 "name": "apache-php", 158 "image": "gcr.io/fake_project/fake_image:fake_tag", 159 "ports": [ 160 { 161 "name": "apache", 162 "hostPort": "13380", 163 "containerPort": 80, 164 "protocol": "TCP" 165 } 166 ], 167 "volumeMounts": [ 168 { 169 "name": "shared-disk", 170 "mountPath": "/var/www/html" 171 } 172 ] 173 } 174 ] 175 } 176 } 177 `) 178 179 Expect(err).To(Equal([]error{ 180 validation.ValidationError{ 181 Path: "io.k8s.api.core.v1.Pod.spec.containers[0].ports[0].hostPort", 182 Err: validation.InvalidTypeError{ 183 Path: "io.k8s.api.core.v1.ContainerPort.hostPort", 184 Expected: "integer", 185 Actual: "string", 186 }, 187 }, 188 })) 189 190 }) 191 192 It("fails because volume is not an array of object", func() { 193 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 194 { 195 "kind": "Pod", 196 "apiVersion": "v1", 197 "metadata": { 198 "name": "apache-php", 199 "labels": { 200 "name": "apache-php" 201 } 202 }, 203 "spec": { 204 "volumes": [ 205 "name": "shared-disk" 206 ], 207 "containers": [ 208 { 209 "name": "apache-php", 210 "image": "gcr.io/fake_project/fake_image:fake_tag", 211 "ports": [ 212 { 213 "name": "apache", 214 "hostPort": 13380, 215 "containerPort": 80, 216 "protocol": "TCP" 217 } 218 ], 219 "volumeMounts": [ 220 { 221 "name": "shared-disk", 222 "mountPath": "/var/www/html" 223 } 224 ] 225 } 226 ] 227 } 228 } 229 `) 230 Expect(err).To(BeNil()) 231 }) 232 233 It("fails because some string lists have empty strings", func() { 234 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 235 apiVersion: v1 236 kind: Pod 237 metadata: 238 labels: 239 name: redis-master 240 name: name 241 spec: 242 containers: 243 - image: gcr.io/fake_project/fake_image:fake_tag 244 name: master 245 args: 246 - 247 command: 248 - 249 `) 250 251 Expect(err).To(Equal([]error{ 252 validation.ValidationError{ 253 Path: "io.k8s.api.core.v1.Pod.spec.containers[0].args", 254 Err: validation.InvalidObjectTypeError{ 255 Path: "io.k8s.api.core.v1.Pod.spec.containers[0].args[0]", 256 Type: "nil", 257 }, 258 }, 259 validation.ValidationError{ 260 Path: "io.k8s.api.core.v1.Pod.spec.containers[0].command", 261 Err: validation.InvalidObjectTypeError{ 262 Path: "io.k8s.api.core.v1.Pod.spec.containers[0].command[0]", 263 Type: "nil", 264 }, 265 }, 266 })) 267 }) 268 269 It("fails if required fields are missing", func() { 270 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 271 apiVersion: v1 272 kind: Pod 273 metadata: 274 labels: 275 name: redis-master 276 name: name 277 spec: 278 containers: 279 - command: ["my", "command"] 280 `) 281 282 Expect(err).To(Equal([]error{ 283 validation.ValidationError{ 284 Path: "io.k8s.api.core.v1.Pod.spec.containers[0]", 285 Err: validation.MissingRequiredFieldError{ 286 Path: "io.k8s.api.core.v1.Container", 287 Field: "name", 288 }, 289 }, 290 validation.ValidationError{ 291 Path: "io.k8s.api.core.v1.Pod.spec.containers[0]", 292 Err: validation.MissingRequiredFieldError{ 293 Path: "io.k8s.api.core.v1.Container", 294 Field: "image", 295 }, 296 }, 297 })) 298 }) 299 300 It("fails if required fields are empty", func() { 301 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 302 apiVersion: v1 303 kind: Pod 304 metadata: 305 labels: 306 name: redis-master 307 name: name 308 spec: 309 containers: 310 - image: 311 name: 312 `) 313 314 Expect(err).To(Equal([]error{ 315 validation.ValidationError{ 316 Path: "io.k8s.api.core.v1.Pod.spec.containers[0]", 317 Err: validation.MissingRequiredFieldError{ 318 Path: "io.k8s.api.core.v1.Container", 319 Field: "name", 320 }, 321 }, 322 validation.ValidationError{ 323 Path: "io.k8s.api.core.v1.Pod.spec.containers[0]", 324 Err: validation.MissingRequiredFieldError{ 325 Path: "io.k8s.api.core.v1.Container", 326 Field: "image", 327 }, 328 }, 329 })) 330 }) 331 332 It("is fine with empty non-mandatory fields", func() { 333 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 334 apiVersion: v1 335 kind: Pod 336 metadata: 337 labels: 338 name: redis-master 339 name: name 340 spec: 341 containers: 342 - image: image 343 name: name 344 command: 345 `) 346 347 Expect(err).To(BeNil()) 348 }) 349 350 It("fails because apiVersion is not provided", func() { 351 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 352 kind: Pod 353 metadata: 354 name: name 355 spec: 356 containers: 357 - name: name 358 image: image 359 `) 360 Expect(err).To(BeNil()) 361 }) 362 363 It("fails because apiVersion type is not string and kind is not provided", func() { 364 err := Validate(models, "io.k8s.api.core.v1.Pod", ` 365 apiVersion: 1 366 metadata: 367 name: name 368 spec: 369 containers: 370 - name: name 371 image: image 372 `) 373 Expect(err).To(BeNil()) 374 }) 375 376 // verify integer literals are considered to be compatible with float schema fields 377 It("validates integer values for float fields", func() { 378 err := ValidateObj(models, "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.CustomResourceDefinition", map[string]interface{}{ 379 "apiVersion": "apiextensions.k8s.io/v1", 380 "kind": "CustomResourceDefinition", 381 "metadata": map[string]interface{}{"name": "foo"}, 382 "spec": map[string]interface{}{ 383 "scope": "Namespaced", 384 "group": "example.com", 385 "names": map[string]interface{}{ 386 "plural": "numbers", 387 "kind": "Number", 388 }, 389 "versions": []interface{}{ 390 map[string]interface{}{ 391 "name": "v1", 392 "served": true, 393 "storage": true, 394 "schema": map[string]interface{}{ 395 "openAPIV3Schema": map[string]interface{}{ 396 "properties": map[string]interface{}{ 397 "replicas": map[string]interface{}{ 398 "default": int64(1), 399 "minimum": int64(0), 400 "type": "integer", 401 }, 402 "resources": map[string]interface{}{ 403 "default": float64(1.1), 404 "minimum": float64(0.1), 405 "type": "number", 406 }, 407 }, 408 }, 409 }, 410 }, 411 }, 412 }, 413 }) 414 Expect(err).To(BeNil()) 415 }) 416 })