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
```
pull/6/head
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/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",

View File

@ -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

View File

@ -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",
},
},
},
},

View File

@ -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) {

View File

@ -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.

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.
// 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

View File

@ -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",

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

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