diff --git a/hack/.linted_packages b/hack/.linted_packages index 045f928dcae..f5a412f5457 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -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 diff --git a/pkg/kubectl/cmd/get.go b/pkg/kubectl/cmd/get.go index a8a799c3e53..1f69ba368d6 100644 --- a/pkg/kubectl/cmd/get.go +++ b/pkg/kubectl/cmd/get.go @@ -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 } diff --git a/pkg/kubectl/cmd/testing/BUILD b/pkg/kubectl/cmd/testing/BUILD index e5dd13aa7dd..d03414078ab 100644 --- a/pkg/kubectl/cmd/testing/BUILD +++ b/pkg/kubectl/cmd/testing/BUILD @@ -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", diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index e43d77c6d51..9c40a3487e2 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -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 } diff --git a/pkg/kubectl/cmd/util/BUILD b/pkg/kubectl/cmd/util/BUILD index 0328ecfb14a..a51ab0c68f9 100644 --- a/pkg/kubectl/cmd/util/BUILD +++ b/pkg/kubectl/cmd/util/BUILD @@ -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"], diff --git a/pkg/kubectl/cmd/util/cached_discovery.go b/pkg/kubectl/cmd/util/cached_discovery.go index c4d0f7e5a9d..e28af388f64 100644 --- a/pkg/kubectl/cmd/util/cached_discovery.go +++ b/pkg/kubectl/cmd/util/cached_discovery.go @@ -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() diff --git a/pkg/kubectl/cmd/util/cached_discovery_test.go b/pkg/kubectl/cmd/util/cached_discovery_test.go index 190e4efaa95..8b32c697f94 100644 --- a/pkg/kubectl/cmd/util/cached_discovery_test.go +++ b/pkg/kubectl/cmd/util/cached_discovery_test.go @@ -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 +} diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 1b8f0fe2a41..975896b6c91 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -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) { diff --git a/pkg/kubectl/cmd/util/factory_object_mapping.go b/pkg/kubectl/cmd/util/factory_object_mapping.go index 7ded04dc690..e9cca1796f0 100644 --- a/pkg/kubectl/cmd/util/factory_object_mapping.go +++ b/pkg/kubectl/cmd/util/factory_object_mapping.go @@ -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() +} diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index bda811d48ba..fc7e2b5f1e9 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -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.") diff --git a/pkg/kubectl/cmd/util/openapi/BUILD b/pkg/kubectl/cmd/util/openapi/BUILD new file mode 100644 index 00000000000..cb01f0de6f0 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/BUILD @@ -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"], +) diff --git a/pkg/kubectl/cmd/util/openapi/doc.go b/pkg/kubectl/cmd/util/openapi/doc.go new file mode 100644 index 00000000000..56b393a1eb2 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/doc.go @@ -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 diff --git a/pkg/kubectl/cmd/util/openapi/openapi.go b/pkg/kubectl/cmd/util/openapi/openapi.go new file mode 100644 index 00000000000..479672e82f3 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/openapi.go @@ -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 +} diff --git a/pkg/kubectl/cmd/util/openapi/openapi_cache.go b/pkg/kubectl/cmd/util/openapi/openapi_cache.go new file mode 100644 index 00000000000..71d9450d2cc --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/openapi_cache.go @@ -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{}) +} diff --git a/pkg/kubectl/cmd/util/openapi/openapi_cache_test.go b/pkg/kubectl/cmd/util/openapi/openapi_cache_test.go new file mode 100644 index 00000000000..d7a4349bc30 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/openapi_cache_test.go @@ -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 +} diff --git a/pkg/kubectl/cmd/util/openapi/openapi_getter.go b/pkg/kubectl/cmd/util/openapi/openapi_getter.go new file mode 100644 index 00000000000..0770bf31b78 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/openapi_getter.go @@ -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 +} diff --git a/pkg/kubectl/cmd/util/openapi/openapi_getter_test.go b/pkg/kubectl/cmd/util/openapi/openapi_getter_test.go new file mode 100644 index 00000000000..b9ac201ddcc --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/openapi_getter_test.go @@ -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)) + }) + }) +}) diff --git a/pkg/kubectl/cmd/util/openapi/openapi_suite_test.go b/pkg/kubectl/cmd/util/openapi/openapi_suite_test.go new file mode 100644 index 00000000000..9201d4e6fc6 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/openapi_suite_test.go @@ -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") +} diff --git a/pkg/kubectl/cmd/util/openapi/openapi_test.go b/pkg/kubectl/cmd/util/openapi/openapi_test.go new file mode 100644 index 00000000000..fd732c5eac8 --- /dev/null +++ b/pkg/kubectl/cmd/util/openapi/openapi_test.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiserver/Godeps/Godeps.json index 3be8ea220d7..acfcc3ed420 100644 --- a/staging/src/k8s.io/apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiserver/Godeps/Godeps.json @@ -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" diff --git a/staging/src/k8s.io/client-go/Godeps/Godeps.json b/staging/src/k8s.io/client-go/Godeps/Godeps.json index bddb4a765c7..e11fbad4f99 100644 --- a/staging/src/k8s.io/client-go/Godeps/Godeps.json +++ b/staging/src/k8s.io/client-go/Godeps/Godeps.json @@ -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" diff --git a/staging/src/k8s.io/client-go/discovery/BUILD b/staging/src/k8s.io/client-go/discovery/BUILD index 810b2b219ed..921ddaedd74 100644 --- a/staging/src/k8s.io/client-go/discovery/BUILD +++ b/staging/src/k8s.io/client-go/discovery/BUILD @@ -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", diff --git a/staging/src/k8s.io/client-go/discovery/discovery_client.go b/staging/src/k8s.io/client-go/discovery/discovery_client.go index ff4c57a4f0c..f1afb651594 100644 --- a/staging/src/k8s.io/client-go/discovery/discovery_client.go +++ b/staging/src/k8s.io/client-go/discovery/discovery_client.go @@ -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 diff --git a/staging/src/k8s.io/client-go/discovery/discovery_client_test.go b/staging/src/k8s.io/client-go/discovery/discovery_client_test.go index 22f01e07a16..f519ffaf4f2 100644 --- a/staging/src/k8s.io/client-go/discovery/discovery_client_test.go +++ b/staging/src/k8s.io/client-go/discovery/discovery_client_test.go @@ -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", diff --git a/staging/src/k8s.io/client-go/discovery/fake/BUILD b/staging/src/k8s.io/client-go/discovery/fake/BUILD index ec665e0e133..c8f646c0163 100644 --- a/staging/src/k8s.io/client-go/discovery/fake/BUILD +++ b/staging/src/k8s.io/client-go/discovery/fake/BUILD @@ -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", diff --git a/staging/src/k8s.io/client-go/discovery/fake/discovery.go b/staging/src/k8s.io/client-go/discovery/fake/discovery.go index 8fbed28d978..9a953efccf2 100644 --- a/staging/src/k8s.io/client-go/discovery/fake/discovery.go +++ b/staging/src/k8s.io/client-go/discovery/fake/discovery.go @@ -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 } diff --git a/staging/src/k8s.io/client-go/discovery/restmapper_test.go b/staging/src/k8s.io/client-go/discovery/restmapper_test.go index 523fdefdef5..917e677cb82 100644 --- a/staging/src/k8s.io/client-go/discovery/restmapper_test.go +++ b/staging/src/k8s.io/client-go/discovery/restmapper_test.go @@ -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 +} diff --git a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json index 686fd35dc39..2df498d8204 100644 --- a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json @@ -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"