mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 06:54:01 +00:00
Merge pull request #50160 from apelisse/openapi-validation
Automatic merge from submit-queue (batch tested with PRs 50537, 49699, 50160, 49025, 50205) openapi: Add validation logic This allows validation of a yaml/json object against an openapi schema. A lot more testing would be needed to validate the logic, and also this is not plumbed in, so it can't be used by kubectl yet. **What this PR does / why we need it**: This is implementing validation against the openapi swagger spec rather than the old swagger spec. **Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes https://github.com/kubernetes/kubectl/issues/49 **Special notes for your reviewer**: **Release note**: ```release-note NONE ```
This commit is contained in:
commit
937fc0d113
@ -40,6 +40,7 @@ go_test(
|
||||
data = ["//api/openapi-spec:swagger-spec"],
|
||||
deps = [
|
||||
":go_default_library",
|
||||
"//pkg/kubectl/cmd/util/openapi/testing:go_default_library",
|
||||
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
|
||||
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
|
||||
"//vendor/github.com/onsi/ginkgo:go_default_library",
|
||||
@ -60,7 +61,11 @@ filegroup(
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//pkg/kubectl/cmd/util/openapi/testing:all-srcs",
|
||||
"//pkg/kubectl/cmd/util/openapi/validation:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
)
|
||||
|
||||
|
@ -132,7 +132,8 @@ func NewOpenAPIData(doc *openapi_v2.Document) (Resources, error) {
|
||||
|
||||
// Now, parse each model. We can validate that references exists.
|
||||
for _, namedSchema := range doc.GetDefinitions().GetAdditionalProperties() {
|
||||
schema, err := definitions.ParseSchema(namedSchema.GetValue(), &Path{key: namedSchema.GetName()})
|
||||
path := NewPath(namedSchema.GetName())
|
||||
schema, err := definitions.ParseSchema(namedSchema.GetValue(), &path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -252,7 +253,8 @@ func (d *Definitions) parseKind(s *openapi_v2.Schema, path *Path) (Schema, error
|
||||
|
||||
for _, namedSchema := range s.GetProperties().GetAdditionalProperties() {
|
||||
var err error
|
||||
fields[namedSchema.GetName()], err = d.ParseSchema(namedSchema.GetValue(), &Path{parent: path, key: namedSchema.GetName()})
|
||||
path := path.FieldPath(namedSchema.GetName())
|
||||
fields[namedSchema.GetName()], err = d.ParseSchema(namedSchema.GetValue(), &path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -77,6 +77,10 @@ type Path struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func NewPath(key string) Path {
|
||||
return Path{key: key}
|
||||
}
|
||||
|
||||
func (p *Path) Get() []string {
|
||||
if p == nil {
|
||||
return []string{}
|
||||
@ -92,7 +96,23 @@ func (p *Path) Len() int {
|
||||
}
|
||||
|
||||
func (p *Path) String() string {
|
||||
return strings.Join(p.Get(), ".")
|
||||
return strings.Join(p.Get(), "")
|
||||
}
|
||||
|
||||
// ArrayPath appends an array index and creates a new path
|
||||
func (p *Path) ArrayPath(i int) Path {
|
||||
return Path{
|
||||
parent: p,
|
||||
key: fmt.Sprintf("[%d]", i),
|
||||
}
|
||||
}
|
||||
|
||||
// FieldPath appends a field name and creates a new path
|
||||
func (p *Path) FieldPath(field string) Path {
|
||||
return Path{
|
||||
parent: p,
|
||||
key: fmt.Sprintf(".%s", field),
|
||||
}
|
||||
}
|
||||
|
||||
// BaseSchema holds data used by each types of schema.
|
||||
|
@ -23,16 +23,17 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
tst "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi/testing"
|
||||
)
|
||||
|
||||
var _ = Describe("Getting the Resources", func() {
|
||||
var client *fakeOpenAPIClient
|
||||
var client *tst.FakeClient
|
||||
var expectedData openapi.Resources
|
||||
var instance openapi.Getter
|
||||
|
||||
BeforeEach(func() {
|
||||
client = &fakeOpenAPIClient{}
|
||||
d, err := data.OpenAPISchema()
|
||||
client = tst.NewFakeClient(&fakeSchema)
|
||||
d, err := fakeSchema.OpenAPISchema()
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
expectedData, err = openapi.NewOpenAPIData(d)
|
||||
@ -43,34 +44,34 @@ var _ = Describe("Getting the Resources", func() {
|
||||
|
||||
Context("when the server returns a successful result", func() {
|
||||
It("should return the same data for multiple calls", func() {
|
||||
Expect(client.calls).To(Equal(0))
|
||||
Expect(client.Calls).To(Equal(0))
|
||||
|
||||
result, err := instance.Get()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).To(Equal(expectedData))
|
||||
Expect(client.calls).To(Equal(1))
|
||||
Expect(client.Calls).To(Equal(1))
|
||||
|
||||
result, err = instance.Get()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(result).To(Equal(expectedData))
|
||||
// No additional client calls expected
|
||||
Expect(client.calls).To(Equal(1))
|
||||
Expect(client.Calls).To(Equal(1))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the server returns an unsuccessful result", func() {
|
||||
It("should return the same instance for multiple calls.", func() {
|
||||
Expect(client.calls).To(Equal(0))
|
||||
Expect(client.Calls).To(Equal(0))
|
||||
|
||||
client.err = fmt.Errorf("expected error")
|
||||
client.Err = fmt.Errorf("expected error")
|
||||
_, err := instance.Get()
|
||||
Expect(err).To(Equal(client.err))
|
||||
Expect(client.calls).To(Equal(1))
|
||||
Expect(err).To(Equal(client.Err))
|
||||
Expect(client.Calls).To(Equal(1))
|
||||
|
||||
_, err = instance.Get()
|
||||
Expect(err).To(Equal(client.err))
|
||||
Expect(err).To(Equal(client.Err))
|
||||
// No additional client calls expected
|
||||
Expect(client.calls).To(Equal(1))
|
||||
Expect(client.Calls).To(Equal(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -17,17 +17,22 @@ limitations under the License.
|
||||
package openapi_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
tst "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi/testing"
|
||||
)
|
||||
|
||||
var fakeSchema = tst.Fake{Path: filepath.Join("..", "..", "..", "..", "..", "api", "openapi-spec", "swagger.json")}
|
||||
|
||||
var _ = Describe("Reading apps/v1beta1/Deployment from openAPIData", func() {
|
||||
var resources openapi.Resources
|
||||
BeforeEach(func() {
|
||||
s, err := data.OpenAPISchema()
|
||||
s, err := fakeSchema.OpenAPISchema()
|
||||
Expect(err).To(BeNil())
|
||||
resources, err = openapi.NewOpenAPIData(s)
|
||||
Expect(err).To(BeNil())
|
||||
@ -60,7 +65,7 @@ var _ = Describe("Reading apps/v1beta1/Deployment from openAPIData", func() {
|
||||
key := deployment.Fields["kind"].(*openapi.Primitive)
|
||||
Expect(key).ToNot(BeNil())
|
||||
Expect(key.Type).To(Equal("string"))
|
||||
Expect(key.GetPath().Get()).To(Equal([]string{"io.k8s.api.apps.v1beta1.Deployment", "kind"}))
|
||||
Expect(key.GetPath().Get()).To(Equal([]string{"io.k8s.api.apps.v1beta1.Deployment", ".kind"}))
|
||||
})
|
||||
|
||||
It("should have a apiVersion key of type string", func() {
|
||||
@ -68,7 +73,7 @@ var _ = Describe("Reading apps/v1beta1/Deployment from openAPIData", func() {
|
||||
key := deployment.Fields["apiVersion"].(*openapi.Primitive)
|
||||
Expect(key).ToNot(BeNil())
|
||||
Expect(key.Type).To(Equal("string"))
|
||||
Expect(key.GetPath().Get()).To(Equal([]string{"io.k8s.api.apps.v1beta1.Deployment", "apiVersion"}))
|
||||
Expect(key.GetPath().Get()).To(Equal([]string{"io.k8s.api.apps.v1beta1.Deployment", ".apiVersion"}))
|
||||
})
|
||||
|
||||
It("should have a metadata key of type Reference", func() {
|
||||
@ -136,7 +141,7 @@ var _ = Describe("Reading apps/v1beta1/Deployment from openAPIData", func() {
|
||||
var _ = Describe("Reading authorization.k8s.io/v1/SubjectAccessReview from openAPIData", func() {
|
||||
var resources openapi.Resources
|
||||
BeforeEach(func() {
|
||||
s, err := data.OpenAPISchema()
|
||||
s, err := fakeSchema.OpenAPISchema()
|
||||
Expect(err).To(BeNil())
|
||||
resources, err = openapi.NewOpenAPIData(s)
|
||||
Expect(err).To(BeNil())
|
||||
@ -171,15 +176,43 @@ var _ = Describe("Reading authorization.k8s.io/v1/SubjectAccessReview from openA
|
||||
extra := sarspec.Fields["extra"].(*openapi.Map)
|
||||
Expect(extra).ToNot(BeNil())
|
||||
Expect(extra.GetName()).To(Equal("Map of Array of string"))
|
||||
Expect(extra.GetPath().Get()).To(Equal([]string{"io.k8s.api.authorization.v1.SubjectAccessReviewSpec", "extra"}))
|
||||
Expect(extra.GetPath().Get()).To(Equal([]string{"io.k8s.api.authorization.v1.SubjectAccessReviewSpec", ".extra"}))
|
||||
array := extra.SubType.(*openapi.Array)
|
||||
Expect(array).ToNot(BeNil())
|
||||
Expect(array.GetName()).To(Equal("Array of string"))
|
||||
Expect(array.GetPath().Get()).To(Equal([]string{"io.k8s.api.authorization.v1.SubjectAccessReviewSpec", "extra"}))
|
||||
Expect(array.GetPath().Get()).To(Equal([]string{"io.k8s.api.authorization.v1.SubjectAccessReviewSpec", ".extra"}))
|
||||
str := array.SubType.(*openapi.Primitive)
|
||||
Expect(str).ToNot(BeNil())
|
||||
Expect(str.Type).To(Equal("string"))
|
||||
Expect(str.GetName()).To(Equal("string"))
|
||||
Expect(str.GetPath().Get()).To(Equal([]string{"io.k8s.api.authorization.v1.SubjectAccessReviewSpec", "extra"}))
|
||||
Expect(str.GetPath().Get()).To(Equal([]string{"io.k8s.api.authorization.v1.SubjectAccessReviewSpec", ".extra"}))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Path", func() {
|
||||
It("can be created by NewPath", func() {
|
||||
path := openapi.NewPath("key")
|
||||
Expect(path.String()).To(Equal("key"))
|
||||
})
|
||||
It("can create and print complex paths", func() {
|
||||
key := openapi.NewPath("key")
|
||||
array := key.ArrayPath(12)
|
||||
field := array.FieldPath("subKey")
|
||||
|
||||
Expect(field.String()).To(Equal("key[12].subKey"))
|
||||
})
|
||||
It("has a length", func() {
|
||||
key := openapi.NewPath("key")
|
||||
array := key.ArrayPath(12)
|
||||
field := array.FieldPath("subKey")
|
||||
|
||||
Expect(field.Len()).To(Equal(3))
|
||||
})
|
||||
It("can look like an array", func() {
|
||||
key := openapi.NewPath("key")
|
||||
array := key.ArrayPath(12)
|
||||
field := array.FieldPath("subKey")
|
||||
|
||||
Expect(field.Get()).To(Equal([]string{"key", "[12]", ".subKey"}))
|
||||
})
|
||||
})
|
||||
|
32
pkg/kubectl/cmd/util/openapi/testing/BUILD
Normal file
32
pkg/kubectl/cmd/util/openapi/testing/BUILD
Normal file
@ -0,0 +1,32 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["openapi.go"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
|
||||
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
|
||||
"//vendor/gopkg.in/yaml.v2:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
89
pkg/kubectl/cmd/util/openapi/testing/openapi.go
Normal file
89
pkg/kubectl/cmd/util/openapi/testing/openapi.go
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/googleapis/gnostic/OpenAPIv2"
|
||||
"github.com/googleapis/gnostic/compiler"
|
||||
)
|
||||
|
||||
// Fake opens and returns a openapi swagger from a file Path. It will
|
||||
// parse only once and then return the same copy everytime.
|
||||
type Fake struct {
|
||||
Path string
|
||||
|
||||
once sync.Once
|
||||
document *openapi_v2.Document
|
||||
err error
|
||||
}
|
||||
|
||||
// OpenAPISchema returns the openapi document and a potential error.
|
||||
func (f *Fake) OpenAPISchema() (*openapi_v2.Document, error) {
|
||||
f.once.Do(func() {
|
||||
_, err := os.Stat(f.Path)
|
||||
if err != nil {
|
||||
f.err = err
|
||||
return
|
||||
}
|
||||
spec, err := ioutil.ReadFile(f.Path)
|
||||
if err != nil {
|
||||
f.err = err
|
||||
return
|
||||
}
|
||||
var info yaml.MapSlice
|
||||
err = yaml.Unmarshal(spec, &info)
|
||||
if err != nil {
|
||||
f.err = err
|
||||
return
|
||||
}
|
||||
f.document, f.err = openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
|
||||
})
|
||||
return f.document, f.err
|
||||
}
|
||||
|
||||
// FakeClient implements a dummy OpenAPISchemaInterface that uses the
|
||||
// fake OpenAPI schema given as a parameter, and count the number of
|
||||
// call to the function.
|
||||
type FakeClient struct {
|
||||
Calls int
|
||||
Err error
|
||||
|
||||
fake *Fake
|
||||
}
|
||||
|
||||
// NewFakeClient creates a new FakeClient from the given Fake.
|
||||
func NewFakeClient(f *Fake) *FakeClient {
|
||||
return &FakeClient{fake: f}
|
||||
}
|
||||
|
||||
// OpenAPISchema returns a OpenAPI Document as returned by the fake, but
|
||||
// it also counts the number of calls.
|
||||
func (f *FakeClient) OpenAPISchema() (*openapi_v2.Document, error) {
|
||||
f.Calls = f.Calls + 1
|
||||
|
||||
if f.Err != nil {
|
||||
return nil, f.Err
|
||||
}
|
||||
|
||||
return f.fake.OpenAPISchema()
|
||||
}
|
60
pkg/kubectl/cmd/util/openapi/validation/BUILD
Normal file
60
pkg/kubectl/cmd/util/openapi/validation/BUILD
Normal file
@ -0,0 +1,60 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
licenses(["notice"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"errors.go",
|
||||
"types.go",
|
||||
"validation.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//pkg/api/util:go_default_library",
|
||||
"//pkg/kubectl/cmd/util/openapi:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_xtest",
|
||||
srcs = [
|
||||
"validation_suite_test.go",
|
||||
"validation_test.go",
|
||||
],
|
||||
data = ["//api/openapi-spec:swagger-spec"],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
":go_default_library",
|
||||
"//pkg/kubectl/cmd/util/openapi:go_default_library",
|
||||
"//pkg/kubectl/cmd/util/openapi/testing:go_default_library",
|
||||
"//vendor/github.com/onsi/ginkgo:go_default_library",
|
||||
"//vendor/github.com/onsi/ginkgo/config:go_default_library",
|
||||
"//vendor/github.com/onsi/ginkgo/types:go_default_library",
|
||||
"//vendor/github.com/onsi/gomega:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
79
pkg/kubectl/cmd/util/openapi/validation/errors.go
Normal file
79
pkg/kubectl/cmd/util/openapi/validation/errors.go
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Errors struct {
|
||||
errors []error
|
||||
}
|
||||
|
||||
func (e *Errors) Errors() []error {
|
||||
return e.errors
|
||||
}
|
||||
|
||||
func (e *Errors) AppendErrors(err ...error) {
|
||||
e.errors = append(e.errors, err...)
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return fmt.Sprintf("ValidationError(%s): %v", e.Path, e.Err)
|
||||
}
|
||||
|
||||
type InvalidTypeError struct {
|
||||
Path string
|
||||
Expected string
|
||||
Actual string
|
||||
}
|
||||
|
||||
func (e InvalidTypeError) Error() string {
|
||||
return fmt.Sprintf("invalid type for %s: got %q, expected %q", e.Path, e.Actual, e.Expected)
|
||||
}
|
||||
|
||||
type MissingRequiredFieldError struct {
|
||||
Path string
|
||||
Field string
|
||||
}
|
||||
|
||||
func (e MissingRequiredFieldError) Error() string {
|
||||
return fmt.Sprintf("missing required field %q in %s", e.Field, e.Path)
|
||||
}
|
||||
|
||||
type UnknownFieldError struct {
|
||||
Path string
|
||||
Field string
|
||||
}
|
||||
|
||||
func (e UnknownFieldError) Error() string {
|
||||
return fmt.Sprintf("unknown field %q in %s", e.Field, e.Path)
|
||||
}
|
||||
|
||||
type InvalidObjectTypeError struct {
|
||||
Path string
|
||||
Type string
|
||||
}
|
||||
|
||||
func (e InvalidObjectTypeError) Error() string {
|
||||
return fmt.Sprintf("unknown object type %q in %s", e.Type, e.Path)
|
||||
}
|
266
pkg/kubectl/cmd/util/openapi/validation/types.go
Normal file
266
pkg/kubectl/cmd/util/openapi/validation/types.go
Normal file
@ -0,0 +1,266 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
)
|
||||
|
||||
type ValidationItem interface {
|
||||
openapi.SchemaVisitor
|
||||
|
||||
Errors() []error
|
||||
Path() *openapi.Path
|
||||
}
|
||||
|
||||
type baseItem struct {
|
||||
errors Errors
|
||||
path openapi.Path
|
||||
}
|
||||
|
||||
// Errors returns the list of errors found for this item.
|
||||
func (item *baseItem) Errors() []error {
|
||||
return item.errors.Errors()
|
||||
}
|
||||
|
||||
// AddValidationError wraps the given error into a ValidationError and
|
||||
// attaches it to this item.
|
||||
func (item *baseItem) AddValidationError(err error) {
|
||||
item.errors.AppendErrors(ValidationError{Path: item.path.String(), Err: err})
|
||||
}
|
||||
|
||||
// AddError adds a regular (non-validation related) error to the list.
|
||||
func (item *baseItem) AddError(err error) {
|
||||
item.errors.AppendErrors(err)
|
||||
}
|
||||
|
||||
// CopyErrors adds a list of errors to this item. This is useful to copy
|
||||
// errors from subitems.
|
||||
func (item *baseItem) CopyErrors(errs []error) {
|
||||
item.errors.AppendErrors(errs...)
|
||||
}
|
||||
|
||||
// Path returns the path of this item, helps print useful errors.
|
||||
func (item *baseItem) Path() *openapi.Path {
|
||||
return &item.path
|
||||
}
|
||||
|
||||
// mapItem represents a map entry in the yaml.
|
||||
type mapItem struct {
|
||||
baseItem
|
||||
|
||||
Map map[string]interface{}
|
||||
}
|
||||
|
||||
func (item *mapItem) sortedKeys() []string {
|
||||
sortedKeys := []string{}
|
||||
for key := range item.Map {
|
||||
sortedKeys = append(sortedKeys, key)
|
||||
}
|
||||
sort.Strings(sortedKeys)
|
||||
return sortedKeys
|
||||
}
|
||||
|
||||
var _ ValidationItem = &mapItem{}
|
||||
|
||||
func (item *mapItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: "map"})
|
||||
}
|
||||
|
||||
func (item *mapItem) VisitArray(schema *openapi.Array) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: "map"})
|
||||
}
|
||||
|
||||
func (item *mapItem) VisitMap(schema *openapi.Map) {
|
||||
for _, key := range item.sortedKeys() {
|
||||
subItem, err := itemFactory(item.Path().FieldPath(key), item.Map[key])
|
||||
if err != nil {
|
||||
item.AddError(err)
|
||||
continue
|
||||
}
|
||||
schema.SubType.Accept(subItem)
|
||||
item.CopyErrors(subItem.Errors())
|
||||
}
|
||||
}
|
||||
|
||||
func (item *mapItem) VisitKind(schema *openapi.Kind) {
|
||||
// Verify each sub-field.
|
||||
for _, key := range item.sortedKeys() {
|
||||
subItem, err := itemFactory(item.Path().FieldPath(key), item.Map[key])
|
||||
if err != nil {
|
||||
item.AddError(err)
|
||||
continue
|
||||
}
|
||||
if _, ok := schema.Fields[key]; !ok {
|
||||
item.AddValidationError(UnknownFieldError{Path: schema.GetPath().String(), Field: key})
|
||||
continue
|
||||
}
|
||||
schema.Fields[key].Accept(subItem)
|
||||
item.CopyErrors(subItem.Errors())
|
||||
}
|
||||
|
||||
// Verify that all required fields are present.
|
||||
for _, required := range schema.RequiredFields {
|
||||
if _, ok := item.Map[required]; !ok {
|
||||
item.AddValidationError(MissingRequiredFieldError{Path: schema.GetPath().String(), Field: required})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// arrayItem represents a yaml array.
|
||||
type arrayItem struct {
|
||||
baseItem
|
||||
|
||||
Array []interface{}
|
||||
}
|
||||
|
||||
var _ ValidationItem = &arrayItem{}
|
||||
|
||||
func (item *arrayItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: "array"})
|
||||
}
|
||||
|
||||
func (item *arrayItem) VisitArray(schema *openapi.Array) {
|
||||
for i, v := range item.Array {
|
||||
subItem, err := itemFactory(item.Path().ArrayPath(i), v)
|
||||
if err != nil {
|
||||
item.AddError(err)
|
||||
continue
|
||||
}
|
||||
schema.SubType.Accept(subItem)
|
||||
item.CopyErrors(subItem.Errors())
|
||||
}
|
||||
}
|
||||
|
||||
func (item *arrayItem) VisitMap(schema *openapi.Map) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: "map"})
|
||||
}
|
||||
|
||||
func (item *arrayItem) VisitKind(schema *openapi.Kind) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: "map"})
|
||||
}
|
||||
|
||||
// primitiveItem represents a yaml value.
|
||||
type primitiveItem struct {
|
||||
baseItem
|
||||
|
||||
Value interface{}
|
||||
Kind string
|
||||
}
|
||||
|
||||
var _ ValidationItem = &primitiveItem{}
|
||||
|
||||
func (item *primitiveItem) VisitPrimitive(schema *openapi.Primitive) {
|
||||
// Some types of primitives can match more than one (a number
|
||||
// can be a string, but not the other way around). Return from
|
||||
// the switch if we have a valid possible type conversion
|
||||
// NOTE(apelisse): This logic is blindly copied from the
|
||||
// existing swagger logic, and I'm not sure I agree with it.
|
||||
switch schema.Type {
|
||||
case openapi.Boolean:
|
||||
switch item.Kind {
|
||||
case openapi.Boolean:
|
||||
return
|
||||
}
|
||||
case openapi.Integer:
|
||||
switch item.Kind {
|
||||
case openapi.Integer, openapi.Number:
|
||||
return
|
||||
}
|
||||
case openapi.Number:
|
||||
switch item.Kind {
|
||||
case openapi.Number:
|
||||
return
|
||||
}
|
||||
case openapi.String:
|
||||
return
|
||||
}
|
||||
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: item.Kind})
|
||||
}
|
||||
|
||||
func (item *primitiveItem) VisitArray(schema *openapi.Array) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: item.Kind})
|
||||
}
|
||||
|
||||
func (item *primitiveItem) VisitMap(schema *openapi.Map) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "map", Actual: item.Kind})
|
||||
}
|
||||
|
||||
func (item *primitiveItem) VisitKind(schema *openapi.Kind) {
|
||||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: "map", Actual: item.Kind})
|
||||
}
|
||||
|
||||
// itemFactory creates the relevant item type/visitor based on the current yaml type.
|
||||
func itemFactory(path openapi.Path, v interface{}) (ValidationItem, error) {
|
||||
// We need to special case for no-type fields in yaml (e.g. empty item in list)
|
||||
if v == nil {
|
||||
return nil, InvalidObjectTypeError{Type: "nil", Path: path.String()}
|
||||
}
|
||||
kind := reflect.TypeOf(v).Kind()
|
||||
switch kind {
|
||||
case reflect.Bool:
|
||||
return &primitiveItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Value: v,
|
||||
Kind: openapi.Boolean,
|
||||
}, nil
|
||||
case reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64:
|
||||
return &primitiveItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Value: v,
|
||||
Kind: openapi.Integer,
|
||||
}, nil
|
||||
case reflect.Float32,
|
||||
reflect.Float64:
|
||||
return &primitiveItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Value: v,
|
||||
Kind: openapi.Number,
|
||||
}, nil
|
||||
case reflect.String:
|
||||
return &primitiveItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Value: v,
|
||||
Kind: openapi.String,
|
||||
}, nil
|
||||
case reflect.Array,
|
||||
reflect.Slice:
|
||||
return &arrayItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Array: v.([]interface{}),
|
||||
}, nil
|
||||
case reflect.Map:
|
||||
return &mapItem{
|
||||
baseItem: baseItem{path: path},
|
||||
Map: v.(map[string]interface{}),
|
||||
}, nil
|
||||
}
|
||||
return nil, InvalidObjectTypeError{Type: kind.String(), Path: path.String()}
|
||||
}
|
103
pkg/kubectl/cmd/util/openapi/validation/validation.go
Normal file
103
pkg/kubectl/cmd/util/openapi/validation/validation.go
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
apiutil "k8s.io/kubernetes/pkg/api/util"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
)
|
||||
|
||||
type SchemaValidation struct {
|
||||
resources openapi.Resources
|
||||
}
|
||||
|
||||
func NewSchemaValidation(resources openapi.Resources) *SchemaValidation {
|
||||
return &SchemaValidation{
|
||||
resources: resources,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *SchemaValidation) Validate(data []byte) error {
|
||||
obj, err := parse(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gvk, err := getObjectKind(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resource := v.resources.LookupResource(gvk)
|
||||
if resource == nil {
|
||||
return fmt.Errorf("unknown object type %q", gvk)
|
||||
}
|
||||
|
||||
rootValidation, err := itemFactory(openapi.NewPath(gvk.Kind), obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resource.Accept(rootValidation)
|
||||
errs := rootValidation.Errors()
|
||||
if errs != nil {
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(data []byte) (interface{}, error) {
|
||||
var obj interface{}
|
||||
out, err := yaml.ToJSON(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(out, &obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func getObjectKind(object interface{}) (schema.GroupVersionKind, error) {
|
||||
fields := object.(map[string]interface{})
|
||||
if fields == nil {
|
||||
return schema.GroupVersionKind{}, errors.New("invalid object to validate")
|
||||
}
|
||||
apiVersion := fields["apiVersion"]
|
||||
if apiVersion == nil {
|
||||
return schema.GroupVersionKind{}, errors.New("apiVersion not set")
|
||||
}
|
||||
if _, ok := apiVersion.(string); !ok {
|
||||
return schema.GroupVersionKind{}, errors.New("apiVersion isn't string type")
|
||||
}
|
||||
version := apiutil.GetVersion(apiVersion.(string))
|
||||
kind := fields["kind"]
|
||||
if kind == nil {
|
||||
return schema.GroupVersionKind{}, errors.New("kind not set")
|
||||
}
|
||||
if _, ok := kind.(string); !ok {
|
||||
return schema.GroupVersionKind{}, errors.New("kind isn't string type")
|
||||
}
|
||||
|
||||
return schema.GroupVersionKind{Kind: kind.(string), Version: version}, nil
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation_test
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/ginkgo/config"
|
||||
. "github.com/onsi/ginkgo/types"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOpenapi(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecsWithDefaultAndCustomReporters(t, "Openapi Suite", []Reporter{newlineReporter{}})
|
||||
}
|
||||
|
||||
// Print a newline after the default newlineReporter due to issue
|
||||
// https://github.com/jstemmer/go-junit-report/issues/31
|
||||
type newlineReporter struct{}
|
||||
|
||||
func (newlineReporter) SpecSuiteWillBegin(config GinkgoConfigType, summary *SuiteSummary) {}
|
||||
|
||||
func (newlineReporter) BeforeSuiteDidRun(setupSummary *SetupSummary) {}
|
||||
|
||||
func (newlineReporter) AfterSuiteDidRun(setupSummary *SetupSummary) {}
|
||||
|
||||
func (newlineReporter) SpecWillRun(specSummary *SpecSummary) {}
|
||||
|
||||
func (newlineReporter) SpecDidComplete(specSummary *SpecSummary) {}
|
||||
|
||||
// SpecSuiteDidEnd Prints a newline between "35 Passed | 0 Failed | 0 Pending | 0 Skipped" and "--- PASS:"
|
||||
func (newlineReporter) SpecSuiteDidEnd(summary *SuiteSummary) { fmt.Printf("\n") }
|
222
pkg/kubectl/cmd/util/openapi/validation/validation_test.go
Normal file
222
pkg/kubectl/cmd/util/openapi/validation/validation_test.go
Normal file
@ -0,0 +1,222 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
|
||||
tst "k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi/testing"
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi/validation"
|
||||
)
|
||||
|
||||
var fakeSchema = tst.Fake{Path: filepath.Join("..", "..", "..", "..", "..", "..", "api", "openapi-spec", "swagger.json")}
|
||||
|
||||
var _ = Describe("resource validation using OpenAPI Schema", func() {
|
||||
var validator *validation.SchemaValidation
|
||||
BeforeEach(func() {
|
||||
s, err := fakeSchema.OpenAPISchema()
|
||||
Expect(err).To(BeNil())
|
||||
resources, err := openapi.NewOpenAPIData(s)
|
||||
Expect(err).To(BeNil())
|
||||
validator = validation.NewSchemaValidation(resources)
|
||||
Expect(validator).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("validates a valid pod", func() {
|
||||
err := validator.Validate([]byte(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
labels:
|
||||
name: redis-master
|
||||
name: name
|
||||
spec:
|
||||
containers:
|
||||
- args:
|
||||
- this
|
||||
- is
|
||||
- an
|
||||
- ok
|
||||
- command
|
||||
image: gcr.io/fake_project/fake_image:fake_tag
|
||||
name: master
|
||||
`))
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
|
||||
It("finds invalid command (string instead of []string) in Json Pod", func() {
|
||||
err := validator.Validate([]byte(`
|
||||
{
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {
|
||||
"name": "name",
|
||||
"labels": {
|
||||
"name": "redis-master"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "master",
|
||||
"image": "gcr.io/fake_project/fake_image:fake_tag",
|
||||
"args": "this is a bad command"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`))
|
||||
Expect(err).To(Equal(utilerrors.NewAggregate([]error{
|
||||
validation.ValidationError{
|
||||
Path: "Pod.spec.containers[0].args",
|
||||
Err: validation.InvalidTypeError{
|
||||
Path: "io.k8s.api.core.v1.Container.args",
|
||||
Expected: "array",
|
||||
Actual: "string",
|
||||
},
|
||||
},
|
||||
})))
|
||||
})
|
||||
|
||||
It("fails because hostPort is string instead of int", func() {
|
||||
err := validator.Validate([]byte(`
|
||||
{
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {
|
||||
"name": "apache-php",
|
||||
"labels": {
|
||||
"name": "apache-php"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"volumes": [{
|
||||
"name": "shared-disk"
|
||||
}],
|
||||
"containers": [
|
||||
{
|
||||
"name": "apache-php",
|
||||
"image": "gcr.io/fake_project/fake_image:fake_tag",
|
||||
"ports": [
|
||||
{
|
||||
"name": "apache",
|
||||
"hostPort": "13380",
|
||||
"containerPort": 80,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"volumeMounts": [
|
||||
{
|
||||
"name": "shared-disk",
|
||||
"mountPath": "/var/www/html"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`))
|
||||
|
||||
Expect(err).To(Equal(utilerrors.NewAggregate([]error{
|
||||
validation.ValidationError{
|
||||
Path: "Pod.spec.containers[0].ports[0].hostPort",
|
||||
Err: validation.InvalidTypeError{
|
||||
Path: "io.k8s.api.core.v1.ContainerPort.hostPort",
|
||||
Expected: "integer",
|
||||
Actual: "string",
|
||||
},
|
||||
},
|
||||
})))
|
||||
|
||||
})
|
||||
|
||||
It("fails because volume is not an array of object", func() {
|
||||
err := validator.Validate([]byte(`
|
||||
{
|
||||
"kind": "Pod",
|
||||
"apiVersion": "v1",
|
||||
"metadata": {
|
||||
"name": "apache-php",
|
||||
"labels": {
|
||||
"name": "apache-php"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"volumes": [
|
||||
"name": "shared-disk"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "apache-php",
|
||||
"image": "gcr.io/fake_project/fake_image:fake_tag",
|
||||
"ports": [
|
||||
{
|
||||
"name": "apache",
|
||||
"hostPort": 13380,
|
||||
"containerPort": 80,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"volumeMounts": [
|
||||
{
|
||||
"name": "shared-disk",
|
||||
"mountPath": "/var/www/html"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`))
|
||||
Expect(err.Error()).To(Equal("invalid character ':' after array element"))
|
||||
})
|
||||
|
||||
It("fails because some string lists have empty strings", func() {
|
||||
err := validator.Validate([]byte(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
labels:
|
||||
name: redis-master
|
||||
name: name
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/fake_project/fake_image:fake_tag
|
||||
name: master
|
||||
args:
|
||||
-
|
||||
command:
|
||||
-
|
||||
`))
|
||||
|
||||
Expect(err).To(Equal(utilerrors.NewAggregate([]error{
|
||||
validation.InvalidObjectTypeError{
|
||||
Path: "Pod.spec.containers[0].args[0]",
|
||||
Type: "nil",
|
||||
},
|
||||
validation.InvalidObjectTypeError{
|
||||
Path: "Pod.spec.containers[0].command[0]",
|
||||
Type: "nil",
|
||||
},
|
||||
})))
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user