Merge pull request #44531 from pwittrock/kubectl-openapi

Automatic merge from submit-queue

OpenAPI support for kubectl

Support for openapi spec in kubectl.

Includes:
- downloading and caching openapi spec to a local file
- parsing openapi spec into binary serializable datastructures (10x faster load times 600ms -> 40ms)
- caching parsed openapi spec in memory for each command

```release-note
NONE
```
This commit is contained in:
Kubernetes Submit Queue 2017-04-26 16:59:17 -07:00 committed by GitHub
commit 433aec11c8
28 changed files with 1763 additions and 0 deletions

View File

@ -184,6 +184,7 @@ pkg/credentialprovider/aws
pkg/fieldpath
pkg/fields
pkg/hyperkube
pkg/kubectl/cmd/util/openapi
pkg/kubelet/api
pkg/kubelet/container
pkg/kubelet/envvars

View File

@ -131,6 +131,7 @@ func NewCmdGet(f cmdutil.Factory, out io.Writer, errOut io.Writer) *cobra.Comman
usage := "identifying the resource to get from a server."
cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage)
cmdutil.AddInclude3rdPartyFlags(cmd)
cmdutil.AddOpenAPIFlags(cmd)
cmd.Flags().StringVar(&options.Raw, "raw", options.Raw, "Raw URI to request from the server. Uses the transport specified by the kubeconfig file.")
return cmd
}

View File

@ -19,6 +19,7 @@ go_library(
"//pkg/client/clientset_generated/internalclientset:go_default_library",
"//pkg/kubectl:go_default_library",
"//pkg/kubectl/cmd/util:go_default_library",
"//pkg/kubectl/cmd/util/openapi:go_default_library",
"//pkg/kubectl/resource:go_default_library",
"//pkg/printers:go_default_library",
"//vendor/github.com/emicklei/go-restful/swagger:go_default_library",

View File

@ -42,6 +42,7 @@ import (
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/kubectl"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/printers"
)
@ -404,6 +405,10 @@ 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) DefaultNamespace() (string, bool, error) {
return f.tf.Namespace, false, f.tf.Err
}

View File

