1
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
43
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
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 })
417
View as plain text