Merge pull request #49146 from apelisse/openapi-new-structure

Automatic merge from submit-queue (batch tested with PRs 49665, 49689, 49495, 49146, 48934)

openapi: refactor into more generic structure

**What this PR does / why we need it**:
Refactor the openapi schema to be a more generic structure that can be
"visited" to get more specific types. Will be used by validation.

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: #44589

**Special notes for your reviewer**:

**Release note**:
```release-note
NONE
```
This commit is contained in:
Kubernetes Submit Queue 2017-07-27 21:45:36 -07:00 committed by GitHub
commit bc3c5bc0d6
14 changed files with 656 additions and 808 deletions

View File

@ -217,7 +217,6 @@ go_test(
"//pkg/printers/internalversion:go_default_library", "//pkg/printers/internalversion:go_default_library",
"//pkg/util/i18n:go_default_library", "//pkg/util/i18n:go_default_library",
"//pkg/util/strings:go_default_library", "//pkg/util/strings:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/gopkg.in/yaml.v2:go_default_library", "//vendor/gopkg.in/yaml.v2:go_default_library",

View File

@ -566,13 +566,13 @@ func outputOptsForMappingFromOpenAPI(f cmdutil.Factory, openAPIcacheDir string,
return nil, false return nil, false
} }
// Found openapi metadata for this resource // Found openapi metadata for this resource
kind, found := api.LookupResource(mapping.GroupVersionKind) schema := api.LookupResource(mapping.GroupVersionKind)
if !found { if schema == nil {
// Kind not found, return empty columns // Schema not found, return empty columns
return nil, false return nil, false
} }
columns, found := openapi.GetPrintColumns(kind.Extensions) columns, found := openapi.GetPrintColumns(schema.GetExtensions())
if !found { if !found {
// Extension not found, return empty columns // Extension not found, return empty columns
return nil, false return nil, false

View File

@ -26,8 +26,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/go-openapi/spec"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -218,20 +216,27 @@ func TestGetObjectsWithOpenAPIOutputFormatPresent(t *testing.T) {
} }
} }
func testOpenAPISchemaData() (*openapi.Resources, error) { type FakeResources struct {
return &openapi.Resources{ resources map[schema.GroupVersionKind]openapi.Schema
GroupVersionKindToName: map[schema.GroupVersionKind]string{ }
func (f FakeResources) LookupResource(s schema.GroupVersionKind) openapi.Schema {
return f.resources[s]
}
var _ openapi.Resources = &FakeResources{}
func testOpenAPISchemaData() (openapi.Resources, error) {
return &FakeResources{
resources: map[schema.GroupVersionKind]openapi.Schema{
{ {
Version: "v1", Version: "v1",
Kind: "Pod", Kind: "Pod",
}: "io.k8s.kubernetes.pkg.api.v1.Pod", }: &openapi.Primitive{
}, BaseSchema: openapi.BaseSchema{
NameToDefinition: map[string]openapi.Kind{ Extensions: map[string]interface{}{
"io.k8s.kubernetes.pkg.api.v1.Pod": { "x-kubernetes-print-columns": "custom-columns=NAME:.metadata.name,RSRC:.metadata.resourceVersion",
Name: "io.k8s.kubernetes.pkg.api.v1.Pod", },
IsResource: false,
Extensions: spec.Extensions{
"x-kubernetes-print-columns": "custom-columns=NAME:.metadata.name,RSRC:.metadata.resourceVersion",
}, },
}, },
}, },

View File

@ -243,7 +243,7 @@ type TestFactory struct {
ClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error) ClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error)
UnstructuredClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error) UnstructuredClientForMappingFunc func(mapping *meta.RESTMapping) (resource.RESTClient, error)
OpenAPISchemaFunc func() (*openapi.Resources, error) OpenAPISchemaFunc func() (openapi.Resources, error)
} }
type FakeFactory struct { type FakeFactory struct {
@ -418,8 +418,8 @@ func (f *FakeFactory) SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDeclar
return nil, nil return nil, nil
} }
func (f *FakeFactory) OpenAPISchema(cacheDir string) (*openapi.Resources, error) { func (f *FakeFactory) OpenAPISchema(cacheDir string) (openapi.Resources, error) {
return &openapi.Resources{}, nil return nil, nil
} }
func (f *FakeFactory) DefaultNamespace() (string, bool, error) { func (f *FakeFactory) DefaultNamespace() (string, bool, error) {
@ -756,11 +756,11 @@ func (f *fakeAPIFactory) SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDec
return nil, nil return nil, nil
} }
func (f *fakeAPIFactory) OpenAPISchema(cacheDir string) (*openapi.Resources, error) { func (f *fakeAPIFactory) OpenAPISchema(cacheDir string) (openapi.Resources, error) {
if f.tf.OpenAPISchemaFunc != nil { if f.tf.OpenAPISchemaFunc != nil {
return f.tf.OpenAPISchemaFunc() return f.tf.OpenAPISchemaFunc()
} }
return &openapi.Resources{}, nil return nil, nil
} }
func NewAPIFactory() (cmdutil.Factory, *TestFactory, runtime.Codec, runtime.NegotiatedSerializer) { func NewAPIFactory() (cmdutil.Factory, *TestFactory, runtime.Codec, runtime.NegotiatedSerializer) {

View File

@ -224,7 +224,7 @@ type ObjectMappingFactory interface {
// SwaggerSchema returns the schema declaration for the provided group version kind. // SwaggerSchema returns the schema declaration for the provided group version kind.
SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDeclaration, error) SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDeclaration, error)
// OpenAPISchema returns the schema openapi schema definiton // OpenAPISchema returns the schema openapi schema definiton
OpenAPISchema(cacheDir string) (*openapi.Resources, error) OpenAPISchema(cacheDir string) (openapi.Resources, error)
} }
// BuilderFactory holds the second level of factory methods. These functions depend upon ObjectMappingFactory and ClientAccessFactory methods. // BuilderFactory holds the second level of factory methods. These functions depend upon ObjectMappingFactory and ClientAccessFactory methods.

View File

