diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index abebae9fbae..2f3d2196528 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -217,7 +217,6 @@ go_test( "//pkg/printers/internalversion:go_default_library", "//pkg/util/i18n: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/stretchr/testify/assert:go_default_library", "//vendor/gopkg.in/yaml.v2:go_default_library", diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index eb3bea9ab82..077fb958aa9 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -566,13 +566,13 @@ func outputOptsForMappingFromOpenAPI(f cmdutil.Factory, openAPIcacheDir string, return nil, false } // Found openapi metadata for this resource - kind, found := api.LookupResource(mapping.GroupVersionKind) - if !found { - // Kind not found, return empty columns + schema := api.LookupResource(mapping.GroupVersionKind) + if schema == nil { + // Schema not found, return empty columns return nil, false } - columns, found := openapi.GetPrintColumns(kind.Extensions) + columns, found := openapi.GetPrintColumns(schema.GetExtensions()) if !found { // Extension not found, return empty columns return nil, false diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index e2c80d5f629..a0f96ac49f5 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -26,8 +26,6 @@ import ( "strings" "testing" - "github.com/go-openapi/spec" - apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -218,20 +216,27 @@ func TestGetObjectsWithOpenAPIOutputFormatPresent(t *testing.T) { } } -func testOpenAPISchemaData() (*openapi.Resources, error) { - return &openapi.Resources{ - GroupVersionKindToName: map[schema.GroupVersionKind]string{ +type FakeResources struct { + resources map[schema.GroupVersionKind]openapi.Schema +} + +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", Kind: "Pod", - }: "io.k8s.kubernetes.pkg.api.v1.Pod", - }, - NameToDefinition: map[string]openapi.Kind{ - "io.k8s.kubernetes.pkg.api.v1.Pod": { - 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", + }: &openapi.Primitive{ + BaseSchema: openapi.BaseSchema{ + Extensions: map[string]interface{}{ + "x-kubernetes-print-columns": "custom-columns=NAME:.metadata.name,RSRC:.metadata.resourceVersion", + }, }, }, }, diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index ba1572a2632..3c3392f133e 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -243,7 +243,7 @@ type TestFactory struct { ClientForMappingFunc 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 { @@ -418,8 +418,8 @@ func (f *FakeFactory) SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDeclar return nil, nil } -func (f *FakeFactory) OpenAPISchema(cacheDir string) (*openapi.Resources, error) { - return &openapi.Resources{}, nil +func (f *FakeFactory) OpenAPISchema(cacheDir string) (openapi.Resources, error) { + return nil, nil } func (f *FakeFactory) DefaultNamespace() (string, bool, error) { @@ -756,11 +756,11 @@ func (f *fakeAPIFactory) SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDec 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 { return f.tf.OpenAPISchemaFunc() } - return &openapi.Resources{}, nil + return nil, nil } func NewAPIFactory() (cmdutil.Factory, *TestFactory, runtime.Codec, runtime.NegotiatedSerializer) { diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index f2a839caf1e..999e49d72fe 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -224,7 +224,7 @@ type ObjectMappingFactory interface { // SwaggerSchema returns the schema declaration for the provided group version kind. SwaggerSchema(schema.GroupVersionKind) (*swagger.ApiDeclaration, error) // 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. diff --git a/pkg/kubectl/cmd/util/factory_object_mapping.go b/pkg/kubectl/cmd/util/factory_object_mapping.go index 88611d73be3..83cb1b30dbf 100644 --- a/pkg/kubectl/cmd/util/factory_object_mapping.go +++ b/pkg/kubectl/cmd/util/factory_object_mapping.go @@ -445,7 +445,7 @@ func (f *ring1Factory) SwaggerSchema(gvk schema.GroupVersionKind) (*swagger.ApiD // 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 // 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() if err != nil { return nil, err diff --git a/pkg/kubectl/cmd/util/openapi/BUILD b/pkg/kubectl/cmd/util/openapi/BUILD index fb7ff7fe20a..eb926ee7e97 100644 --- a/pkg/kubectl/cmd/util/openapi/BUILD +++ b/pkg/kubectl/cmd/util/openapi/BUILD @@ -12,6 +12,7 @@ go_library( name = "go_default_library", srcs = [ "doc.go", + "document.go", "extensions.go", "openapi.go", "openapi_cache.go", @@ -22,10 +23,10 @@ go_library( "//pkg/version:go_default_library", "//vendor/github.com/go-openapi/spec: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/gopkg.in/yaml.v2: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", ], ) @@ -43,7 +44,6 @@ go_test( tags = ["automanaged"], deps = [ "//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/compiler:go_default_library", "//vendor/github.com/onsi/ginkgo:go_default_library", diff --git a/pkg/kubectl/cmd/util/openapi/document.go b/pkg/kubectl/cmd/util/openapi/document.go new file mode 100644 index 00000000000..7456ae4aa33 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/document.go @@ -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 +} diff --git a/pkg/kubectl/cmd/util/openapi/openapi.go b/pkg/kubectl/cmd/util/openapi/openapi.go index 0da0cf60340..89e15de4d26 100644 --- a/pkg/kubectl/cmd/util/openapi/openapi.go +++ b/pkg/kubectl/cmd/util/openapi/openapi.go @@ -20,398 +20,182 @@ import ( "fmt" "strings" - "gopkg.in/yaml.v2" - - "github.com/golang/glog" - "github.com/googleapis/gnostic/OpenAPIv2" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/sets" ) -// 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" +// Defines openapi types. +const ( + Integer = "integer" + Number = "number" + String = "string" + Boolean = "boolean" -// Integer is the name for integer types -const Integer = "integer" + // These types are private as they should never leak, and are + // represented by actual structs. + array = "array" + object = "object" +) -// String is the name for string types -const String = "string" - -// Bool is the name for boolean types -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 +// Resources interface describe a resources provider, that can give you +// resource based on group-version-kind. +type Resources interface { + LookupResource(gvk schema.GroupVersionKind) Schema } -// LookupResource returns the Kind for the specified groupVersionKind -func (r Resources) LookupResource(groupVersionKind schema.GroupVersionKind) (Kind, bool) { - name, found := r.GroupVersionKindToName[groupVersionKind] - if !found { - return Kind{}, false - } - def, found := r.NameToDefinition[name] - if !found { - return Kind{}, false - } - return def, true +// SchemaVisitor is an interface that you need to implement if you want +// to "visit" an openapi schema. A dispatch on the Schema type will call +// the appropriate function based on its actual type: +// - Array is a list of one and only one given subtype +// - Map is a map of string to one and only one given subtype +// - Primitive can be string, integer, number and boolean. +// - Kind is an object with specific fields mapping to specific types. +type SchemaVisitor interface { + VisitArray(*Array) + VisitMap(*Map) + 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 { - // Name is the lookup key given to this Kind by the open API spec. - // 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 + BaseSchema - // IsResource is true if the Kind is a Resource (it has API endpoints) - // e.g. Deployment is a Resource, DeploymentStatus is NOT a Resource - IsResource bool - - // 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 + // Lists names of required fields. + RequiredFields []string + // Maps field names to types. + Fields map[string]Schema } -// Type defines a field type and are expected to be one of: -// - IsKind -// - IsMap -// - IsArray -// - IsPrimitive -type Type struct { - // Name is the name of the type - TypeName string +var _ Schema = &Kind{} - // IsKind is true if the definition represents a Kind - IsKind bool - // 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 (k *Kind) Accept(v SchemaVisitor) { + v.VisitKind(k) } -func vendorExtensionToMap(e []*openapi_v2.NamedAny) map[string]interface{} { - var 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 - } - if values == nil { - values = make(map[string]interface{}) - } - values[na.GetName()] = value +func (k *Kind) GetName() string { + properties := []string{} + for key := range k.Fields { + properties = append(properties, key) } - - return values + return fmt.Sprintf("Kind(%v)", properties) } -// NewOpenAPIData parses the resource definitions in openapi data by groupversionkind and name -func NewOpenAPIData(doc *openapi_v2.Document) (*Resources, error) { - o := &Resources{ - GroupVersionKindToName: map[schema.GroupVersionKind]string{}, - 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() - } - } +// Map is an object who values must all be of the same `SubType`. +// The key of the object is always of type `string`. +type Map struct { + BaseSchema - if err := o.validate(); err != nil { - return nil, err - } - - return o, nil + SubType Schema } -// validate makes sure the definition for each field type is found in the map -func (o *Resources) validate() error { - types := sets.String{} - for _, d := range o.NameToDefinition { - 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 +var _ Schema = &Map{} + +func (m *Map) Accept(v SchemaVisitor) { + v.VisitMap(m) } -func (o *Resources) getTypeNames(elem Type) []string { - t := []string{} - 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 (m *Map) GetName() string { + return fmt.Sprintf("Map of %s", m.SubType.GetName()) } -func (o *Resources) parseDefinition(name string, s *openapi_v2.Schema) Kind { - gvk, err := o.getGroupVersionKind(s) - value := Kind{ - Name: name, - GroupVersionKind: gvk, - Extensions: vendorExtensionToMap(s.GetVendorExtension()), - Fields: map[string]Type{}, - } - if err != nil { - glog.V(2).Info(err) - } +// Primitive is a literal. There can be multiple types of primitives, +// and this subtype can be visited through the `subType` field. +type Primitive struct { + BaseSchema - // Definition represents a primitive type - e.g. - // io.k8s.apimachinery.pkg.util.intstr.IntOrString - if o.isPrimitive(s) { - value.PrimitiveType = o.getTypeNameForField(s) - } - for _, ns := range s.GetProperties().GetAdditionalProperties() { - value.Fields[ns.GetName()] = o.parseField(ns.GetValue()) - } - return value + // Type of a primitive must be one of: integer, number, string, boolean. + Type string + Format string } -func (o *Resources) parseField(s *openapi_v2.Schema) Type { - def := Type{ - TypeName: o.getTypeNameForField(s), - IsPrimitive: o.isPrimitive(s), - IsArray: o.isArray(s), - IsMap: o.isMap(s), - IsKind: o.isDefinitionReference(s), - } +var _ Schema = &Primitive{} - if elementType, arrayErr := o.getElementType(s); arrayErr == nil { - d := o.parseField(elementType) - 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 +func (p *Primitive) Accept(v SchemaVisitor) { + v.VisitPrimitive(p) } -// isArray returns true if s is an array type. -func (o *Resources) isArray(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 +func (p *Primitive) GetName() string { + if p.Format == "" { + return p.Type } - return o.getType(s) == Array -} - -// 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 + return fmt.Sprintf("%s (%s)", p.Type, p.Format) } diff --git a/pkg/kubectl/cmd/util/openapi/openapi_cache.go b/pkg/kubectl/cmd/util/openapi/openapi_cache.go index 98d7341c43d..faf83456b49 100644 --- a/pkg/kubectl/cmd/util/openapi/openapi_cache.go +++ b/pkg/kubectl/cmd/util/openapi/openapi_cache.go @@ -18,7 +18,6 @@ package openapi import ( "bytes" - "encoding/gob" "fmt" "io" "io/ioutil" @@ -26,15 +25,13 @@ import ( "path/filepath" "github.com/golang/glog" + "github.com/golang/protobuf/proto" + openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2" "k8s.io/client-go/discovery" "k8s.io/kubernetes/pkg/version" ) -func init() { - registerBinaryEncodingTypes() -} - const openapiFileName = "openapi_cache" 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 // If it cannot read a local cache, it will read the file // 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 if c.useCache() { doc, err := c.readOpenAPICache() 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 if c.useCache() { - err = c.writeToCache(oa) + err = c.writeToCache(s) if err != nil { // 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) @@ -102,7 +99,7 @@ func (c *CachingOpenAPIClient) useCache() bool { } // 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 filename := c.openAPICacheFilename() @@ -112,38 +109,18 @@ func (c *CachingOpenAPIClient) readOpenAPICache() (*Resources, error) { return nil, err } - // Decode the openapi spec - s, err := c.decodeSpec(data) - - 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 - + doc := &openapi_v2.Document{} + return doc, proto.Unmarshal(data, doc) } // 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 -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. cacheFile := c.openAPICacheFilename() // 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 { return fmt.Errorf("Could not binary encode openapi spec: %v", err) } @@ -184,9 +161,3 @@ func linkFiles(old, new string) error { } return nil } - -// registerBinaryEncodingTypes registers the types so they can be binary encoded by gob -func registerBinaryEncodingTypes() { - gob.Register(map[interface{}]interface{}{}) - gob.Register([]interface{}{}) -} diff --git a/pkg/kubectl/cmd/util/openapi/openapi_cache_test.go b/pkg/kubectl/cmd/util/openapi/openapi_cache_test.go index 19c47bc0a45..de93c028484 100644 --- a/pkg/kubectl/cmd/util/openapi/openapi_cache_test.go +++ b/pkg/kubectl/cmd/util/openapi/openapi_cache_test.go @@ -38,7 +38,7 @@ var _ = Describe("When reading openAPIData", func() { var err error var client *fakeOpenAPIClient var instance *openapi.CachingOpenAPIClient - var expectedData *openapi.Resources + var expectedData openapi.Resources BeforeEach(func() { 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") result, err := instance.OpenAPIData() Expect(err).To(BeNil()) - expectEqual(result, expectedData) + Expect(result).To(Equal(expectedData)) Expect(client.calls).To(Equal(1)) 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 result, err := instance.OpenAPIData() Expect(err).To(BeNil()) - expectEqual(result, expectedData) + Expect(result).To(Equal(expectedData)) Expect(client.calls).To(Equal(1)) // Second call shouldn't use the client result, err = instance.OpenAPIData() Expect(err).To(BeNil()) - expectEqual(result, expectedData) + Expect(result).To(Equal(expectedData)) Expect(client.calls).To(Equal(1)) names, err := getFilenames(tmpDir) @@ -153,7 +153,7 @@ var _ = Describe("Reading openAPIData", func() { By("getting the live openapi schema") result, err := instance.OpenAPIData() Expect(err).To(BeNil()) - expectEqual(result, expectedData) + Expect(result).To(Equal(expectedData)) Expect(client.calls).To(Equal(1)) files, err := ioutil.ReadDir(tmpDir) @@ -181,7 +181,7 @@ var _ = Describe("Reading openAPIData", func() { By("getting the live openapi schema") result, err := instance.OpenAPIData() Expect(err).To(BeNil()) - expectEqual(result, expectedData) + Expect(result).To(Equal(expectedData)) Expect(client.calls).To(Equal(1)) files, err := ioutil.ReadDir(tmpDir) @@ -204,19 +204,6 @@ func getFilenames(path string) ([]string, error) { 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 { calls int 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)) }) + return d.data, d.err } diff --git a/pkg/kubectl/cmd/util/openapi/openapi_getter.go b/pkg/kubectl/cmd/util/openapi/openapi_getter.go index 27febb562be..0b655dc0471 100644 --- a/pkg/kubectl/cmd/util/openapi/openapi_getter.go +++ b/pkg/kubectl/cmd/util/openapi/openapi_getter.go @@ -26,7 +26,7 @@ import ( type synchronizedOpenAPIGetter struct { // Cached results sync.Once - openAPISchema *Resources + openAPISchema Resources err error serverVersion string @@ -39,7 +39,7 @@ var _ Getter = &synchronizedOpenAPIGetter{} // Getter is an interface for fetching openapi specs and parsing them into an Resources struct type Getter interface { // OpenAPIData returns the parsed OpenAPIData - Get() (*Resources, error) + Get() (Resources, error) } // 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 -func (g *synchronizedOpenAPIGetter) Get() (*Resources, error) { +func (g *synchronizedOpenAPIGetter) Get() (Resources, error) { g.Do(func() { client := NewCachingOpenAPIClient(g.openAPIClient, g.serverVersion, g.cacheDir) result, err := client.OpenAPIData() diff --git a/pkg/kubectl/cmd/util/openapi/openapi_getter_test.go b/pkg/kubectl/cmd/util/openapi/openapi_getter_test.go index 0f61a5e6ba3..bbaca9dee35 100644 --- a/pkg/kubectl/cmd/util/openapi/openapi_getter_test.go +++ b/pkg/kubectl/cmd/util/openapi/openapi_getter_test.go @@ -27,7 +27,7 @@ import ( var _ = Describe("Getting the Resources", func() { var client *fakeOpenAPIClient - var expectedData *openapi.Resources + var expectedData openapi.Resources var instance openapi.Getter BeforeEach(func() { @@ -47,12 +47,12 @@ var _ = Describe("Getting the Resources", func() { result, err := instance.Get() Expect(err).To(BeNil()) - expectEqual(result, expectedData) + Expect(result).To(Equal(expectedData)) Expect(client.calls).To(Equal(1)) result, err = instance.Get() Expect(err).To(BeNil()) - expectEqual(result, expectedData) + Expect(result).To(Equal(expectedData)) // No additional client calls expected Expect(client.calls).To(Equal(1)) }) diff --git a/pkg/kubectl/cmd/util/openapi/openapi_test.go b/pkg/kubectl/cmd/util/openapi/openapi_test.go index 0b549f155aa..2d9ea5b97d6 100644 --- a/pkg/kubectl/cmd/util/openapi/openapi_test.go +++ b/pkg/kubectl/cmd/util/openapi/openapi_test.go @@ -17,9 +17,6 @@ limitations under the License. package openapi_test import ( - "fmt" - - "github.com/go-openapi/spec" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -28,395 +25,161 @@ import ( ) var _ = Describe("Reading apps/v1beta1/Deployment from openAPIData", func() { - var instance *openapi.Resources + var resources openapi.Resources BeforeEach(func() { s, err := data.OpenAPISchema() Expect(err).To(BeNil()) - instance, err = openapi.NewOpenAPIData(s) + resources, err = openapi.NewOpenAPIData(s) 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{ Kind: "Deployment", Version: "v1beta1", Group: "apps", } - It("should find the name by its GroupVersionKind", func() { - name, found := instance.GroupVersionKindToName[gvk] - fmt.Fprintf(GinkgoWriter, fmt.Sprintf("CHAO: instance.GroupVersionKindToName=%#v\n", instance.GroupVersionKindToName)) - Expect(found).To(BeTrue()) - Expect(name).To(Equal(deploymentName)) + var schema openapi.Schema + It("should lookup the Schema by its GroupVersionKind", func() { + schema = resources.LookupResource(gvk) + Expect(schema).ToNot(BeNil()) }) - var definition openapi.Kind - It("should find the definition by name", func() { - var found bool - definition, found = instance.NameToDefinition[deploymentName] - Expect(found).To(BeTrue()) - Expect(definition.Name).To(Equal(deploymentName)) - Expect(definition.PrimitiveType).To(BeEmpty()) + var deployment *openapi.Kind + It("should be a Kind", func() { + deployment = schema.(*openapi.Kind) + Expect(deployment).ToNot(BeNil()) }) - It("should lookup the Kind by its GroupVersionKind", func() { - d, found := instance.LookupResource(gvk) - Expect(found).To(BeTrue()) - Expect(d).To(Equal(definition)) + It("should have a path", func() { + Expect(deployment.GetPath().Get()).To(Equal([]string{"io.k8s.api.apps.v1beta1.Deployment"})) }) - It("should find the definition GroupVersionKind", func() { - Expect(definition.GroupVersionKind).To(Equal(gvk)) + It("should have a kind key of type string", func() { + 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() { - Expect(definition.Extensions).To(HaveKey("x-kubernetes-group-version-kind")) + It("should have a apiVersion key of type string", func() { + 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() { - By("for 'kind'") - Expect(definition.Fields).To(HaveKeyWithValue("kind", openapi.Type{ - TypeName: "string", - IsPrimitive: true, + It("should have a metadata key of type Reference", func() { + Expect(deployment.Fields).To(HaveKey("metadata")) + key := deployment.Fields["metadata"].(*openapi.Reference) + Expect(key).ToNot(BeNil()) + 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", })) - - By("for 'apiVersion'") - 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()) + condition := conditions.SubType.(*openapi.Reference) + Expect(condition.Reference).To(Equal("io.k8s.api.apps.v1beta1.DeploymentCondition")) }) - deploymentStatusName := "io.k8s.api.apps.v1beta1.DeploymentStatus" - - var definition openapi.Kind - It("should find the definition by name", func() { - var found bool - definition, found = instance.NameToDefinition[deploymentStatusName] - Expect(found).To(BeTrue()) - Expect(definition.Name).To(Equal(deploymentStatusName)) - Expect(definition.PrimitiveType).To(BeEmpty()) + var spec *openapi.Kind + It("should have a spec key of type Reference", func() { + Expect(deployment.Fields).To(HaveKey("spec")) + key := deployment.Fields["spec"].(*openapi.Reference) + Expect(key).ToNot(BeNil()) + Expect(key.Reference).To(Equal("io.k8s.api.apps.v1beta1.DeploymentSpec")) + spec = key.GetSubSchema().(*openapi.Kind) + Expect(spec).ToNot(BeNil()) }) - 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"] + It("should have a spec with no gvk", func() { + _, found := spec.GetExtensions()["x-kubernetes-group-version-kind"] Expect(found).To(BeFalse()) }) - It("should find the definition fields", func() { - By("for 'availableReplicas'") - Expect(definition.Fields).To(HaveKeyWithValue("availableReplicas", openapi.Type{ - TypeName: "integer", - IsPrimitive: true, - })) - - 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", - }, - })) + It("should have a spec with a PodTemplateSpec sub-field", func() { + Expect(spec.Fields).To(HaveKey("template")) + key := spec.Fields["template"].(*openapi.Reference) + Expect(key).ToNot(BeNil()) + Expect(key.Reference).To(Equal("io.k8s.api.core.v1.PodTemplateSpec")) }) }) -var _ = Describe("Reading apps/v1beta1/DeploymentSpec from openAPIData", func() { - var instance *openapi.Resources +var _ = Describe("Reading authorization.k8s.io/v1/SubjectAccessReview from openAPIData", func() { + var resources openapi.Resources BeforeEach(func() { - d, err := data.OpenAPISchema() + s, err := data.OpenAPISchema() Expect(err).To(BeNil()) - instance, err = openapi.NewOpenAPIData(d) + resources, err = openapi.NewOpenAPIData(s) Expect(err).To(BeNil()) }) - deploymentSpecName := "io.k8s.api.apps.v1beta1.DeploymentSpec" - - var definition openapi.Kind - It("should find the definition by name", func() { - 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 - } + gvk := schema.GroupVersionKind{ + Kind: "SubjectAccessReview", + Version: "v1", + Group: "authorization.k8s.io", } - 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"})) + }) +})