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  })