@ -445,7 +445,7 @@ func (f *ring1Factory) SwaggerSchema(gvk schema.GroupVersionKind) (*swagger.ApiD
// schema will be cached separately for different client / server combinations. // schema will be cached separately for different client / server combinations.
// Note, the cache will not be invalidated if the server changes its open API schema without // Note, the cache will not be invalidated if the server changes its open API schema without
// changing the server version. // changing the server version.
func (f *ring1Factory) OpenAPISchema(cacheDir string) (*openapi.Resources, error) { func (f *ring1Factory) OpenAPISchema(cacheDir string) (openapi.Resources, error) {
discovery, err := f.clientAccessFactory.DiscoveryClient() discovery, err := f.clientAccessFactory.DiscoveryClient()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -12,6 +12,7 @@ go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"doc.go", "doc.go",
"document.go",
"extensions.go", "extensions.go",
"openapi.go", "openapi.go",
"openapi_cache.go", "openapi_cache.go",
@ -22,10 +23,10 @@ go_library(
"//pkg/version:go_default_library", "//pkg/version:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/golang/protobuf/proto:go_default_library",
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library", "//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
"//vendor/gopkg.in/yaml.v2:go_default_library", "//vendor/gopkg.in/yaml.v2:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library", "//vendor/k8s.io/client-go/discovery:go_default_library",
], ],
) )
@ -43,7 +44,6 @@ go_test(
tags = ["automanaged"], tags = ["automanaged"],
deps = [ deps = [
"//pkg/kubectl/cmd/util/openapi:go_default_library", "//pkg/kubectl/cmd/util/openapi:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library", "//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library", "//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
"//vendor/github.com/onsi/ginkgo:go_default_library", "//vendor/github.com/onsi/ginkgo:go_default_library",

View File

@ -0,0 +1,338 @@
/*
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 openapi
import (
"fmt"
"strings"
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
yaml "gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/runtime/schema"
)
func newSchemaError(path *Path, format string, a ...interface{}) error {
err := fmt.Sprintf(format, a...)
if path.Len() == 0 {
return fmt.Errorf("SchemaError: %v", err)
}
return fmt.Errorf("SchemaError(%v): %v", path, err)
}
// groupVersionKindExtensionKey is the key used to lookup the
// GroupVersionKind value for an object definition from the
// definition's "extensions" map.
const groupVersionKindExtensionKey = "x-kubernetes-group-version-kind"
func vendorExtensionToMap(e []*openapi_v2.NamedAny) map[string]interface{} {
values := map[string]interface{}{}
for _, na := range e {
if na.GetName() == "" || na.GetValue() == nil {
continue
}
if na.GetValue().GetYaml() == "" {
continue
}
var value interface{}
err := yaml.Unmarshal([]byte(na.GetValue().GetYaml()), &value)
if err != nil {
continue
}
values[na.GetName()] = value
}
return values
}
// Get and parse GroupVersionKind from the extension. Returns empty if it doesn't have one.
func parseGroupVersionKind(s *openapi_v2.Schema) schema.GroupVersionKind {
extensionMap := vendorExtensionToMap(s.GetVendorExtension())
// Get the extensions
gvkExtension, ok := extensionMap[groupVersionKindExtensionKey]
if !ok {
return schema.GroupVersionKind{}
}
// gvk extension must be a list of 1 element.
gvkList, ok := gvkExtension.([]interface{})
if !ok {
return schema.GroupVersionKind{}
}
if len(gvkList) != 1 {
return schema.GroupVersionKind{}
}
gvk := gvkList[0]
// gvk extension list must be a map with group, version, and
// kind fields
gvkMap, ok := gvk.(map[interface{}]interface{})
if !ok {
return schema.GroupVersionKind{}
}
group, ok := gvkMap["group"].(string)
if !ok {
return schema.GroupVersionKind{}
}
version, ok := gvkMap["version"].(string)
if !ok {
return schema.GroupVersionKind{}
}
kind, ok := gvkMap["kind"].(string)
if !ok {
return schema.GroupVersionKind{}
}
return schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
}
}
// Definitions is an implementation of `Resources`. It looks for
// resources in an openapi Schema.
type Definitions struct {
models map[string]Schema
resources map[schema.GroupVersionKind]string
}
var _ Resources = &Definitions{}
// NewOpenAPIData creates a new `Resources` out of the openapi document.
func NewOpenAPIData(doc *openapi_v2.Document) (Resources, error) {
definitions := Definitions{
models: map[string]Schema{},
resources: map[schema.GroupVersionKind]string{},
}
// Save the list of all models first. This will allow us to
// validate that we don't have any dangling reference.
for _, namedSchema := range doc.GetDefinitions().GetAdditionalProperties() {
definitions.models[namedSchema.GetName()] = nil
}
// 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()})
if err != nil {
return nil, err
}
definitions.models[namedSchema.GetName()] = schema
gvk := parseGroupVersionKind(namedSchema.GetValue())
if len(gvk.Kind) > 0 {
definitions.resources[gvk] = namedSchema.GetName()
}
}
return &definitions, nil
}
// We believe the schema is a reference, verify that and returns a new
// Schema
func (d *Definitions) parseReference(s *openapi_v2.Schema, path *Path) (Schema, error) {
if len(s.GetProperties().GetAdditionalProperties()) > 0 {
return nil, newSchemaError(path, "unallowed embedded type definition")
}
if len(s.GetType().GetValue()) > 0 {
return nil, newSchemaError(path, "definition reference can't have a type")
}
if !strings.HasPrefix(s.GetXRef(), "#/definitions/") {
return nil, newSchemaError(path, "unallowed reference to non-definition %q", s.GetXRef())
}
reference := strings.TrimPrefix(s.GetXRef(), "#/definitions/")
if _, ok := d.models[reference]; !ok {
return nil, newSchemaError(path, "unknown model in reference: %q", reference)
}
return &Reference{
Reference: reference,
definitions: d,
}, nil
}
func (d *Definitions) parseBaseSchema(s *openapi_v2.Schema, path *Path) BaseSchema {
return BaseSchema{
Description: s.GetDescription(),
Extensions: vendorExtensionToMap(s.GetVendorExtension()),
Path: *path,
}
}
// We believe the schema is a map, verify and return a new schema
func (d *Definitions) parseMap(s *openapi_v2.Schema, path *Path) (Schema, error) {
if len(s.GetType().GetValue()) != 0 && s.GetType().GetValue()[0] != object {
return nil, newSchemaError(path, "invalid object type")
}
if s.GetAdditionalProperties().GetSchema() == nil {
return nil, newSchemaError(path, "invalid object doesn't have additional properties")
}
sub, err := d.ParseSchema(s.GetAdditionalProperties().GetSchema(), path)
if err != nil {
return nil, err
}
return &Map{
BaseSchema: d.parseBaseSchema(s, path),
SubType: sub,
}, nil
}
func (d *Definitions) parsePrimitive(s *openapi_v2.Schema, path *Path) (Schema, error) {
var t string
if len(s.GetType().GetValue()) > 1 {
return nil, newSchemaError(path, "primitive can't have more than 1 type")
}
if len(s.GetType().GetValue()) == 1 {
t = s.GetType().GetValue()[0]
}
switch t {
case String:
case Number:
case Integer:
case Boolean:
case "": // Some models are completely empty, and can be safely ignored.
// Do nothing
default:
return nil, newSchemaError(path, "Unknown primitive type: %q", t)
}
return &Primitive{
BaseSchema: d.parseBaseSchema(s, path),
Type: t,
Format: s.GetFormat(),
}, nil
}
func (d *Definitions) parseArray(s *openapi_v2.Schema, path *Path) (Schema, error) {
if len(s.GetType().GetValue()) != 1 {
return nil, newSchemaError(path, "array should have exactly one type")
}
if s.GetType().GetValue()[0] != array {
return nil, newSchemaError(path, `array should have type "array"`)
}
if len(s.GetItems().GetSchema()) != 1 {
return nil, newSchemaError(path, "array should have exactly one sub-item")
}
sub, err := d.ParseSchema(s.GetItems().GetSchema()[0], path)
if err != nil {
return nil, err
}
return &Array{
BaseSchema: d.parseBaseSchema(s, path),
SubType: sub,
}, nil
}
func (d *Definitions) parseKind(s *openapi_v2.Schema, path *Path) (Schema, error) {
if len(s.GetType().GetValue()) != 0 && s.GetType().GetValue()[0] != object {
return nil, newSchemaError(path, "invalid object type")
}
if s.GetProperties() == nil {
return nil, newSchemaError(path, "object doesn't have properties")
}
fields := map[string]Schema{}
for _, namedSchema := range s.GetProperties().GetAdditionalProperties() {
var err error
fields[namedSchema.GetName()], err = d.ParseSchema(namedSchema.GetValue(), &Path{parent: path, key: namedSchema.GetName()})
if err != nil {
return nil, err
}
}
return &Kind{
BaseSchema: d.parseBaseSchema(s, path),
RequiredFields: s.GetRequired(),
Fields: fields,
}, nil
}
// ParseSchema creates a walkable Schema from an openapi schema. While
// this function is public, it doesn't leak through the interface.
func (d *Definitions) ParseSchema(s *openapi_v2.Schema, path *Path) (Schema, error) {
if len(s.GetType().GetValue()) == 1 {
t := s.GetType().GetValue()[0]
switch t {
case object:
return d.parseMap(s, path)
case array:
return d.parseArray(s, path)
}
}
if s.GetXRef() != "" {
return d.parseReference(s, path)
}
if s.GetProperties() != nil {
return d.parseKind(s, path)
}
return d.parsePrimitive(s, path)
}
// LookupResource is public through the interface of Resources. It
// returns a visitable schema from the given group-version-kind.
func (d *Definitions) LookupResource(gvk schema.GroupVersionKind) Schema {
modelName, found := d.resources[gvk]
if !found {
return nil
}
model, found := d.models[modelName]
if !found {
return nil
}
return model
}
// SchemaReference doesn't match a specific type. It's mostly a
// pass-through type.
type Reference struct {
Reference string
definitions *Definitions
}
var _ Schema = &Reference{}
func (r *Reference) GetSubSchema() Schema {
return r.definitions.models[r.Reference]
}
func (r *Reference) Accept(s SchemaVisitor) {
r.GetSubSchema().Accept(s)
}
func (r *Reference) GetDescription() string {
return r.GetSubSchema().GetDescription()
}
func (r *Reference) GetExtensions() map[string]interface{} {
return r.GetSubSchema().GetExtensions()
}
func (*Reference) GetPath() *Path {
// Reference never has a path, because it can be referenced from
// multiple locations.
return &Path{}
}
func (r *Reference) GetName() string {
return r.Reference
}

View File

@ -20,398 +20,182 @@ import (
"fmt" "fmt"
"strings" "strings"
"gopkg.in/yaml.v2"
"github.com/golang/glog"
"github.com/googleapis/gnostic/OpenAPIv2"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
) )
// groupVersionKindExtensionKey is the key used to lookup the GroupVersionKind value // Defines openapi types.
// for an object definition from the definition's "extensions" map. const (
const groupVersionKindExtensionKey = "x-kubernetes-group-version-kind" Integer = "integer"
Number = "number"
String = "string"
Boolean = "boolean"
// Integer is the name for integer types // These types are private as they should never leak, and are
const Integer = "integer" // represented by actual structs.
array = "array"
object = "object"
)
// String is the name for string types // Resources interface describe a resources provider, that can give you
const String = "string" // resource based on group-version-kind.
type Resources interface {
// Bool is the name for boolean types LookupResource(gvk schema.GroupVersionKind) Schema
const Boolean = "boolean"
// Map is the name for map types
// types.go struct fields that are maps will have an open API type "object"
// types.go struct fields that are actual objects appearing as a struct
// in a types.go file will have no type defined
// and have a json pointer reference to the type definition
const Map = "object"
// Array is the name for array types
const Array = "array"
// Resources contains the object definitions for Kubernetes resource apis
// Fields are public for binary serialization (private fields don't get serialized)
type Resources struct {
// GroupVersionKindToName maps GroupVersionKinds to Type names
GroupVersionKindToName map[schema.GroupVersionKind]string
// NameToDefinition maps Type names to TypeDefinitions
NameToDefinition map[string]Kind
} }
// LookupResource returns the Kind for the specified groupVersionKind // SchemaVisitor is an interface that you need to implement if you want
func (r Resources) LookupResource(groupVersionKind schema.GroupVersionKind) (Kind, bool) { // to "visit" an openapi schema. A dispatch on the Schema type will call
name, found := r.GroupVersionKindToName[groupVersionKind] // the appropriate function based on its actual type:
if !found { // - Array is a list of one and only one given subtype
return Kind{}, false // - Map is a map of string to one and only one given subtype
} // - Primitive can be string, integer, number and boolean.
def, found := r.NameToDefinition[name] // - Kind is an object with specific fields mapping to specific types.
if !found { type SchemaVisitor interface {
return Kind{}, false VisitArray(*Array)
} VisitMap(*Map)
return def, true VisitPrimitive(*Primitive)
VisitKind(*Kind)
} }
// Kind defines a Kubernetes object Kind // Schema is the base definition of an openapi type.
type Schema interface {
// Giving a visitor here will let you visit the actual type.
Accept(SchemaVisitor)
// Pretty print the name of the type.
GetName() string
// Describes how to access this field.
GetPath() *Path
// Describes the field.
GetDescription() string
// Returns type extensions.
GetExtensions() map[string]interface{}
}
// Path helps us keep track of type paths
type Path struct {
parent *Path
key string
}
func (p *Path) Get() []string {
if p == nil {
return []string{}
}
if p.key == "" {
return p.parent.Get()
}
return append(p.parent.Get(), p.key)
}
func (p *Path) Len() int {
return len(p.Get())
}
func (p *Path) String() string {
return strings.Join(p.Get(), ".")
}
// BaseSchema holds data used by each types of schema.
type BaseSchema struct {
Description string
Extensions map[string]interface{}
Path Path
}
func (b *BaseSchema) GetDescription() string {
return b.Description
}
func (b *BaseSchema) GetExtensions() map[string]interface{} {
return b.Extensions
}
func (b *BaseSchema) GetPath() *Path {
return &b.Path
}
// Array must have all its element of the same `SubType`.
type Array struct {
BaseSchema
SubType Schema
}
var _ Schema = &Array{}
func (a *Array) Accept(v SchemaVisitor) {
v.VisitArray(a)
}
func (a *Array) GetName() string {
return fmt.Sprintf("Array of %s", a.SubType.GetName())
}
// Kind is a complex object. It can have multiple different
// subtypes for each field, as defined in the `Fields` field. Mandatory
// fields are listed in `RequiredFields`. The key of the object is
// always of type `string`.
type Kind struct { type Kind struct {
// Name is the lookup key given to this Kind by the open API spec. BaseSchema
// May not contain any semantic meaning or relation to the API definition,
// simply must be unique for each object definition in the Open API spec.
// e.g. io.k8s.api.apps.v1beta1.Deployment
Name string
// IsResource is true if the Kind is a Resource (it has API endpoints) // Lists names of required fields.
// e.g. Deployment is a Resource, DeploymentStatus is NOT a Resource RequiredFields []string
IsResource bool // Maps field names to types.
Fields map[string]Schema
// GroupVersionKind uniquely defines a resource type in the Kubernetes API
// and is present for all resources.
// Empty for non-resource Kinds (e.g. those without APIs).
// e.g. "Group": "apps", "Version": "v1beta1", "Kind": "Deployment"
GroupVersionKind schema.GroupVersionKind
// Present only for definitions that represent primitive types with additional
// semantic meaning beyond just string, integer, boolean - e.g.
// Fields with a PrimitiveType should follow the validation of the primitive type.
// io.k8s.apimachinery.pkg.apis.meta.v1.Time
// io.k8s.apimachinery.pkg.util.intstr.IntOrString
PrimitiveType string
// Extensions are openapi extensions for the object definition.
Extensions map[string]interface{}
// Fields are the fields defined for this Kind
Fields map[string]Type
} }
// Type defines a field type and are expected to be one of: var _ Schema = &Kind{}
// - IsKind
// - IsMap
// - IsArray
// - IsPrimitive
type Type struct {
// Name is the name of the type
TypeName string
// IsKind is true if the definition represents a Kind func (k *Kind) Accept(v SchemaVisitor) {
IsKind bool v.VisitKind(k)
// IsPrimitive is true if the definition represents a primitive type - e.g. string, boolean, integer
IsPrimitive bool
// IsArray is true if the definition represents an array type
IsArray bool
// IsMap is true if the definition represents a map type
IsMap bool
// ElementType will be specified for arrays and maps
// if IsMap == true, then ElementType is the type of the value (key is always string)
// if IsArray == true, then ElementType is the type of the element
ElementType *Type
// Extensions are extensions for this field and may contain
// metadata from the types.go struct field tags.
// e.g. contains patchStrategy, patchMergeKey, etc
Extensions map[string]interface{}
} }
func vendorExtensionToMap(e []*openapi_v2.NamedAny) map[string]interface{} { func (k *Kind) GetName() string {
var values map[string]interface{} properties := []string{}
for key := range k.Fields {
for _, na := range e { properties = append(properties, key)
if na.GetName() == "" || na.GetValue() == nil {
continue
}
if na.GetValue().GetYaml() == "" {
continue
}
var value interface{}
err := yaml.Unmarshal([]byte(na.GetValue().GetYaml()), &value)
if err != nil {
continue
}
if values == nil {
values = make(map[string]interface{})
}
values[na.GetName()] = value
} }
return fmt.Sprintf("Kind(%v)", properties)
return values
} }
// NewOpenAPIData parses the resource definitions in openapi data by groupversionkind and name // Map is an object who values must all be of the same `SubType`.
func NewOpenAPIData(doc *openapi_v2.Document) (*Resources, error) { // The key of the object is always of type `string`.
o := &Resources{ type Map struct {
GroupVersionKindToName: map[schema.GroupVersionKind]string{}, BaseSchema
NameToDefinition: map[string]Kind{},
}
// Parse and index definitions by name
for _, ns := range doc.GetDefinitions().GetAdditionalProperties() {
definition := o.parseDefinition(ns.GetName(), ns.GetValue())
o.NameToDefinition[ns.GetName()] = definition
if len(definition.GroupVersionKind.Kind) > 0 {
o.GroupVersionKindToName[definition.GroupVersionKind] = ns.GetName()
}
}
if err := o.validate(); err != nil { SubType Schema
return nil, err
}
return o, nil
} }
// validate makes sure the definition for each field type is found in the map var _ Schema = &Map{}
func (o *Resources) validate() error {
types := sets.String{} func (m *Map) Accept(v SchemaVisitor) {
for _, d := range o.NameToDefinition { v.VisitMap(m)
for _, f := range d.Fields {
for _, t := range o.getTypeNames(f) {
types.Insert(t)
}
}
}
for _, n := range types.List() {
_, found := o.NameToDefinition[n]
if !found {
return fmt.Errorf("Unable to find definition for field of type %v", n)
}
}
return nil
} }
func (o *Resources) getTypeNames(elem Type) []string { func (m *Map) GetName() string {
t := []string{} return fmt.Sprintf("Map of %s", m.SubType.GetName())
if elem.IsKind {
t = append(t, elem.TypeName)
}
if elem.ElementType != nil && elem.ElementType.IsKind {
t = append(t, o.getTypeNames(*elem.ElementType)...)
}
return t
} }
func (o *Resources) parseDefinition(name string, s *openapi_v2.Schema) Kind { // Primitive is a literal. There can be multiple types of primitives,
gvk, err := o.getGroupVersionKind(s) // and this subtype can be visited through the `subType` field.
value := Kind{ type Primitive struct {
Name: name, BaseSchema
GroupVersionKind: gvk,
Extensions: vendorExtensionToMap(s.GetVendorExtension()),
Fields: map[string]Type{},
}
if err != nil {
glog.V(2).Info(err)
}
// Definition represents a primitive type - e.g. // Type of a primitive must be one of: integer, number, string, boolean.
// io.k8s.apimachinery.pkg.util.intstr.IntOrString Type string
if o.isPrimitive(s) { Format string
value.PrimitiveType = o.getTypeNameForField(s)
}
for _, ns := range s.GetProperties().GetAdditionalProperties() {
value.Fields[ns.GetName()] = o.parseField(ns.GetValue())
}
return value
} }
func (o *Resources) parseField(s *openapi_v2.Schema) Type { var _ Schema = &Primitive{}
def := Type{
TypeName: o.getTypeNameForField(s),
IsPrimitive: o.isPrimitive(s),
IsArray: o.isArray(s),
IsMap: o.isMap(s),
IsKind: o.isDefinitionReference(s),
}
if elementType, arrayErr := o.getElementType(s); arrayErr == nil { func (p *Primitive) Accept(v SchemaVisitor) {
d := o.parseField(elementType) v.VisitPrimitive(p)
def.ElementType = &d
} else if valueType, mapErr := o.getValueType(s); mapErr == nil {
d := o.parseField(valueType)
def.ElementType = &d
}
def.Extensions = vendorExtensionToMap(s.GetVendorExtension())
return def
} }
// isArray returns true if s is an array type. func (p *Primitive) GetName() string {
func (o *Resources) isArray(s *openapi_v2.Schema) bool { if p.Format == "" {
if len(s.GetProperties().GetAdditionalProperties()) > 0 { return p.Type
// Open API can have embedded type definitions, but Kubernetes doesn't generate these.
// This should just be a sanity check against changing the format.
return false
} }
return o.getType(s) == Array return fmt.Sprintf("%s (%s)", p.Type, p.Format)
}
// isMap returns true if s is a map type.
func (o *Resources) isMap(s *openapi_v2.Schema) bool {
if len(s.GetProperties().GetAdditionalProperties()) > 0 {
// Open API can have embedded type definitions, but Kubernetes doesn't generate these.
// This should just be a sanity check against changing the format.
return false
}
return o.getType(s) == Map
}
// isPrimitive returns true if s is a primitive type
// Note: For object references that represent primitive types - e.g. IntOrString - this will
// be false, and the referenced Kind will have a non-empty "PrimitiveType".
func (o *Resources) isPrimitive(s *openapi_v2.Schema) bool {
if len(s.GetProperties().GetAdditionalProperties()) > 0 {
// Open API can have embedded type definitions, but Kubernetes doesn't generate these.
// This should just be a sanity check against changing the format.
return false
}
t := o.getType(s)
if t == Integer || t == Boolean || t == String {
return true
}
return false
}
func (*Resources) getType(s *openapi_v2.Schema) string {
if len(s.GetType().GetValue()) != 1 {
return ""
}
return strings.ToLower(s.GetType().GetValue()[0])
}
func (o *Resources) getTypeNameForField(s *openapi_v2.Schema) string {
// Get the reference for complex types
if o.isDefinitionReference(s) {
return o.nameForDefinitionField(s)
}
// Recurse if type is array
if o.isArray(s) {
return fmt.Sprintf("%s array", o.getTypeNameForField(s.GetItems().GetSchema()[0]))
}
if o.isMap(s) {
return fmt.Sprintf("%s map", o.getTypeNameForField(s.GetAdditionalProperties().GetSchema()))
}
// Get the value for primitive types
if o.isPrimitive(s) {
return fmt.Sprintf("%s", s.GetType().GetValue()[0])
}
return ""
}
// isDefinitionReference returns true s is a complex type that should have a Kind.
func (o *Resources) isDefinitionReference(s *openapi_v2.Schema) bool {
if len(s.GetProperties().GetAdditionalProperties()) > 0 {
// Open API can have embedded type definitions, but Kubernetes doesn't generate these.
// This should just be a sanity check against changing the format.
return false
}
if len(s.GetType().GetValue()) > 0 {
// Definition references won't have a type
return false
}
p := s.GetXRef()
return len(p) > 0 && strings.HasPrefix(p, "#/definitions/")
}
// getElementType returns the type of an element for arrays
// returns an error if s is not an array.
func (o *Resources) getElementType(s *openapi_v2.Schema) (*openapi_v2.Schema, error) {
if !o.isArray(s) {
return &openapi_v2.Schema{}, fmt.Errorf("%v is not an array type", o.getTypeNameForField(s))
}
return s.GetItems().GetSchema()[0], nil
}
// getValueType returns the type of an element for maps
// returns an error if s is not a map.
func (o *Resources) getValueType(s *openapi_v2.Schema) (*openapi_v2.Schema, error) {
if !o.isMap(s) {
return &openapi_v2.Schema{}, fmt.Errorf("%v is not an map type", o.getTypeNameForField(s))
}
return s.GetAdditionalProperties().GetSchema(), nil
}
// nameForDefinitionField returns the definition name for the schema (field) if it is a complex type
func (o *Resources) nameForDefinitionField(s *openapi_v2.Schema) string {
p := s.GetXRef()
if len(p) == 0 {
return ""
}
// Strip the "definitions/" pieces of the reference
return strings.Replace(p, "#/definitions/", "", -1)
}
// getGroupVersionKind implements OpenAPIData
// getGVK parses the gropuversionkind for a resource definition from the x-kubernetes
// extensions
// map[x-kubernetes-group-version-kind:[map[Group:authentication.k8s.io Version:v1 Kind:TokenReview]]]
func (o *Resources) getGroupVersionKind(s *openapi_v2.Schema) (schema.GroupVersionKind, error) {
empty := schema.GroupVersionKind{}
extensionMap := vendorExtensionToMap(s.GetVendorExtension())
// Get the extensions
extList, f := extensionMap[groupVersionKindExtensionKey]
if !f {
return empty, fmt.Errorf("No %s extension present in %v", groupVersionKindExtensionKey, extensionMap)
}
// Expect a empty of a list with 1 element
extListCasted, ok := extList.([]interface{})
if !ok {
return empty, fmt.Errorf("%s extension has unexpected type %T in %s", groupVersionKindExtensionKey, extListCasted, extensionMap)
}
if len(extListCasted) == 0 {
return empty, fmt.Errorf("No Group Version Kind found in %v", extListCasted)
}
if len(extListCasted) != 1 {
return empty, fmt.Errorf("Multiple Group Version gvkToName found in %v", extListCasted)
}
gvk := extListCasted[0]
// Expect a empty of a map with 3 entries
gvkMap, ok := gvk.(map[interface{}]interface{})
if !ok {
return empty, fmt.Errorf("%s extension has unexpected type %T in %s", groupVersionKindExtensionKey, gvk, extList)
}
group, ok := gvkMap["group"].(string)
if !ok {
return empty, fmt.Errorf("%s extension missing Group: %v", groupVersionKindExtensionKey, gvkMap)
}
version, ok := gvkMap["version"].(string)
if !ok {
return empty, fmt.Errorf("%s extension missing Version: %v", groupVersionKindExtensionKey, gvkMap)
}
kind, ok := gvkMap["kind"].(string)
if !ok {
return empty, fmt.Errorf("%s extension missing Kind: %v", groupVersionKindExtensionKey, gvkMap)
}
return schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
}, nil
} }

View File

@ -18,7 +18,6 @@ package openapi
import ( import (
"bytes" "bytes"
"encoding/gob"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -26,15 +25,13 @@ import (
"path/filepath" "path/filepath"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/golang/protobuf/proto"
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
"k8s.io/kubernetes/pkg/version" "k8s.io/kubernetes/pkg/version"
) )
func init() {
registerBinaryEncodingTypes()
}
const openapiFileName = "openapi_cache" const openapiFileName = "openapi_cache"
type CachingOpenAPIClient struct { type CachingOpenAPIClient struct {
@ -61,12 +58,12 @@ func NewCachingOpenAPIClient(client discovery.OpenAPISchemaInterface, version, c
// It will first attempt to read the spec from a local cache // It will first attempt to read the spec from a local cache
// If it cannot read a local cache, it will read the file // If it cannot read a local cache, it will read the file
// using the client and then write the cache. // using the client and then write the cache.
func (c *CachingOpenAPIClient) OpenAPIData() (*Resources, error) { func (c *CachingOpenAPIClient) OpenAPIData() (Resources, error) {
// Try to use the cached version // Try to use the cached version
if c.useCache() { if c.useCache() {
doc, err := c.readOpenAPICache() doc, err := c.readOpenAPICache()
if err == nil { if err == nil {
return doc, nil return NewOpenAPIData(doc)
} }
} }
@ -85,7 +82,7 @@ func (c *CachingOpenAPIClient) OpenAPIData() (*Resources, error) {
// Try to cache the openapi spec // Try to cache the openapi spec
if c.useCache() { if c.useCache() {
err = c.writeToCache(oa) err = c.writeToCache(s)
if err != nil { if err != nil {
// Just log an message, no need to fail the command since we got the data we need // Just log an message, no need to fail the command since we got the data we need
glog.V(2).Infof("Unable to cache openapi spec %v", err) glog.V(2).Infof("Unable to cache openapi spec %v", err)
@ -102,7 +99,7 @@ func (c *CachingOpenAPIClient) useCache() bool {
} }
// readOpenAPICache tries to read the openapi spec from the local file cache // readOpenAPICache tries to read the openapi spec from the local file cache
func (c *CachingOpenAPIClient) readOpenAPICache() (*Resources, error) { func (c *CachingOpenAPIClient) readOpenAPICache() (*openapi_v2.Document, error) {
// Get the filename to read // Get the filename to read
filename := c.openAPICacheFilename() filename := c.openAPICacheFilename()
@ -112,38 +109,18 @@ func (c *CachingOpenAPIClient) readOpenAPICache() (*Resources, error) {
return nil, err return nil, err
} }
// Decode the openapi spec doc := &openapi_v2.Document{}
s, err := c.decodeSpec(data) return doc, proto.Unmarshal(data, doc)
return s, err
}
// decodeSpec binary decodes the openapi spec
func (c *CachingOpenAPIClient) decodeSpec(data []byte) (*Resources, error) {
b := bytes.NewBuffer(data)
d := gob.NewDecoder(b)
parsed := &Resources{}
err := d.Decode(parsed)
return parsed, err
}
// encodeSpec binary encodes the openapi spec
func (c *CachingOpenAPIClient) encodeSpec(parsed *Resources) ([]byte, error) {
b := &bytes.Buffer{}
e := gob.NewEncoder(b)
err := e.Encode(parsed)
return b.Bytes(), err
} }
// writeToCache tries to write the openapi spec to the local file cache. // writeToCache tries to write the openapi spec to the local file cache.
// writes the data to a new tempfile, and then links the cache file and the tempfile // writes the data to a new tempfile, and then links the cache file and the tempfile
func (c *CachingOpenAPIClient) writeToCache(parsed *Resources) error { func (c *CachingOpenAPIClient) writeToCache(doc *openapi_v2.Document) error {
// Get the constant filename used to read the cache. // Get the constant filename used to read the cache.
cacheFile := c.openAPICacheFilename() cacheFile := c.openAPICacheFilename()
// Binary encode the spec. This is 10x as fast as using json encoding. (60ms vs 600ms) // Binary encode the spec. This is 10x as fast as using json encoding. (60ms vs 600ms)
b, err := c.encodeSpec(parsed) b, err := proto.Marshal(doc)
if err != nil { if err != nil {
return fmt.Errorf("Could not binary encode openapi spec: %v", err) return fmt.Errorf("Could not binary encode openapi spec: %v", err)
} }
@ -184,9 +161,3 @@ func linkFiles(old, new string) error {
} }
return nil return nil
} }
// registerBinaryEncodingTypes registers the types so they can be binary encoded by gob
func registerBinaryEncodingTypes() {
gob.Register(map[interface{}]interface{}{})
gob.Register([]interface{}{})
}

View File

@ -38,7 +38,7 @@ var _ = Describe("When reading openAPIData", func() {
var err error var err error
var client *fakeOpenAPIClient var client *fakeOpenAPIClient
var instance *openapi.CachingOpenAPIClient var instance *openapi.CachingOpenAPIClient
var expectedData *openapi.Resources var expectedData openapi.Resources
BeforeEach(func() { BeforeEach(func() {
tmpDir, err = ioutil.TempDir("", "openapi_cache_test") tmpDir, err = ioutil.TempDir("", "openapi_cache_test")
@ -61,7 +61,7 @@ var _ = Describe("When reading openAPIData", func() {
By("getting the live openapi spec from the server") By("getting the live openapi spec from the server")
result, err := instance.OpenAPIData() result, err := instance.OpenAPIData()
Expect(err).To(BeNil()) Expect(err).To(BeNil())
expectEqual(result, expectedData) Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1)) Expect(client.calls).To(Equal(1))
By("writing the live openapi spec to a local cache file") By("writing the live openapi spec to a local cache file")
@ -83,13 +83,13 @@ var _ = Describe("When reading openAPIData", func() {
// First call should use the client // First call should use the client
result, err := instance.OpenAPIData() result, err := instance.OpenAPIData()
Expect(err).To(BeNil()) Expect(err).To(BeNil())
expectEqual(result, expectedData) Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1)) Expect(client.calls).To(Equal(1))
// Second call shouldn't use the client // Second call shouldn't use the client
result, err = instance.OpenAPIData() result, err = instance.OpenAPIData()
Expect(err).To(BeNil()) Expect(err).To(BeNil())
expectEqual(result, expectedData) Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1)) Expect(client.calls).To(Equal(1))
names, err := getFilenames(tmpDir) names, err := getFilenames(tmpDir)
@ -153,7 +153,7 @@ var _ = Describe("Reading openAPIData", func() {
By("getting the live openapi schema") By("getting the live openapi schema")
result, err := instance.OpenAPIData() result, err := instance.OpenAPIData()
Expect(err).To(BeNil()) Expect(err).To(BeNil())
expectEqual(result, expectedData) Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1)) Expect(client.calls).To(Equal(1))
files, err := ioutil.ReadDir(tmpDir) files, err := ioutil.ReadDir(tmpDir)
@ -181,7 +181,7 @@ var _ = Describe("Reading openAPIData", func() {
By("getting the live openapi schema") By("getting the live openapi schema")
result, err := instance.OpenAPIData() result, err := instance.OpenAPIData()
Expect(err).To(BeNil()) Expect(err).To(BeNil())
expectEqual(result, expectedData) Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1)) Expect(client.calls).To(Equal(1))
files, err := ioutil.ReadDir(tmpDir) files, err := ioutil.ReadDir(tmpDir)
@ -204,19 +204,6 @@ func getFilenames(path string) ([]string, error) {
return result, nil return result, nil
} }
func expectEqual(a *openapi.Resources, b *openapi.Resources) {
Expect(a.NameToDefinition).To(HaveLen(len(b.NameToDefinition)))
for k, v := range a.NameToDefinition {
Expect(v).To(Equal(b.NameToDefinition[k]),
fmt.Sprintf("Names for GVK do not match %v", k))
}
Expect(a.GroupVersionKindToName).To(HaveLen(len(b.GroupVersionKindToName)))
for k, v := range a.GroupVersionKindToName {
Expect(v).To(Equal(b.GroupVersionKindToName[k]),
fmt.Sprintf("Values for name do not match %v", k))
}
}
type fakeOpenAPIClient struct { type fakeOpenAPIClient struct {
calls int calls int
err error err error
@ -276,5 +263,6 @@ func (d *apiData) OpenAPISchema() (*openapi_v2.Document, error) {
} }
d.data, d.err = openapi_v2.NewDocument(info, compiler.NewContext("$root", nil)) d.data, d.err = openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
}) })
return d.data, d.err return d.data, d.err
} }

View File

@ -26,7 +26,7 @@ import (
type synchronizedOpenAPIGetter struct { type synchronizedOpenAPIGetter struct {
// Cached results // Cached results
sync.Once sync.Once
openAPISchema *Resources openAPISchema Resources
err error err error
serverVersion string serverVersion string
@ -39,7 +39,7 @@ var _ Getter = &synchronizedOpenAPIGetter{}
// Getter is an interface for fetching openapi specs and parsing them into an Resources struct // Getter is an interface for fetching openapi specs and parsing them into an Resources struct
type Getter interface { type Getter interface {
// OpenAPIData returns the parsed OpenAPIData // OpenAPIData returns the parsed OpenAPIData
Get() (*Resources, error) Get() (Resources, error)
} }
// NewOpenAPIGetter returns an object to return OpenAPIDatas which either read from a // NewOpenAPIGetter returns an object to return OpenAPIDatas which either read from a
@ -53,7 +53,7 @@ func NewOpenAPIGetter(cacheDir, serverVersion string, openAPIClient discovery.Op
} }
// Resources implements Getter // Resources implements Getter
func (g *synchronizedOpenAPIGetter) Get() (*Resources, error) { func (g *synchronizedOpenAPIGetter) Get() (Resources, error) {
g.Do(func() { g.Do(func() {
client := NewCachingOpenAPIClient(g.openAPIClient, g.serverVersion, g.cacheDir) client := NewCachingOpenAPIClient(g.openAPIClient, g.serverVersion, g.cacheDir)
result, err := client.OpenAPIData() result, err := client.OpenAPIData()

View File

@ -27,7 +27,7 @@ import (
var _ = Describe("Getting the Resources", func() { var _ = Describe("Getting the Resources", func() {
var client *fakeOpenAPIClient var client *fakeOpenAPIClient
var expectedData *openapi.Resources var expectedData openapi.Resources
var instance openapi.Getter var instance openapi.Getter
BeforeEach(func() { BeforeEach(func() {
@ -47,12 +47,12 @@ var _ = Describe("Getting the Resources", func() {
result, err := instance.Get() result, err := instance.Get()
Expect(err).To(BeNil()) Expect(err).To(BeNil())
expectEqual(result, expectedData) Expect(result).To(Equal(expectedData))
Expect(client.calls).To(Equal(1)) Expect(client.calls).To(Equal(1))
result, err = instance.Get() result, err = instance.Get()
Expect(err).To(BeNil()) Expect(err).To(BeNil())
expectEqual(result, expectedData) Expect(result).To(Equal(expectedData))
// No additional client calls expected // No additional client calls expected
Expect(client.calls).To(Equal(1)) Expect(client.calls).To(Equal(1))
}) })

View File

@ -17,9 +17,6 @@ limitations under the License.
package openapi_test package openapi_test
import ( import (
"fmt"
"github.com/go-openapi/spec"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
@ -28,395 +25,161 @@ import (
) )
var _ = Describe("Reading apps/v1beta1/Deployment from openAPIData", func() { var _ = Describe("Reading apps/v1beta1/Deployment from openAPIData", func() {
var instance *openapi.Resources var resources openapi.Resources
BeforeEach(func() { BeforeEach(func() {
s, err := data.OpenAPISchema() s, err := data.OpenAPISchema()
Expect(err).To(BeNil()) Expect(err).To(BeNil())
instance, err = openapi.NewOpenAPIData(s) resources, err = openapi.NewOpenAPIData(s)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
fmt.Fprintf(GinkgoWriter, fmt.Sprintf("CHAO: instance.GroupVersionKindToName=%#v\n", instance.GroupVersionKindToName))
}) })
deploymentName := "io.k8s.api.apps.v1beta1.Deployment"
gvk := schema.GroupVersionKind{ gvk := schema.GroupVersionKind{
Kind: "Deployment", Kind: "Deployment",
Version: "v1beta1", Version: "v1beta1",
Group: "apps", Group: "apps",
} }
It("should find the name by its GroupVersionKind", func() { var schema openapi.Schema
name, found := instance.GroupVersionKindToName[gvk] It("should lookup the Schema by its GroupVersionKind", func() {
fmt.Fprintf(GinkgoWriter, fmt.Sprintf("CHAO: instance.GroupVersionKindToName=%#v\n", instance.GroupVersionKindToName)) schema = resources.LookupResource(gvk)
Expect(found).To(BeTrue()) Expect(schema).ToNot(BeNil())
Expect(name).To(Equal(deploymentName))
}) })
var definition openapi.Kind var deployment *openapi.Kind
It("should find the definition by name", func() { It("should be a Kind", func() {
var found bool deployment = schema.(*openapi.Kind)
definition, found = instance.NameToDefinition[deploymentName] Expect(deployment).ToNot(BeNil())
Expect(found).To(BeTrue())
Expect(definition.Name).To(Equal(deploymentName))
Expect(definition.PrimitiveType).To(BeEmpty())
}) })
It("should lookup the Kind by its GroupVersionKind", func() { It("should have a path", func() {
d, found := instance.LookupResource(gvk) Expect(deployment.GetPath().Get()).To(Equal([]string{"io.k8s.api.apps.v1beta1.Deployment"}))
Expect(found).To(BeTrue())
Expect(d).To(Equal(definition))
}) })
It("should find the definition GroupVersionKind", func() { It("should have a kind key of type string", func() {
Expect(definition.GroupVersionKind).To(Equal(gvk)) Expect(deployment.Fields).To(HaveKey("kind"))
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"}))
}) })
It("should find the definition GroupVersionKind extensions", func() { It("should have a apiVersion key of type string", func() {
Expect(definition.Extensions).To(HaveKey("x-kubernetes-group-version-kind")) Expect(deployment.Fields).To(HaveKey("apiVersion"))
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"}))
}) })
It("should find the definition fields", func() { It("should have a metadata key of type Reference", func() {
By("for 'kind'") Expect(deployment.Fields).To(HaveKey("metadata"))
Expect(definition.Fields).To(HaveKeyWithValue("kind", openapi.Type{ key := deployment.Fields["metadata"].(*openapi.Reference)
TypeName: "string", Expect(key).ToNot(BeNil())
IsPrimitive: true, Expect(key.Reference).To(Equal("io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"))
subSchema := key.GetSubSchema().(*openapi.Kind)
Expect(subSchema).ToNot(BeNil())
})
var status *openapi.Kind
It("should have a status key of type Reference", func() {
Expect(deployment.Fields).To(HaveKey("status"))
key := deployment.Fields["status"].(*openapi.Reference)
Expect(key).ToNot(BeNil())
Expect(key.Reference).To(Equal("io.k8s.api.apps.v1beta1.DeploymentStatus"))
status = key.GetSubSchema().(*openapi.Kind)
Expect(status).ToNot(BeNil())
})
It("should have a valid DeploymentStatus", func() {
By("having availableReplicas key")
Expect(status.Fields).To(HaveKey("availableReplicas"))
replicas := status.Fields["availableReplicas"].(*openapi.Primitive)
Expect(replicas).ToNot(BeNil())
Expect(replicas.Type).To(Equal("integer"))
By("having conditions key")
Expect(status.Fields).To(HaveKey("conditions"))
conditions := status.Fields["conditions"].(*openapi.Array)
Expect(conditions).ToNot(BeNil())
Expect(conditions.GetName()).To(Equal("Array of io.k8s.api.apps.v1beta1.DeploymentCondition"))
Expect(conditions.GetExtensions()).To(Equal(map[string]interface{}{
"x-kubernetes-patch-merge-key": "type",
"x-kubernetes-patch-strategy": "merge",
})) }))
condition := conditions.SubType.(*openapi.Reference)
By("for 'apiVersion'") Expect(condition.Reference).To(Equal("io.k8s.api.apps.v1beta1.DeploymentCondition"))
Expect(definition.Fields).To(HaveKeyWithValue("apiVersion", openapi.Type{
TypeName: "string",
IsPrimitive: true,
}))
By("for 'metadata'")
Expect(definition.Fields).To(HaveKeyWithValue("metadata", openapi.Type{
TypeName: "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
IsKind: true,
}))
By("for 'spec'")
Expect(definition.Fields).To(HaveKeyWithValue("spec", openapi.Type{
TypeName: "io.k8s.api.apps.v1beta1.DeploymentSpec",
IsKind: true,
}))
By("for 'status'")
Expect(definition.Fields).To(HaveKeyWithValue("status", openapi.Type{
TypeName: "io.k8s.api.apps.v1beta1.DeploymentStatus",
IsKind: true,
}))
})
})
var _ = Describe("Reading apps/v1beta1/DeploymentStatus from openAPIData", func() {
var instance *openapi.Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
}) })
deploymentStatusName := "io.k8s.api.apps.v1beta1.DeploymentStatus" var spec *openapi.Kind
It("should have a spec key of type Reference", func() {
var definition openapi.Kind Expect(deployment.Fields).To(HaveKey("spec"))
It("should find the definition by name", func() { key := deployment.Fields["spec"].(*openapi.Reference)
var found bool Expect(key).ToNot(BeNil())
definition, found = instance.NameToDefinition[deploymentStatusName] Expect(key.Reference).To(Equal("io.k8s.api.apps.v1beta1.DeploymentSpec"))
Expect(found).To(BeTrue()) spec = key.GetSubSchema().(*openapi.Kind)
Expect(definition.Name).To(Equal(deploymentStatusName)) Expect(spec).ToNot(BeNil())
Expect(definition.PrimitiveType).To(BeEmpty())
}) })
It("should not find the definition GroupVersionKind", func() { It("should have a spec with no gvk", func() {
Expect(definition.GroupVersionKind).To(Equal(schema.GroupVersionKind{})) _, found := spec.GetExtensions()["x-kubernetes-group-version-kind"]
})
It("should not find the definition GroupVersionKind extensions", func() {
_, found := definition.Extensions["x-kubernetes-group-version-kind"]
Expect(found).To(BeFalse()) Expect(found).To(BeFalse())
}) })
It("should find the definition fields", func() { It("should have a spec with a PodTemplateSpec sub-field", func() {
By("for 'availableReplicas'") Expect(spec.Fields).To(HaveKey("template"))
Expect(definition.Fields).To(HaveKeyWithValue("availableReplicas", openapi.Type{ key := spec.Fields["template"].(*openapi.Reference)
TypeName: "integer", Expect(key).ToNot(BeNil())
IsPrimitive: true, Expect(key.Reference).To(Equal("io.k8s.api.core.v1.PodTemplateSpec"))
}))
By("for 'conditions'")
Expect(definition.Fields).To(HaveKeyWithValue("conditions", openapi.Type{
TypeName: "io.k8s.api.apps.v1beta1.DeploymentCondition array",
IsArray: true,
ElementType: &openapi.Type{
TypeName: "io.k8s.api.apps.v1beta1.DeploymentCondition",
IsKind: true,
},
Extensions: spec.Extensions{
"x-kubernetes-patch-merge-key": "type",
"x-kubernetes-patch-strategy": "merge",
},
}))
}) })
}) })
var _ = Describe("Reading apps/v1beta1/DeploymentSpec from openAPIData", func() { var _ = Describe("Reading authorization.k8s.io/v1/SubjectAccessReview from openAPIData", func() {
var instance *openapi.Resources var resources openapi.Resources
BeforeEach(func() { BeforeEach(func() {
d, err := data.OpenAPISchema() s, err := data.OpenAPISchema()
Expect(err).To(BeNil()) Expect(err).To(BeNil())
instance, err = openapi.NewOpenAPIData(d) resources, err = openapi.NewOpenAPIData(s)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
}) })
deploymentSpecName := "io.k8s.api.apps.v1beta1.DeploymentSpec" gvk := schema.GroupVersionKind{
Kind: "SubjectAccessReview",
var definition openapi.Kind Version: "v1",
It("should find the definition by name", func() { Group: "authorization.k8s.io",
var found bool
definition, found = instance.NameToDefinition[deploymentSpecName]
Expect(found).To(BeTrue())
Expect(definition.Name).To(Equal(deploymentSpecName))
Expect(definition.PrimitiveType).To(BeEmpty())
})
It("should not find the definition GroupVersionKind", func() {
Expect(definition.GroupVersionKind).To(Equal(schema.GroupVersionKind{}))
})
It("should not find the definition GroupVersionKind extensions", func() {
_, found := definition.Extensions["x-kubernetes-group-version-kind"]
Expect(found).To(BeFalse())
})
It("should find the definition fields", func() {
By("for 'template'")
Expect(definition.Fields).To(HaveKeyWithValue("template", openapi.Type{
TypeName: "io.k8s.api.core.v1.PodTemplateSpec",
IsKind: true,
}))
})
})
var _ = Describe("Reading v1/ObjectMeta from openAPIData", func() {
var instance *openapi.Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
})
objectMetaName := "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
var definition openapi.Kind
It("should find the definition by name", func() {
var found bool
definition, found = instance.NameToDefinition[objectMetaName]
Expect(found).To(BeTrue())
Expect(definition.Name).To(Equal(objectMetaName))
Expect(definition.PrimitiveType).To(BeEmpty())
})
It("should not find the definition GroupVersionKind", func() {
Expect(definition.GroupVersionKind).To(Equal(schema.GroupVersionKind{}))
})
It("should not find the definition GroupVersionKind extensions", func() {
_, found := definition.Extensions["x-kubernetes-group-version-kind"]
Expect(found).To(BeFalse())
})
It("should find the definition fields", func() {
By("for 'finalizers'")
Expect(definition.Fields).To(HaveKeyWithValue("finalizers", openapi.Type{
TypeName: "string array",
IsArray: true,
ElementType: &openapi.Type{
TypeName: "string",
IsPrimitive: true,
},
Extensions: spec.Extensions{
"x-kubernetes-patch-strategy": "merge",
},
}))
By("for 'ownerReferences'")
Expect(definition.Fields).To(HaveKeyWithValue("ownerReferences", openapi.Type{
TypeName: "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference array",
IsArray: true,
ElementType: &openapi.Type{
TypeName: "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference",
IsKind: true,
},
Extensions: spec.Extensions{
"x-kubernetes-patch-merge-key": "uid",
"x-kubernetes-patch-strategy": "merge",
},
}))
By("for 'labels'")
Expect(definition.Fields).To(HaveKeyWithValue("labels", openapi.Type{
TypeName: "string map",
IsMap: true,
ElementType: &openapi.Type{
TypeName: "string",
IsPrimitive: true,
},
}))
})
})
var _ = Describe("Reading v1/NodeStatus from openAPIData", func() {
var instance *openapi.Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
})
nodeStatusName := "io.k8s.api.core.v1.NodeStatus"
var definition openapi.Kind
It("should find the definition by name", func() {
var found bool
definition, found = instance.NameToDefinition[nodeStatusName]
Expect(found).To(BeTrue())
Expect(definition.Name).To(Equal(nodeStatusName))
Expect(definition.PrimitiveType).To(BeEmpty())
})
It("should not find the definition GroupVersionKind", func() {
Expect(definition.GroupVersionKind).To(Equal(schema.GroupVersionKind{}))
})
It("should not find the definition GroupVersionKind extensions", func() {
_, found := definition.Extensions["x-kubernetes-group-version-kind"]
Expect(found).To(BeFalse())
})
It("should find the definition fields", func() {
By("for 'allocatable'")
Expect(definition.Fields).To(HaveKeyWithValue("allocatable", openapi.Type{
TypeName: "io.k8s.apimachinery.pkg.api.resource.Quantity map",
IsMap: true,
ElementType: &openapi.Type{
TypeName: "io.k8s.apimachinery.pkg.api.resource.Quantity",
IsKind: true,
},
}))
})
})
var _ = Describe("Reading Utility Definitions from openAPIData", func() {
var instance *openapi.Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
})
Context("for util.intstr.IntOrString", func() {
var definition openapi.Kind
It("should find the definition by name", func() {
intOrStringName := "io.k8s.apimachinery.pkg.util.intstr.IntOrString"
var found bool
definition, found = instance.NameToDefinition[intOrStringName]
Expect(found).To(BeTrue())
Expect(definition.Name).To(Equal(intOrStringName))
Expect(definition.PrimitiveType).To(Equal("string"))
})
})
Context("for apis.meta.v1.Time", func() {
var definition openapi.Kind
It("should find the definition by name", func() {
intOrStringName := "io.k8s.apimachinery.pkg.apis.meta.v1.Time"
var found bool
definition, found = instance.NameToDefinition[intOrStringName]
Expect(found).To(BeTrue())
Expect(definition.Name).To(Equal(intOrStringName))
Expect(definition.PrimitiveType).To(Equal("string"))
})
})
})
var _ = Describe("When parsing the openAPIData", func() {
var instance *openapi.Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
})
It("should result in each definition and field having a single type", func() {
for _, d := range instance.NameToDefinition {
Expect(d.Name).ToNot(BeEmpty())
for n, f := range d.Fields {
Expect(f.TypeName).ToNot(BeEmpty(),
fmt.Sprintf("TypeName for %v.%v is empty %+v", d.Name, n, f))
Expect(oneOf(f.IsArray, f.IsMap, f.IsPrimitive, f.IsKind)).To(BeTrue(),
fmt.Sprintf("%+v has multiple types", f))
}
}
})
It("should find every GroupVersionKind by name", func() {
for _, name := range instance.GroupVersionKindToName {
_, found := instance.NameToDefinition[name]
Expect(found).To(BeTrue())
}
})
})
var _ = Describe("Reading authorization/v1/SubjectAccessReviewSpec from openAPIData", func() {
var instance *openapi.Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = openapi.NewOpenAPIData(d)
Expect(err).To(BeNil())
})
subjectAccessReviewSpecName := "io.k8s.api.authorization.v1.SubjectAccessReviewSpec"
var definition openapi.Kind
It("should find the definition by name", func() {
var found bool
definition, found = instance.NameToDefinition[subjectAccessReviewSpecName]
Expect(found).To(BeTrue())
Expect(definition.Name).To(Equal(subjectAccessReviewSpecName))
Expect(definition.PrimitiveType).To(BeEmpty())
})
It("should find the definition fields", func() {
By("for 'allocatable'")
Expect(definition.Fields).To(HaveKeyWithValue("extra", openapi.Type{
TypeName: "string array map",
IsMap: true,
ElementType: &openapi.Type{
TypeName: "string array",
IsArray: true,
ElementType: &openapi.Type{
TypeName: "string",
IsPrimitive: true,
},
},
}))
})
})
func oneOf(values ...bool) bool {
found := false
for _, v := range values {
if v && found {
return false
}
if v {
found = true
}
} }
return found
} var schema openapi.Schema
It("should lookup the Schema by its GroupVersionKind", func() {
schema = resources.LookupResource(gvk)
Expect(schema).ToNot(BeNil())
})
var sarspec *openapi.Kind
It("should be a Kind and have a spec", func() {
sar := schema.(*openapi.Kind)
Expect(sar).ToNot(BeNil())
Expect(sar.Fields).To(HaveKey("spec"))
specRef := sar.Fields["spec"].(*openapi.Reference)
Expect(specRef).ToNot(BeNil())
Expect(specRef.Reference).To(Equal("io.k8s.api.authorization.v1.SubjectAccessReviewSpec"))
sarspec = specRef.GetSubSchema().(*openapi.Kind)
Expect(sarspec).ToNot(BeNil())
})
It("should have a valid SubjectAccessReviewSpec", func() {
Expect(sarspec.Fields).To(HaveKey("extra"))
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"}))
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"}))
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"}))
})
})