@ -37,6 +37,7 @@ go_library(
"//pkg/client/unversioned:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/kubectl:go_default_library",
"//pkg/kubectl/cmd/util/openapi:go_default_library",
"//pkg/kubectl/resource:go_default_library",
"//pkg/printers:go_default_library",
"//pkg/printers/internalversion:go_default_library",
@ -44,6 +45,7 @@ go_library(
"//pkg/version:go_default_library",
"//vendor/github.com/emicklei/go-restful/swagger:go_default_library",
"//vendor/github.com/evanphx/json-patch:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
@ -102,6 +104,7 @@ go_test(
"//pkg/kubectl/resource:go_default_library",
"//pkg/util/exec:go_default_library",
"//vendor/github.com/emicklei/go-restful/swagger:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
@ -137,6 +140,7 @@ filegroup(
":package-srcs",
"//pkg/kubectl/cmd/util/editor:all-srcs",
"//pkg/kubectl/cmd/util/jsonmerge:all-srcs",
"//pkg/kubectl/cmd/util/openapi:all-srcs",
"//pkg/kubectl/cmd/util/sanity:all-srcs",
],
tags = ["automanaged"],

View File

@ -25,6 +25,7 @@ import (
"time"
"github.com/emicklei/go-restful/swagger"
"github.com/go-openapi/spec"
"github.com/golang/glog"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -236,6 +237,10 @@ func (d *CachedDiscoveryClient) SwaggerSchema(version schema.GroupVersion) (*swa
return d.delegate.SwaggerSchema(version)
}
func (d *CachedDiscoveryClient) OpenAPISchema() (*spec.Swagger, error) {
return d.delegate.OpenAPISchema()
}
func (d *CachedDiscoveryClient) Fresh() bool {
d.mutex.Lock()
defer d.mutex.Unlock()

View File

@ -23,6 +23,7 @@ import (
"time"
"github.com/emicklei/go-restful/swagger"
"github.com/go-openapi/spec"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/errors"
@ -101,6 +102,7 @@ type fakeDiscoveryClient struct {
resourceCalls int
versionCalls int
swaggerCalls int
openAPICalls int
serverResourcesHandler func() ([]*metav1.APIResourceList, error)
}
@ -168,3 +170,8 @@ func (c *fakeDiscoveryClient) SwaggerSchema(version schema.GroupVersion) (*swagg
c.swaggerCalls = c.swaggerCalls + 1
return &swagger.ApiDeclaration{}, nil
}
func (c *fakeDiscoveryClient) OpenAPISchema() (*spec.Swagger, error) {
c.openAPICalls = c.openAPICalls + 1
return &spec.Swagger{}, nil
}

View File

@ -30,6 +30,7 @@ import (
"time"
"github.com/emicklei/go-restful/swagger"
"github.com/golang/glog"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@ -50,6 +51,7 @@ import (
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/printers"
)
@ -219,6 +221,8 @@ type ObjectMappingFactory interface {
Validator(validate bool, cacheDir string) (validation.Schema, error)
// 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)
}
// BuilderFactory holds the second level of factory methods. These functions depend upon ObjectMappingFactory and ClientAccessFactory methods.
@ -417,6 +421,7 @@ func writeSchemaFile(schemaData []byte, cacheDir, cacheFile, prefix, groupVersio
if _, err := io.Copy(tmpFile, bytes.NewBuffer(schemaData)); err != nil {
return err
}
glog.V(4).Infof("Writing swagger cache (dir %v) file %v (from %v)", cacheDir, cacheFile, tmpFile.Name())
if err := os.Link(tmpFile.Name(), cacheFile); err != nil {
// If we can't write due to file existing, or permission problems, keep going.
if os.IsExist(err) || os.IsPermission(err) {

View File

@ -24,6 +24,7 @@ import (
"os"
"path"
"sort"
"sync"
"time"
"github.com/emicklei/go-restful/swagger"
@ -46,6 +47,7 @@ import (
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/kubectl/cmd/util/openapi"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/printers"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
@ -53,6 +55,14 @@ import (
type ring1Factory struct {
clientAccessFactory ClientAccessFactory
// openAPIGetter loads and caches openapi specs
openAPIGetter openAPIGetter
}
type openAPIGetter struct {
once sync.Once
getter openapi.Getter
}
func NewObjectMappingFactory(clientAccessFactory ClientAccessFactory) ObjectMappingFactory {
@ -427,3 +437,41 @@ func (f *ring1Factory) SwaggerSchema(gvk schema.GroupVersionKind) (*swagger.ApiD
}
return discovery.SwaggerSchema(version)
}
// OpenAPISchema returns metadata and structural information about Kubernetes object definitions.
// Will try to cache the data to a local file. Cache is written and read from a
// file created with ioutil.TempFile and obeys the expiration semantics of that file.
// The cache location is a function of the client and server versions so that the open API
// 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) {
discovery, err := f.clientAccessFactory.DiscoveryClient()
if err != nil {
return nil, err
}
// Lazily initialize the OpenAPIGetter once
f.openAPIGetter.once.Do(func() {
// Get the server version for caching the openapi spec
versionString := ""
version, err := discovery.ServerVersion()
if err != nil {
// Cache the result under the server version
versionString = version.String()
}
// Get the cache directory for caching the openapi spec
cacheDir, err = substituteUserHome(cacheDir)
if err != nil {
// Don't cache the result if we couldn't substitute the home directory
cacheDir = ""
}
// Create the caching OpenAPIGetter
f.openAPIGetter.getter = openapi.NewOpenAPIGetter(cacheDir, versionString, discovery)
})
// Delegate to the OpenAPIGetter
return f.openAPIGetter.getter.Get()
}

View File

@ -408,6 +408,15 @@ func AddValidateOptionFlags(cmd *cobra.Command, options *ValidateOptions) {
cmd.MarkFlagFilename("schema-cache-dir")
}
func AddOpenAPIFlags(cmd *cobra.Command) {
cmd.Flags().String("schema-cache-dir",
fmt.Sprintf("~/%s/%s", clientcmd.RecommendedHomeDir, clientcmd.RecommendedSchemaName),
fmt.Sprintf("If non-empty, load/store cached API schemas in this directory, default is '$HOME/%s/%s'",
clientcmd.RecommendedHomeDir, clientcmd.RecommendedSchemaName),
)
cmd.MarkFlagFilename("schema-cache-dir")
}
func AddFilenameOptionFlags(cmd *cobra.Command, options *resource.FilenameOptions, usage string) {
kubectl.AddJsonFilenameFlag(cmd, &options.Filenames, "Filename, directory, or URL to files "+usage)
cmd.Flags().BoolVarP(&options.Recursive, "recursive", "R", options.Recursive, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.")

View File

@ -0,0 +1,69 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = [
"openapi_cache_test.go",
"openapi_getter_test.go",
"openapi_test.go",
],
library = ":go_default_library",
tags = ["automanaged"],
deps = [
"//vendor/github.com/go-openapi/loads:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/onsi/ginkgo:go_default_library",
"//vendor/github.com/onsi/gomega:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"openapi.go",
"openapi_cache.go",
"openapi_getter.go",
],
tags = ["automanaged"],
deps = [
"//pkg/version:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/golang/glog: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",
],
)
go_test(
name = "go_default_xtest",
srcs = ["openapi_suite_test.go"],
tags = ["automanaged"],
deps = [
"//vendor/github.com/onsi/ginkgo:go_default_library",
"//vendor/github.com/onsi/gomega:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,21 @@
/*
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 is a collection of libraries for fetching the openapi spec
// from a Kubernetes server and then indexing the type definitions.
// The openapi spec contains the object model definitions and extensions metadata
// such as the patchStrategy and patchMergeKey for creating patches.
package openapi

View File

@ -0,0 +1,391 @@
/*
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"
"github.com/go-openapi/spec"
"github.com/golang/glog"
"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"
// Integer is the name for integer types
const Integer = "integer"
// 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
}
// 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
}
// Kind defines a Kubernetes object Kind
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.kubernetes.pkg.apis.apps.v1beta1.Deployment
Name string
// 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 spec.Extensions
// Fields are the fields defined for this Kind
Fields map[string]Type
}
// 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
// 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 spec.Extensions
}
// newOpenAPIData parses the resource definitions in openapi data by groupversionkind and name
func newOpenAPIData(s *spec.Swagger) (*Resources, error) {
o := &Resources{
GroupVersionKindToName: map[schema.GroupVersionKind]string{},
NameToDefinition: map[string]Kind{},
}
// Parse and index definitions by name
for name, d := range s.Definitions {
definition := o.parseDefinition(name, d)
o.NameToDefinition[name] = definition
if len(definition.GroupVersionKind.Kind) > 0 {
o.GroupVersionKindToName[definition.GroupVersionKind] = name
}
}
if err := o.validate(); err != nil {
return nil, err
}
return o, nil
}
// 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
}
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 (o *Resources) parseDefinition(name string, s spec.Schema) Kind {
gvk, err := o.getGroupVersionKind(s)
value := Kind{
Name: name,
GroupVersionKind: gvk,
Extensions: s.Extensions,
Fields: map[string]Type{},
}
if err != nil {
glog.Warning(err)
}
// Definition represents a primitive type - e.g.
// io.k8s.apimachinery.pkg.util.intstr.IntOrString
if o.isPrimitive(s) {
value.PrimitiveType = o.getTypeNameForField(s)
}
for fieldname, property := range s.Properties {
value.Fields[fieldname] = o.parseField(property)
}
return value
}
func (o *Resources) parseField(s spec.Schema) Type {
def := Type{
TypeName: o.getTypeNameForField(s),
IsPrimitive: o.isPrimitive(s),
IsArray: o.isArray(s),
IsMap: o.isMap(s),
IsKind: o.isDefinitionReference(s),
}
if elementType, arrayErr := o.getElementType(s); arrayErr == nil {
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 = s.Extensions
return def
}
// isArray returns true if s is an array type.
func (o *Resources) isArray(s spec.Schema) bool {
if len(s.Properties) > 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) == Array
}
// isMap returns true if s is a map type.
func (o *Resources) isMap(s spec.Schema) bool {
if len(s.Properties) > 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 spec.Schema) bool {
if len(s.Properties) > 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 spec.Schema) string {
if len(s.Type) != 1 {
return ""
}
return strings.ToLower(s.Type[0])
}
func (o *Resources) getTypeNameForField(s spec.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.Items.Schema))
}
if o.isMap(s) {
return fmt.Sprintf("%s map", o.getTypeNameForField(*s.AdditionalProperties.Schema))
}
// Get the value for primitive types
if o.isPrimitive(s) {
return fmt.Sprintf("%s", s.Type[0])
}
return ""
}
// isDefinitionReference returns true s is a complex type that should have a Kind.
func (o *Resources) isDefinitionReference(s spec.Schema) bool {
if len(s.Properties) > 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.Type) > 0 {
// Definition references won't have a type
return false
}
p := s.SchemaProps.Ref.GetPointer().String()
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 spec.Schema) (spec.Schema, error) {
if !o.isArray(s) {
return spec.Schema{}, fmt.Errorf("%v is not an array type", s.Type)
}
return *s.Items.Schema, nil
}
// getElementType returns the type of an element for maps
// returns an error if s is not a map.
func (o *Resources) getValueType(s spec.Schema) (spec.Schema, error) {
if !o.isMap(s) {
return spec.Schema{}, fmt.Errorf("%v is not an map type", s.Type)
}
return *s.AdditionalProperties.Schema, nil
}
// nameForDefinitionField returns the definition name for the schema (field) if it is a complex type
func (o *Resources) nameForDefinitionField(s spec.Schema) string {
p := s.SchemaProps.Ref.GetPointer().String()
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
// Expected format for s.Extensions: map[string][]map[string]string
// map[x-kubernetes-group-version-kind:[map[Group:authentication.k8s.io Version:v1 Kind:TokenReview]]]
func (o *Resources) getGroupVersionKind(s spec.Schema) (schema.GroupVersionKind, error) {
empty := schema.GroupVersionKind{}
// Get the extensions
extList, f := s.Extensions[groupVersionKindExtensionKey]
if !f {
return empty, fmt.Errorf("No %s extension present in %v", groupVersionKindExtensionKey, s.Extensions)
}
// 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, s.Extensions)
}
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[string]interface{})
if !ok {
return empty, fmt.Errorf("%s extension has unexpected type %T in %s", groupVersionKindExtensionKey, gvk, s.Extensions)
}
group, ok := gvkMap["Group"].(string)
if !ok {
return empty, fmt.Errorf("%s extension missing Group: %v", groupVersionKindExtensionKey, gvkMap)
}
version, ok := gvkMap["Version"].(string)
if !ok {
return empty, fmt.Errorf("%s extension missing Version: %v", groupVersionKindExtensionKey, gvkMap)
}
kind, ok := gvkMap["Kind"].(string)
if !ok {
return empty, fmt.Errorf("%s extension missing Kind: %v", groupVersionKindExtensionKey, gvkMap)
}
return schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
}, nil
}

View File

@ -0,0 +1,193 @@
/*
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 (
"bytes"
"encoding/gob"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/golang/glog"
"k8s.io/client-go/discovery"
"k8s.io/kubernetes/pkg/version"
)
func init() {
registerBinaryEncodingTypes()
}
const openapiFileName = "openapi_cache"
type cachingOpenAPIClient struct {
version string
client discovery.OpenAPISchemaInterface
cacheDirName string
}
// newCachingOpenAPIClient returns a new discovery.OpenAPISchemaInterface
// that will read the openapi spec from a local cache if it exists, and
// if not will then fetch an openapi spec using a client.
// client: used to fetch a new openapi spec if a local cache is not found
// version: the server version and used as part of the cache file location
// cacheDir: the directory under which the cache file will be written
func newCachingOpenAPIClient(client discovery.OpenAPISchemaInterface, version, cacheDir string) *cachingOpenAPIClient {
return &cachingOpenAPIClient{
client: client,
version: version,
cacheDirName: cacheDir,
}
}
// openAPIData returns an openapi spec.
// 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) {
// Try to use the cached version
if c.useCache() {
doc, err := c.readOpenAPICache()
if err == nil {
return doc, nil
}
}
// No cached version found, download from server
s, err := c.client.OpenAPISchema()
if err != nil {
glog.V(2).Infof("Failed to download openapi data %v", err)
return nil, err
}
oa, err := newOpenAPIData(s)
if err != nil {
glog.V(2).Infof("Failed to parse openapi data %v", err)
return nil, err
}
// Try to cache the openapi spec
if c.useCache() {
err = c.writeToCache(oa)
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)
}
}
// Return the parsed data
return oa, nil
}
// useCache returns true if the client should try to use the cache file
func (c *cachingOpenAPIClient) useCache() bool {
return len(c.version) > 0 && len(c.cacheDirName) > 0
}
// readOpenAPICache tries to read the openapi spec from the local file cache
func (c *cachingOpenAPIClient) readOpenAPICache() (*Resources, error) {
// Get the filename to read
filename := c.openAPICacheFilename()
// Read the cached file
data, err := ioutil.ReadFile(filename)
if err != nil {
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
}
// 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 {
// 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)
if err != nil {
return fmt.Errorf("Could not binary encode openapi spec: %v", err)
}
// Create a new temp file for the cached openapi spec.
cacheDir := filepath.Dir(cacheFile)
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return fmt.Errorf("Could not create directory: %v %v", cacheDir, err)
}
tmpFile, err := ioutil.TempFile(cacheDir, "openapi")
if err != nil {
return fmt.Errorf("Could not create temp cache file: %v %v", cacheFile, err)
}
// Write the binary encoded openapi spec to the temp file
if _, err := io.Copy(tmpFile, bytes.NewBuffer(b)); err != nil {
return fmt.Errorf("Could not write temp cache file: %v", err)
}
// Link the temp cache file to the constant cache filepath
return linkFiles(tmpFile.Name(), cacheFile)
}
// openAPICacheFilename returns the filename to read the cache from
func (c *cachingOpenAPIClient) openAPICacheFilename() string {
// Cache using the client and server versions
return filepath.Join(c.cacheDirName, c.version, version.Get().GitVersion, openapiFileName)
}
// linkFiles links the old file to the new file
func linkFiles(old, new string) error {
if err := os.Link(old, new); err != nil {
// If we can't write due to file existing, or permission problems, keep going.
if os.IsExist(err) || os.IsPermission(err) {
return nil
}
return err
}
return nil
}
// registerBinaryEncodingTypes registers the types so they can be binary encoded by gob
func registerBinaryEncodingTypes() {
gob.Register(map[string]interface{}{})
gob.Register([]interface{}{})
gob.Register(Resources{})
}

View File

@ -0,0 +1,272 @@
/*
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"
"io/ioutil"
"os"
"path/filepath"
"sync"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("When reading openAPIData", func() {
var tmpDir string
var err error
var client *fakeOpenAPIClient
var instance *cachingOpenAPIClient
var expectedData *Resources
BeforeEach(func() {
tmpDir, err = ioutil.TempDir("", "openapi_cache_test")
Expect(err).To(BeNil())
client = &fakeOpenAPIClient{}
instance = newCachingOpenAPIClient(client, "v1.6", tmpDir)
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
expectedData, err = newOpenAPIData(d)
Expect(err).To(BeNil())
})
AfterEach(func() {
os.RemoveAll(tmpDir)
})
It("should write to the cache", func() {
By("getting the live openapi spec from the server")
result, err := instance.openAPIData()
Expect(err).To(BeNil())
expectEqual(result, expectedData)
Expect(client.calls).To(Equal(1))
By("writing the live openapi spec to a local cache file")
names, err := getFilenames(tmpDir)
Expect(err).To(BeNil())
Expect(names).To(ConsistOf("v1.6"))
names, err = getFilenames(filepath.Join(tmpDir, "v1.6"))
Expect(err).To(BeNil())
Expect(names).To(HaveLen(1))
clientVersion := names[0]
names, err = getFilenames(filepath.Join(tmpDir, "v1.6", clientVersion))
Expect(err).To(BeNil())
Expect(names).To(ContainElement("openapi_cache"))
})
It("should read from the cache", func() {
// First call should use the client
result, err := instance.openAPIData()
Expect(err).To(BeNil())
expectEqual(result, 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(client.calls).To(Equal(1))
names, err := getFilenames(tmpDir)
Expect(err).To(BeNil())
Expect(names).To(ConsistOf("v1.6"))
})
It("propagate errors that are encountered", func() {
// Expect an error
client.err = fmt.Errorf("expected error")
result, err := instance.openAPIData()
Expect(err.Error()).To(Equal(client.err.Error()))
Expect(result).To(BeNil())
Expect(client.calls).To(Equal(1))
// No cache file is written
files, err := ioutil.ReadDir(tmpDir)
Expect(err).To(BeNil())
Expect(files).To(HaveLen(0))
// Client error is not cached
result, err = instance.openAPIData()
Expect(err.Error()).To(Equal(client.err.Error()))
Expect(result).To(BeNil())
Expect(client.calls).To(Equal(2))
})
})
var _ = Describe("Reading openAPIData", func() {
var tmpDir string
var serverVersion string
var cacheDir string
BeforeEach(func() {
var err error
tmpDir, err = ioutil.TempDir("", "openapi_cache_test")
Expect(err).To(BeNil())
})
AfterEach(func() {
os.RemoveAll(tmpDir)
})
// Set the serverVersion to empty
Context("when the server version is empty", func() {
BeforeEach(func() {
serverVersion = ""
cacheDir = tmpDir
})
It("should not cache the result", func() {
client := &fakeOpenAPIClient{}
instance := newCachingOpenAPIClient(client, serverVersion, cacheDir)
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
expectedData, err := newOpenAPIData(d)
Expect(err).To(BeNil())
By("getting the live openapi schema")
result, err := instance.openAPIData()
Expect(err).To(BeNil())
expectEqual(result, expectedData)
Expect(client.calls).To(Equal(1))
files, err := ioutil.ReadDir(tmpDir)
Expect(err).To(BeNil())
Expect(files).To(HaveLen(0))
})
})
Context("when the cache directory is empty", func() {
BeforeEach(func() {
serverVersion = "v1.6"
cacheDir = ""
})
It("should not cache the result", func() {
client := &fakeOpenAPIClient{}
instance := newCachingOpenAPIClient(client, serverVersion, cacheDir)
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
expectedData, err := newOpenAPIData(d)
Expect(err).To(BeNil())
By("getting the live openapi schema")
result, err := instance.openAPIData()
Expect(err).To(BeNil())
expectEqual(result, expectedData)
Expect(client.calls).To(Equal(1))
files, err := ioutil.ReadDir(tmpDir)
Expect(err).To(BeNil())
Expect(files).To(HaveLen(0))
})
})
})
// Test Utils
func getFilenames(path string) ([]string, error) {
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
result := []string{}
for _, n := range files {
result = append(result, n.Name())
}
return result, nil
}
func expectEqual(a *Resources, b *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
}
func (f *fakeOpenAPIClient) OpenAPISchema() (*spec.Swagger, error) {
f.calls = f.calls + 1
if f.err != nil {
return nil, f.err
}
return data.OpenAPISchema()
}
// Test utils
var data apiData
type apiData struct {
sync.Once
data *spec.Swagger
err error
}
func (d *apiData) OpenAPISchema() (*spec.Swagger, error) {
d.Do(func() {
// Get the path to the swagger.json file
wd, err := os.Getwd()
if err != nil {
d.err = err
return
}
abs, err := filepath.Abs(wd)
if err != nil {
d.err = err
return
}
root := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(abs)))))
specpath := filepath.Join(root, "api", "openapi-spec", "swagger.json")
_, err = os.Stat(specpath)
if err != nil {
d.err = err
return
}
// Load the openapi document
doc, err := loads.Spec(specpath)
if err != nil {
d.err = err
return
}
d.data = doc.Spec()
})
return d.data, d.err
}

View File

@ -0,0 +1,71 @@
/*
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 (
"sync"
"k8s.io/client-go/discovery"
)
// synchronizedOpenAPIGetter fetches the openapi schema once and then caches it in memory
type synchronizedOpenAPIGetter struct {
// Cached results
sync.Once
openAPISchema *Resources
err error
serverVersion string
cacheDir string
openAPIClient discovery.OpenAPISchemaInterface
}
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)
}
// NewOpenAPIGetter returns an object to return OpenAPIDatas which either read from a
// local file cache or read from a server, and then stored in memory for subsequent invocations
func NewOpenAPIGetter(cacheDir, serverVersion string, openAPIClient discovery.OpenAPISchemaInterface) Getter {
return &synchronizedOpenAPIGetter{
serverVersion: serverVersion,
cacheDir: cacheDir,
openAPIClient: openAPIClient,
}
}
// Resources implements Getter
func (g *synchronizedOpenAPIGetter) Get() (*Resources, error) {
g.Do(func() {
client := newCachingOpenAPIClient(g.openAPIClient, g.serverVersion, g.cacheDir)
result, err := client.openAPIData()
if err != nil {
g.err = err
return
}
// Save the result
g.openAPISchema = result
})
// Return the save result
return g.openAPISchema, g.err
}

View File

@ -0,0 +1,74 @@
/*
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"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Getting the Resources", func() {
var client *fakeOpenAPIClient
var expectedData *Resources
var instance Getter
BeforeEach(func() {
client = &fakeOpenAPIClient{}
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
expectedData, err = newOpenAPIData(d)
Expect(err).To(BeNil())
instance = NewOpenAPIGetter("", "", client)
})
Context("when the server returns a successful result", func() {
It("should return the same data for multiple calls", func() {
Expect(client.calls).To(Equal(0))
result, err := instance.Get()
Expect(err).To(BeNil())
expectEqual(result, expectedData)
Expect(client.calls).To(Equal(1))
result, err = instance.Get()
Expect(err).To(BeNil())
expectEqual(result, expectedData)
// No additional client calls expected
Expect(client.calls).To(Equal(1))
})
})
Context("when the server returns an unsuccessful result", func() {
It("should return the same instance for multiple calls.", func() {
Expect(client.calls).To(Equal(0))
client.err = fmt.Errorf("expected error")
_, err := instance.Get()
Expect(err).To(Equal(client.err))
Expect(client.calls).To(Equal(1))
_, err = instance.Get()
Expect(err).To(Equal(client.err))
// No additional client calls expected
Expect(client.calls).To(Equal(1))
})
})
})

View File

@ -0,0 +1,29 @@
/*
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_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestOpenapi(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Openapi Suite")
}

View File

@ -0,0 +1,419 @@
/*
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"
"github.com/go-openapi/spec"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var _ = Describe("Reading apps/v1beta1/Deployment from openAPIData", func() {
var instance *Resources
BeforeEach(func() {
s, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = newOpenAPIData(s)
Expect(err).To(BeNil())
})
deploymentName := "io.k8s.kubernetes.pkg.apis.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]
Expect(found).To(BeTrue())
Expect(name).To(Equal(deploymentName))
})
var definition 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())
})
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 find the definition GroupVersionKind", func() {
Expect(definition.GroupVersionKind).To(Equal(gvk))
})
It("should find the definition GroupVersionKind extensions", func() {
Expect(definition.Extensions).To(HaveKey("x-kubernetes-group-version-kind"))
})
It("should find the definition fields", func() {
By("for 'kind'")
Expect(definition.Fields).To(HaveKeyWithValue("kind", Type{
TypeName: "string",
IsPrimitive: true,
}))
By("for 'apiVersion'")
Expect(definition.Fields).To(HaveKeyWithValue("apiVersion", Type{
TypeName: "string",
IsPrimitive: true,
}))
By("for 'metadata'")
Expect(definition.Fields).To(HaveKeyWithValue("metadata", Type{
TypeName: "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta",
IsKind: true,
}))
By("for 'spec'")
Expect(definition.Fields).To(HaveKeyWithValue("spec", Type{
TypeName: "io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentSpec",
IsKind: true,
}))
By("for 'status'")
Expect(definition.Fields).To(HaveKeyWithValue("status", Type{
TypeName: "io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentStatus",
IsKind: true,
}))
})
})
var _ = Describe("Reading apps/v1beta1/DeploymentStatus from openAPIData", func() {
var instance *Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = newOpenAPIData(d)
Expect(err).To(BeNil())
})
deploymentStatusName := "io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentStatus"
var definition 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())
})
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 'availableReplicas'")
Expect(definition.Fields).To(HaveKeyWithValue("availableReplicas", Type{
TypeName: "integer",
IsPrimitive: true,
}))
By("for 'conditions'")
Expect(definition.Fields).To(HaveKeyWithValue("conditions", Type{
TypeName: "io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentCondition array",
IsArray: true,
ElementType: &Type{
TypeName: "io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentCondition",
IsKind: true,
},
Extensions: spec.Extensions{
"x-kubernetes-patch-merge-key": "type",
"x-kubernetes-patch-strategy": "merge",
},
}))
})
})
var _ = Describe("Reading apps/v1beta1/DeploymentSpec from openAPIData", func() {
var instance *Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = newOpenAPIData(d)
Expect(err).To(BeNil())
})
deploymentSpecName := "io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentSpec"
var definition 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", Type{
TypeName: "io.k8s.kubernetes.pkg.api.v1.PodTemplateSpec",
IsKind: true,
}))
})
})
var _ = Describe("Reading v1/ObjectMeta from openAPIData", func() {
var instance *Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = newOpenAPIData(d)
Expect(err).To(BeNil())
})
objectMetaName := "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
var definition 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", Type{
TypeName: "string array",
IsArray: true,
ElementType: &Type{
TypeName: "string",
IsPrimitive: true,
},
Extensions: spec.Extensions{
"x-kubernetes-patch-strategy": "merge",
},
}))
By("for 'ownerReferences'")
Expect(definition.Fields).To(HaveKeyWithValue("ownerReferences", Type{
TypeName: "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference array",
IsArray: true,
ElementType: &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", Type{
TypeName: "string map",
IsMap: true,
ElementType: &Type{
TypeName: "string",
IsPrimitive: true,
},
}))
})
})
var _ = Describe("Reading v1/NodeStatus from openAPIData", func() {
var instance *Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = newOpenAPIData(d)
Expect(err).To(BeNil())
})
nodeStatusName := "io.k8s.kubernetes.pkg.api.v1.NodeStatus"
var definition 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", Type{
TypeName: "io.k8s.apimachinery.pkg.api.resource.Quantity map",
IsMap: true,
ElementType: &Type{
TypeName: "io.k8s.apimachinery.pkg.api.resource.Quantity",
IsKind: true,
},
}))
})
})
var _ = Describe("Reading Utility Definitions from openAPIData", func() {
var instance *Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = newOpenAPIData(d)
Expect(err).To(BeNil())
})
Context("for util.intstr.IntOrString", func() {
var definition 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 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 *Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = 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 *Resources
BeforeEach(func() {
d, err := data.OpenAPISchema()
Expect(err).To(BeNil())
instance, err = newOpenAPIData(d)
Expect(err).To(BeNil())
})
subjectAccessReviewSpecName := "io.k8s.kubernetes.pkg.apis.authorization.v1.SubjectAccessReviewSpec"
var definition 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", Type{
TypeName: "string array map",
IsMap: true,
ElementType: &Type{
TypeName: "string array",
IsArray: true,
ElementType: &Type{
TypeName: "string",
IsPrimitive: true,
},
},
}))
})
})
func oneOf(values ...bool) bool {
found := false
for _, v := range values {
if v && found {
return false
}
if v {
found = true
}
}
return found
}

View File

@ -334,6 +334,10 @@
"ImportPath": "github.com/ghodss/yaml",
"Rev": "73d445a93680fa1a78ae23a5839bad48f32ba1ee"
},
{
"ImportPath": "github.com/go-openapi/analysis",
"Rev": "b44dc874b601d9e4e2f6e19140e794ba24bead3b"
},
{
"ImportPath": "github.com/go-openapi/jsonpointer",
"Rev": "46af16f9f7b149af66e5d1bd010e3574dc06de98"
@ -342,6 +346,10 @@
"ImportPath": "github.com/go-openapi/jsonreference",
"Rev": "13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272"
},
{
"ImportPath": "github.com/go-openapi/loads",
"Rev": "18441dfa706d924a39a030ee2c3b1d8d81917b38"
},
{
"ImportPath": "github.com/go-openapi/spec",
"Rev": "6aced65f8501fe1217321abf0749d354824ba2ff"

View File

@ -82,6 +82,10 @@
"ImportPath": "github.com/ghodss/yaml",
"Rev": "73d445a93680fa1a78ae23a5839bad48f32ba1ee"
},
{
"ImportPath": "github.com/go-openapi/analysis",
"Rev": "b44dc874b601d9e4e2f6e19140e794ba24bead3b"
},
{
"ImportPath": "github.com/go-openapi/jsonpointer",
"Rev": "46af16f9f7b149af66e5d1bd010e3574dc06de98"
@ -90,6 +94,10 @@
"ImportPath": "github.com/go-openapi/jsonreference",
"Rev": "13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272"
},
{
"ImportPath": "github.com/go-openapi/loads",
"Rev": "18441dfa706d924a39a030ee2c3b1d8d81917b38"
},
{
"ImportPath": "github.com/go-openapi/spec",
"Rev": "6aced65f8501fe1217321abf0749d354824ba2ff"

View File

@ -19,6 +19,8 @@ go_library(
tags = ["automanaged"],
deps = [
"//vendor/github.com/emicklei/go-restful/swagger:go_default_library",
"//vendor/github.com/go-openapi/loads:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
@ -44,6 +46,7 @@ go_test(
tags = ["automanaged"],
deps = [
"//vendor/github.com/emicklei/go-restful/swagger:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",

View File

@ -25,6 +25,8 @@ import (
"github.com/emicklei/go-restful/swagger"
"github.com/go-openapi/loads"
"github.com/go-openapi/spec"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@ -47,6 +49,7 @@ type DiscoveryInterface interface {
ServerResourcesInterface
ServerVersionInterface
SwaggerSchemaInterface
OpenAPISchemaInterface
}
// CachedDiscoveryInterface is a DiscoveryInterface with cache invalidation and freshness.
@ -91,6 +94,12 @@ type SwaggerSchemaInterface interface {
SwaggerSchema(version schema.GroupVersion) (*swagger.ApiDeclaration, error)
}
// OpenAPISchemaInterface has a method to retrieve the open API schema.
type OpenAPISchemaInterface interface {
// OpenAPISchema retrieves and parses the swagger API schema the server supports.
OpenAPISchema() (*spec.Swagger, error)
}
// DiscoveryClient implements the functions that discover server-supported API groups,
// versions and resources.
type DiscoveryClient struct {
@ -332,6 +341,7 @@ func (d *DiscoveryClient) ServerVersion() (*version.Info, error) {
}
// SwaggerSchema retrieves and parses the swagger API schema the server supports.
// TODO: Replace usages with Open API. Tracked in https://github.com/kubernetes/kubernetes/issues/44589
func (d *DiscoveryClient) SwaggerSchema(version schema.GroupVersion) (*swagger.ApiDeclaration, error) {
if version.Empty() {
return nil, fmt.Errorf("groupVersion cannot be empty")
@ -365,6 +375,21 @@ func (d *DiscoveryClient) SwaggerSchema(version schema.GroupVersion) (*swagger.A
return &schema, nil
}
// OpenAPISchema fetches the open api schema using a rest client and parses the json.
// Warning: this is very expensive (~1.2s)
func (d *DiscoveryClient) OpenAPISchema() (*spec.Swagger, error) {
data, err := d.restClient.Get().AbsPath("/swagger.json").Do().Raw()
if err != nil {
return nil, err
}
msg := json.RawMessage(data)
doc, err := loads.Analyzed(msg, "")
if err != nil {
return nil, err
}
return doc.Spec(), err
}
// withRetries retries the given recovery function in case the groups supported by the server change after ServerGroup() returns.
func withRetries(maxRetries int, f func(failEarly bool) ([]*metav1.APIResourceList, error)) ([]*metav1.APIResourceList, error) {
var result []*metav1.APIResourceList

View File

@ -18,6 +18,7 @@ package discovery_test
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
@ -25,6 +26,7 @@ import (
"github.com/emicklei/go-restful/swagger"
"github.com/go-openapi/spec"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
@ -325,6 +327,81 @@ func TestGetSwaggerSchemaFail(t *testing.T) {
}
}
var returnedOpenAPI = spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Definitions: spec.Definitions{
"fake.type.1": spec.Schema{
SchemaProps: spec.SchemaProps{
Properties: map[string]spec.Schema{
"count": {
SchemaProps: spec.SchemaProps{
Type: []string{"integer"},
},
},
},
},
},
"fake.type.2": spec.Schema{
SchemaProps: spec.SchemaProps{
Properties: map[string]spec.Schema{
"count": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
},
},
},
},
},
},
},
},
},
},
}
func openapiSchemaFakeServer() (*httptest.Server, error) {
var sErr error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/swagger.json" {
sErr = fmt.Errorf("Unexpected url %v", req.URL)
}
if req.Method != "GET" {
sErr = fmt.Errorf("Unexpected method %v", req.Method)
}
output, err := json.Marshal(returnedOpenAPI)
if err != nil {
sErr = err
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(output)
}))
return server, sErr
}
func TestGetOpenAPISchema(t *testing.T) {
server, err := openapiSchemaFakeServer()
if err != nil {
t.Errorf("unexpected error starting fake server: %v", err)
}
defer server.Close()
client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL})
got, err := client.OpenAPISchema()
if err != nil {
t.Fatalf("unexpected error getting openapi: %v", err)
}
if e, a := returnedOpenAPI, *got; !reflect.DeepEqual(e, a) {
t.Errorf("expected %v, got %v", e, a)
}
}
func TestServerPreferredResources(t *testing.T) {
stable := metav1.APIResourceList{
GroupVersion: "v1",

View File

@ -13,6 +13,7 @@ go_library(
tags = ["automanaged"],
deps = [
"//vendor/github.com/emicklei/go-restful/swagger:go_default_library",
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/version:go_default_library",

View File

@ -21,6 +21,7 @@ import (
"github.com/emicklei/go-restful/swagger"
"github.com/go-openapi/spec"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/version"
@ -92,6 +93,8 @@ func (c *FakeDiscovery) SwaggerSchema(version schema.GroupVersion) (*swagger.Api
return &swagger.ApiDeclaration{}, nil
}
func (c *FakeDiscovery) OpenAPISchema() (*spec.Swagger, error) { return &spec.Swagger{}, nil }
func (c *FakeDiscovery) RESTClient() restclient.Interface {
return nil
}

View File

@ -30,6 +30,7 @@ import (
"k8s.io/client-go/rest/fake"
"github.com/emicklei/go-restful/swagger"
"github.com/go-openapi/spec"
"github.com/stretchr/testify/assert"
)
@ -347,3 +348,7 @@ func (c *fakeCachedDiscoveryInterface) ServerVersion() (*version.Info, error) {
func (c *fakeCachedDiscoveryInterface) SwaggerSchema(version schema.GroupVersion) (*swagger.ApiDeclaration, error) {
return &swagger.ApiDeclaration{}, nil
}
func (c *fakeCachedDiscoveryInterface) OpenAPISchema() (*spec.Swagger, error) {
return &spec.Swagger{}, nil
}

View File

@ -122,6 +122,10 @@
"ImportPath": "github.com/ghodss/yaml",
"Rev": "73d445a93680fa1a78ae23a5839bad48f32ba1ee"
},
{
"ImportPath": "github.com/go-openapi/analysis",
"Rev": "b44dc874b601d9e4e2f6e19140e794ba24bead3b"
},
{
"ImportPath": "github.com/go-openapi/jsonpointer",
"Rev": "46af16f9f7b149af66e5d1bd010e3574dc06de98"
@ -130,6 +134,10 @@
"ImportPath": "github.com/go-openapi/jsonreference",
"Rev": "13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272"
},
{
"ImportPath": "github.com/go-openapi/loads",
"Rev": "18441dfa706d924a39a030ee2c3b1d8d81917b38"
},
{
"ImportPath": "github.com/go-openapi/spec",
"Rev": "6aced65f8501fe1217321abf0749d354824ba2ff"