mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-01 15:58:37 +00:00
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:
commit
433aec11c8
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"],
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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.")
|
||||
|
69
pkg/kubectl/cmd/util/openapi/BUILD
Normal file
69
pkg/kubectl/cmd/util/openapi/BUILD
Normal 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"],
|
||||
)
|
21
pkg/kubectl/cmd/util/openapi/doc.go
Normal file
21
pkg/kubectl/cmd/util/openapi/doc.go
Normal 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
|
391
pkg/kubectl/cmd/util/openapi/openapi.go
Normal file
391
pkg/kubectl/cmd/util/openapi/openapi.go
Normal 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
|
||||
}
|
193
pkg/kubectl/cmd/util/openapi/openapi_cache.go
Normal file
193
pkg/kubectl/cmd/util/openapi/openapi_cache.go
Normal 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{})
|
||||
}
|
272
pkg/kubectl/cmd/util/openapi/openapi_cache_test.go
Normal file
272
pkg/kubectl/cmd/util/openapi/openapi_cache_test.go
Normal 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
|
||||
}
|
71
pkg/kubectl/cmd/util/openapi/openapi_getter.go
Normal file
71
pkg/kubectl/cmd/util/openapi/openapi_getter.go
Normal 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
|
||||
}
|
74
pkg/kubectl/cmd/util/openapi/openapi_getter_test.go
Normal file
74
pkg/kubectl/cmd/util/openapi/openapi_getter_test.go
Normal 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))
|
||||
})
|
||||
})
|
||||
})
|
29
pkg/kubectl/cmd/util/openapi/openapi_suite_test.go
Normal file
29
pkg/kubectl/cmd/util/openapi/openapi_suite_test.go
Normal 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")
|
||||
}
|
419
pkg/kubectl/cmd/util/openapi/openapi_test.go
Normal file
419
pkg/kubectl/cmd/util/openapi/openapi_test.go
Normal 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
|
||||
}
|
8
staging/src/k8s.io/apiserver/Godeps/Godeps.json
generated
8
staging/src/k8s.io/apiserver/Godeps/Godeps.json
generated
@ -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"
|
||||
|
8
staging/src/k8s.io/client-go/Godeps/Godeps.json
generated
8
staging/src/k8s.io/client-go/Godeps/Godeps.json
generated
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user