From 01807c1fac15560836fcaf615c75b102fffed930 Mon Sep 17 00:00:00 2001 From: Muhammed Uluyol Date: Mon, 10 Aug 2015 13:05:57 -0700 Subject: [PATCH 1/3] Make ClientCache public --- pkg/kubectl/cmd/util/clientcache.go | 12 ++++++------ pkg/kubectl/cmd/util/factory.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/kubectl/cmd/util/clientcache.go b/pkg/kubectl/cmd/util/clientcache.go index 2d6f2c4abf0..30598163a6c 100644 --- a/pkg/kubectl/cmd/util/clientcache.go +++ b/pkg/kubectl/cmd/util/clientcache.go @@ -22,17 +22,17 @@ import ( "k8s.io/kubernetes/pkg/client/clientcmd" ) -func NewClientCache(loader clientcmd.ClientConfig) *clientCache { - return &clientCache{ +func NewClientCache(loader clientcmd.ClientConfig) *ClientCache { + return &ClientCache{ clients: make(map[string]*client.Client), configs: make(map[string]*client.Config), loader: loader, } } -// clientCache caches previously loaded clients for reuse, and ensures MatchServerVersion +// ClientCache caches previously loaded clients for reuse, and ensures MatchServerVersion // is invoked only once -type clientCache struct { +type ClientCache struct { loader clientcmd.ClientConfig clients map[string]*client.Client configs map[string]*client.Config @@ -42,7 +42,7 @@ type clientCache struct { } // ClientConfigForVersion returns the correct config for a server -func (c *clientCache) ClientConfigForVersion(version string) (*client.Config, error) { +func (c *ClientCache) ClientConfigForVersion(version string) (*client.Config, error) { if c.defaultConfig == nil { config, err := c.loader.ClientConfig() if err != nil { @@ -73,7 +73,7 @@ func (c *clientCache) ClientConfigForVersion(version string) (*client.Config, er // ClientForVersion initializes or reuses a client for the specified version, or returns an // error if that is not possible -func (c *clientCache) ClientForVersion(version string) (*client.Client, error) { +func (c *ClientCache) ClientForVersion(version string) (*client.Client, error) { if client, ok := c.clients[version]; ok { return client, nil } diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index d68fb57f49a..9299c9b65dc 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -49,7 +49,7 @@ const ( // TODO: pass the various interfaces on the factory directly into the command constructors (so the // commands are decoupled from the factory). type Factory struct { - clients *clientCache + clients *ClientCache flags *pflag.FlagSet generators map[string]kubectl.Generator From fab367230f0e2963b2b4ef4a35159134ce955f95 Mon Sep 17 00:00:00 2001 From: Muhammed Uluyol Date: Mon, 10 Aug 2015 13:08:34 -0700 Subject: [PATCH 2/3] Add experimental API support to kubectl --- pkg/api/latest/latest.go | 2 +- pkg/api/mapper.go | 9 +- pkg/api/meta/interfaces.go | 1 + pkg/api/meta/restmapper.go | 27 ++++- pkg/api/meta/restmapper_test.go | 37 ++++++- pkg/apiserver/apiserver_test.go | 2 +- pkg/expapi/latest/latest.go | 2 +- pkg/kubectl/cmd/apiversions.go | 25 ++++- pkg/kubectl/cmd/cmd_test.go | 2 +- pkg/kubectl/cmd/util/factory.go | 115 ++++++++++++++++----- pkg/kubectl/describe.go | 17 ++- pkg/kubectl/stop.go | 2 +- pkg/kubectl/stop_test.go | 2 +- pkg/kubectl/version.go | 10 -- test/e2e/rc.go | 2 +- test/integration/framework/master_utils.go | 2 +- 16 files changed, 194 insertions(+), 63 deletions(-) diff --git a/pkg/api/latest/latest.go b/pkg/api/latest/latest.go index 81428fec3f8..bc2f93b5652 100644 --- a/pkg/api/latest/latest.go +++ b/pkg/api/latest/latest.go @@ -97,7 +97,7 @@ func init() { "PodProxyOptions", "Daemon") - mapper := api.NewDefaultRESTMapper(versions, InterfacesFor, importPrefix, ignoredKinds, rootScoped) + mapper := api.NewDefaultRESTMapper("api", versions, InterfacesFor, importPrefix, ignoredKinds, rootScoped) // setup aliases for groups of resources mapper.AddResourceAlias("all", userResources...) RESTMapper = mapper diff --git a/pkg/api/mapper.go b/pkg/api/mapper.go index 7e4fad1b597..dba16d36af9 100644 --- a/pkg/api/mapper.go +++ b/pkg/api/mapper.go @@ -33,11 +33,12 @@ func RegisterRESTMapper(m meta.RESTMapper) { RESTMapper = append(RESTMapper.(meta.MultiRESTMapper), m) } -func NewDefaultRESTMapper(versions []string, interfacesFunc meta.VersionInterfacesFunc, importPathPrefix string, - ignoredKinds, rootScoped util.StringSet) *meta.DefaultRESTMapper { +func NewDefaultRESTMapper(group string, versions []string, interfacesFunc meta.VersionInterfacesFunc, + importPathPrefix string, ignoredKinds, rootScoped util.StringSet) *meta.DefaultRESTMapper { - mapper := meta.NewDefaultRESTMapper(versions, interfacesFunc) - // enumerate all supported versions, get the kinds, and register with the mapper how to address our resources. + mapper := meta.NewDefaultRESTMapper(group, versions, interfacesFunc) + // enumerate all supported versions, get the kinds, and register with the mapper how to address + // our resources. for _, version := range versions { for kind, oType := range Scheme.KnownTypes(version) { // TODO: Remove import path prefix check. diff --git a/pkg/api/meta/interfaces.go b/pkg/api/meta/interfaces.go index 888fc73a96d..eaaec6d9e8d 100644 --- a/pkg/api/meta/interfaces.go +++ b/pkg/api/meta/interfaces.go @@ -144,6 +144,7 @@ type RESTMapping struct { // consumers of Kubernetes compatible REST APIs as defined in docs/api-conventions.md. type RESTMapper interface { VersionAndKindForResource(resource string) (defaultVersion, kind string, err error) + GroupForResource(resource string) (string, error) RESTMapping(kind string, versions ...string) (*RESTMapping, error) AliasesForResource(resource string) ([]string, bool) ResourceSingularizer(resource string) (singular string, err error) diff --git a/pkg/api/meta/restmapper.go b/pkg/api/meta/restmapper.go index 05f62be969d..fdc9c9376ad 100644 --- a/pkg/api/meta/restmapper.go +++ b/pkg/api/meta/restmapper.go @@ -76,6 +76,7 @@ type DefaultRESTMapper struct { mapping map[string]typeMeta reverse map[typeMeta]string scopes map[typeMeta]RESTScope + group string versions []string plurals map[string]string singulars map[string]string @@ -88,10 +89,10 @@ type VersionInterfacesFunc func(apiVersion string) (*VersionInterfaces, error) // NewDefaultRESTMapper initializes a mapping between Kind and APIVersion // to a resource name and back based on the objects in a runtime.Scheme -// and the Kubernetes API conventions. Takes a priority list of the versions to -// search when an object has no default version (set empty to return an error) +// and the Kubernetes API conventions. Takes a group name, a priority list of the versions +// to search when an object has no default version (set empty to return an error), // and a function that retrieves the correct codec and metadata for a given version. -func NewDefaultRESTMapper(versions []string, f VersionInterfacesFunc) *DefaultRESTMapper { +func NewDefaultRESTMapper(group string, versions []string, f VersionInterfacesFunc) *DefaultRESTMapper { mapping := make(map[string]typeMeta) reverse := make(map[typeMeta]string) scopes := make(map[typeMeta]RESTScope) @@ -103,6 +104,7 @@ func NewDefaultRESTMapper(versions []string, f VersionInterfacesFunc) *DefaultRE mapping: mapping, reverse: reverse, scopes: scopes, + group: group, versions: versions, plurals: plurals, singulars: singulars, @@ -174,6 +176,13 @@ func (m *DefaultRESTMapper) VersionAndKindForResource(resource string) (defaultV return meta.APIVersion, meta.Kind, nil } +func (m *DefaultRESTMapper) GroupForResource(resource string) (string, error) { + if _, ok := m.mapping[strings.ToLower(resource)]; !ok { + return "", fmt.Errorf("no resource %q has been defined", resource) + } + return m.group, nil +} + // RESTMapping returns a struct representing the resource path and conversion interfaces a // RESTClient should use to operate on the provided kind in order of versions. If a version search // order is not provided, the search order provided to DefaultRESTMapper will be used to resolve which @@ -292,6 +301,18 @@ func (m MultiRESTMapper) VersionAndKindForResource(resource string) (defaultVers return } +// GroupForResource provides the Group mappings for the REST resources. This +// implementation supports multiple REST schemas and returns the first match. +func (m MultiRESTMapper) GroupForResource(resource string) (group string, err error) { + for _, t := range m { + group, err = t.GroupForResource(resource) + if err == nil { + return + } + } + return +} + // RESTMapping provides the REST mapping for the resource based on the resource // kind and version. This implementation supports multiple REST schemas and // return the first match. diff --git a/pkg/api/meta/restmapper_test.go b/pkg/api/meta/restmapper_test.go index ed84849bc29..83d6284bcf6 100644 --- a/pkg/api/meta/restmapper_test.go +++ b/pkg/api/meta/restmapper_test.go @@ -93,7 +93,7 @@ func TestRESTMapperVersionAndKindForResource(t *testing.T) { {Resource: "internalObjects", MixedCase: true, Kind: "InternalObject", APIVersion: "test"}, } for i, testCase := range testCases { - mapper := NewDefaultRESTMapper([]string{"test"}, fakeInterfaces) + mapper := NewDefaultRESTMapper("tgroup", []string{"test"}, fakeInterfaces) mapper.Add(RESTScopeNamespace, testCase.Kind, testCase.APIVersion, testCase.MixedCase) v, k, err := mapper.VersionAndKindForResource(testCase.Resource) hasErr := err != nil @@ -107,6 +107,33 @@ func TestRESTMapperVersionAndKindForResource(t *testing.T) { } } +func TestRESTMapperGroupForResource(t *testing.T) { + testCases := []struct { + Resource string + Kind, APIVersion, Group string + Err bool + }{ + {Resource: "myObject", Kind: "MyObject", APIVersion: "test", Group: "testapi"}, + {Resource: "myobject", Kind: "MyObject", APIVersion: "test", Group: "testapi2"}, + {Resource: "myObje", Err: true, Kind: "MyObject", APIVersion: "test", Group: "testapi"}, + {Resource: "myobje", Err: true, Kind: "MyObject", APIVersion: "test", Group: "testapi"}, + } + for i, testCase := range testCases { + mapper := NewDefaultRESTMapper(testCase.Group, []string{"test"}, fakeInterfaces) + mapper.Add(RESTScopeNamespace, testCase.Kind, testCase.APIVersion, false) + g, err := mapper.GroupForResource(testCase.Resource) + if testCase.Err { + if err == nil { + t.Errorf("%d: expected error", i) + } + } else if err != nil { + t.Errorf("%d: unexpected error: %v", i, err) + } else if g != testCase.Group { + t.Errorf("%d: expected group %q, got %q", i, testCase.Group, g) + } + } +} + func TestKindToResource(t *testing.T) { testCases := []struct { Kind string @@ -159,7 +186,7 @@ func TestRESTMapperResourceSingularizer(t *testing.T) { {Kind: "lowercases", APIVersion: "test", MixedCase: false, Plural: "lowercases", Singular: "lowercases"}, } for i, testCase := range testCases { - mapper := NewDefaultRESTMapper([]string{"test"}, fakeInterfaces) + mapper := NewDefaultRESTMapper("tgroup", []string{"test"}, fakeInterfaces) // create singular/plural mapping mapper.Add(RESTScopeNamespace, testCase.Kind, testCase.APIVersion, testCase.MixedCase) singular, _ := mapper.ResourceSingularizer(testCase.Plural) @@ -198,7 +225,7 @@ func TestRESTMapperRESTMapping(t *testing.T) { // TODO: add test for a resource that exists in one version but not another } for i, testCase := range testCases { - mapper := NewDefaultRESTMapper(testCase.DefaultVersions, fakeInterfaces) + mapper := NewDefaultRESTMapper("tgroup", testCase.DefaultVersions, fakeInterfaces) mapper.Add(RESTScopeNamespace, "InternalObject", "test", testCase.MixedCase) mapping, err := mapper.RESTMapping(testCase.Kind, testCase.APIVersions...) hasErr := err != nil @@ -225,7 +252,7 @@ func TestRESTMapperRESTMapping(t *testing.T) { } func TestRESTMapperRESTMappingSelectsVersion(t *testing.T) { - mapper := NewDefaultRESTMapper([]string{"test1", "test2"}, fakeInterfaces) + mapper := NewDefaultRESTMapper("tgroup", []string{"test1", "test2"}, fakeInterfaces) mapper.Add(RESTScopeNamespace, "InternalObject", "test1", false) mapper.Add(RESTScopeNamespace, "OtherObject", "test2", false) @@ -278,7 +305,7 @@ func TestRESTMapperRESTMappingSelectsVersion(t *testing.T) { } func TestRESTMapperReportsErrorOnBadVersion(t *testing.T) { - mapper := NewDefaultRESTMapper([]string{"test1", "test2"}, unmatchedVersionInterfaces) + mapper := NewDefaultRESTMapper("tgroup", []string{"test1", "test2"}, unmatchedVersionInterfaces) mapper.Add(RESTScopeNamespace, "InternalObject", "test1", false) _, err := mapper.RESTMapping("InternalObject", "test1") if err == nil { diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 516406eacec..ab2ccd484ee 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -88,7 +88,7 @@ func interfacesFor(version string) (*meta.VersionInterfaces, error) { } func newMapper() *meta.DefaultRESTMapper { - return meta.NewDefaultRESTMapper(versions, interfacesFor) + return meta.NewDefaultRESTMapper("testgroup", versions, interfacesFor) } func addTestTypes() { diff --git a/pkg/expapi/latest/latest.go b/pkg/expapi/latest/latest.go index 4470465e84d..11b4ff2536c 100644 --- a/pkg/expapi/latest/latest.go +++ b/pkg/expapi/latest/latest.go @@ -55,7 +55,7 @@ func init() { ignoredKinds := util.NewStringSet() - RESTMapper = api.NewDefaultRESTMapper(Versions, InterfacesFor, importPrefix, ignoredKinds, rootScoped) + RESTMapper = api.NewDefaultRESTMapper("experimental", Versions, InterfacesFor, importPrefix, ignoredKinds, rootScoped) api.RegisterRESTMapper(RESTMapper) } diff --git a/pkg/kubectl/cmd/apiversions.go b/pkg/kubectl/cmd/apiversions.go index ba21309e022..032c3b597bb 100644 --- a/pkg/kubectl/cmd/apiversions.go +++ b/pkg/kubectl/cmd/apiversions.go @@ -17,12 +17,13 @@ limitations under the License. package cmd import ( + "fmt" "io" "os" "github.com/spf13/cobra" - "k8s.io/kubernetes/pkg/kubectl" + "k8s.io/kubernetes/pkg/api" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) @@ -40,7 +41,7 @@ func NewCmdApiVersions(f *cmdutil.Factory, out io.Writer) *cobra.Command { return cmd } -func RunApiVersions(f *cmdutil.Factory, out io.Writer) error { +func RunApiVersions(f *cmdutil.Factory, w io.Writer) error { if len(os.Args) > 1 && os.Args[1] == "apiversions" { printDeprecationWarning("api-versions", "apiversions") } @@ -50,6 +51,24 @@ func RunApiVersions(f *cmdutil.Factory, out io.Writer) error { return err } - kubectl.GetApiVersions(out, client) + apiVersions, err := client.ServerAPIVersions() + if err != nil { + fmt.Printf("Couldn't get available api versions from server: %v\n", err) + os.Exit(1) + } + + var expAPIVersions *api.APIVersions + showExpVersions := false + expClient, err := f.ExperimentalClient() + if err == nil { + expAPIVersions, err = expClient.ServerAPIVersions() + showExpVersions = err == nil + } + + fmt.Fprintf(w, "Available Server Api Versions: %#v\n", *apiVersions) + if showExpVersions { + fmt.Fprintf(w, "Available Server Experimental Api Versions: %#v\n", *expAPIVersions) + } + return nil } diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index bc06438b740..2357cde51a2 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -83,7 +83,7 @@ func newExternalScheme() (*runtime.Scheme, meta.RESTMapper, runtime.Codec) { codec := runtime.CodecFor(scheme, "unlikelyversion") validVersion := testapi.Version() - mapper := meta.NewDefaultRESTMapper([]string{"unlikelyversion", validVersion}, func(version string) (*meta.VersionInterfaces, error) { + mapper := meta.NewDefaultRESTMapper("apitest", []string{"unlikelyversion", validVersion}, func(version string) (*meta.VersionInterfaces, error) { return &meta.VersionInterfaces{ Codec: runtime.CodecFor(scheme, version), ObjectConvertor: scheme, diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 9299c9b65dc..2a1cff79a73 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -17,6 +17,7 @@ limitations under the License. package util import ( + "errors" "flag" "fmt" "io" @@ -27,7 +28,6 @@ import ( "github.com/spf13/pflag" "k8s.io/kubernetes/pkg/api" - "k8s.io/kubernetes/pkg/api/latest" "k8s.io/kubernetes/pkg/api/meta" "k8s.io/kubernetes/pkg/api/registered" "k8s.io/kubernetes/pkg/api/validation" @@ -57,6 +57,8 @@ type Factory struct { Object func() (meta.RESTMapper, runtime.ObjectTyper) // Returns a client for accessing Kubernetes resources or an error. Client func() (*client.Client, error) + // Returns a client for accessing experimental Kubernetes resources or an error. + ExperimentalClient func() (*client.ExperimentalClient, error) // Returns a client.Config for accessing the Kubernetes server. ClientConfig func() (*client.Config, error) // Returns a RESTClient for working with the specified RESTMapping or an error. This is intended @@ -90,7 +92,7 @@ type Factory struct { // if optionalClientConfig is nil, then flags will be bound to a new clientcmd.ClientConfig. // if optionalClientConfig is not nil, then this factory will make use of it. func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { - mapper := kubectl.ShortcutExpander{RESTMapper: latest.RESTMapper} + mapper := kubectl.ShortcutExpander{RESTMapper: api.RESTMapper} flags := pflag.NewFlagSet("", pflag.ContinueOnError) flags.SetNormalizeFunc(util.WarnWordSepNormalizeFunc) // Warn for "_" flags @@ -109,6 +111,25 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { clients := NewClientCache(clientConfig) + // Initialize the experimental client (if possible). Failing here is non-fatal, errors + // will be returned when an experimental client is explicitly requested. + var experimentalClient *client.ExperimentalClient + cfg, experimentalClientErr := clientConfig.ClientConfig() + if experimentalClientErr == nil { + experimentalClient, experimentalClientErr = client.NewExperimental(cfg) + } + + noClientErr := errors.New("could not get client") + getBothClients := func(group string, version string) (client *client.Client, expClient *client.ExperimentalClient, err error) { + err = noClientErr + switch group { + case "api": + client, err = clients.ClientForVersion(version) + case "experimental": + expClient, err = experimentalClient, experimentalClientErr + } + return + } return &Factory{ clients: clients, flags: flags, @@ -124,26 +145,46 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { Client: func() (*client.Client, error) { return clients.ClientForVersion("") }, + ExperimentalClient: func() (*client.ExperimentalClient, error) { + return experimentalClient, experimentalClientErr + }, ClientConfig: func() (*client.Config, error) { return clients.ClientConfigForVersion("") }, RESTClient: func(mapping *meta.RESTMapping) (resource.RESTClient, error) { - client, err := clients.ClientForVersion(mapping.APIVersion) + group, err := api.RESTMapper.GroupForResource(mapping.Resource) if err != nil { return nil, err } - return client.RESTClient, nil + switch group { + case "api": + client, err := clients.ClientForVersion(mapping.APIVersion) + if err != nil { + return nil, err + } + return client.RESTClient, nil + case "experimental": + client, err := experimentalClient, experimentalClientErr + if err != nil { + return nil, err + } + return client.RESTClient, nil + } + return nil, fmt.Errorf("unable to get RESTClient for resource '%s'", mapping.Resource) }, Describer: func(mapping *meta.RESTMapping) (kubectl.Describer, error) { - client, err := clients.ClientForVersion(mapping.APIVersion) + group, err := api.RESTMapper.GroupForResource(mapping.Resource) if err != nil { return nil, err } - describer, ok := kubectl.DescriberFor(mapping.Kind, client) - if !ok { - return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind) + client, expClient, err := getBothClients(group, mapping.APIVersion) + if err != nil { + return nil, err } - return describer, nil + if describer, ok := kubectl.DescriberFor(mapping.Kind, client, expClient); ok { + return describer, nil + } + return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind) }, Printer: func(mapping *meta.RESTMapping, noHeaders, withNamespace bool, wide bool, columnLabels []string) (kubectl.ResourcePrinter, error) { return kubectl.NewHumanReadablePrinter(noHeaders, withNamespace, wide, columnLabels), nil @@ -192,18 +233,26 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { return meta.NewAccessor().Labels(object) }, Scaler: func(mapping *meta.RESTMapping) (kubectl.Scaler, error) { - client, err := clients.ClientForVersion(mapping.APIVersion) + group, err := api.RESTMapper.GroupForResource(mapping.Resource) + if err != nil { + return nil, err + } + client, _, err := getBothClients(group, mapping.APIVersion) if err != nil { return nil, err } return kubectl.ScalerFor(mapping.Kind, kubectl.NewScalerClient(client)) }, Reaper: func(mapping *meta.RESTMapping) (kubectl.Reaper, error) { - client, err := clients.ClientForVersion(mapping.APIVersion) + group, err := api.RESTMapper.GroupForResource(mapping.Resource) if err != nil { return nil, err } - return kubectl.ReaperFor(mapping.Kind, client) + client, expClient, err := getBothClients(group, mapping.APIVersion) + if err != nil { + return nil, err + } + return kubectl.ReaperFor(mapping.Kind, client, expClient) }, Validator: func() (validation.Schema, error) { if flags.Lookup("validate").Value.String() == "true" { @@ -211,7 +260,7 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { if err != nil { return nil, err } - return &clientSwaggerSchema{client, api.Scheme}, nil + return &clientSwaggerSchema{client, experimentalClient, api.Scheme}, nil } return validation.NullSchema{}, nil }, @@ -274,20 +323,14 @@ func getServicePorts(spec api.ServiceSpec) []string { } type clientSwaggerSchema struct { - c *client.Client - t runtime.ObjectTyper + c *client.Client + ec *client.ExperimentalClient + t runtime.ObjectTyper } -func (c *clientSwaggerSchema) ValidateBytes(data []byte) error { - version, _, err := runtime.UnstructuredJSONScheme.DataVersionAndKind(data) - if err != nil { - return err - } - if ok := registered.IsRegisteredAPIVersion(version); !ok { - return fmt.Errorf("API version %q isn't supported, only supports API versions %q", version, registered.RegisteredVersions) - } - schemaData, err := c.c.RESTClient.Get(). - AbsPath("/swaggerapi/api", version). +func getSchemaAndValidate(c *client.RESTClient, data []byte, group, version string) error { + schemaData, err := c.Get(). + AbsPath("/swaggerapi", group, version). Do(). Raw() if err != nil { @@ -300,6 +343,28 @@ func (c *clientSwaggerSchema) ValidateBytes(data []byte) error { return schema.ValidateBytes(data) } +func (c *clientSwaggerSchema) ValidateBytes(data []byte) error { + version, _, err := runtime.UnstructuredJSONScheme.DataVersionAndKind(data) + if err != nil { + return err + } + if ok := registered.IsRegisteredAPIVersion(version); !ok { + return fmt.Errorf("API version %q isn't supported, only supports API versions %q", version, registered.RegisteredVersions) + } + // First try stable api, if we can't validate using that, try experimental. + // If experimental fails, return error from stable api. + // TODO: Figure out which group to try once multiple group support is merged + // instead of trying everything. + err = getSchemaAndValidate(c.c.RESTClient, data, "api", version) + if err != nil && c.ec != nil { + errExp := getSchemaAndValidate(c.ec.RESTClient, data, "experimental", version) + if errExp == nil { + return nil + } + } + return err +} + // DefaultClientConfig creates a clientcmd.ClientConfig with the following hierarchy: // 1. Use the kubeconfig builder. The number of merges and overrides here gets a little crazy. Stay with me. // 1. Merge together the kubeconfig itself. This is done with the following hierarchy rules: diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 3e0145e3bc3..4733d201cf2 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -81,6 +81,10 @@ func describerMap(c *client.Client) map[string]Describer { return m } +func expDescriberMap(c *client.ExperimentalClient) map[string]Describer { + return map[string]Describer{} +} + // List of all resource types we can describe func DescribableResources() []string { keys := make([]string, 0) @@ -94,12 +98,15 @@ func DescribableResources() []string { // Describer returns the default describe functions for each of the standard // Kubernetes types. -func DescriberFor(kind string, c *client.Client) (Describer, bool) { - f, ok := describerMap(c)[kind] - if ok { - return f, true +func DescriberFor(kind string, c *client.Client, ec *client.ExperimentalClient) (Describer, bool) { + var f Describer + var ok bool + if c != nil { + f, ok = describerMap(c)[kind] + } else if ec != nil { + f, ok = expDescriberMap(ec)[kind] } - return nil, false + return f, ok } // DefaultObjectDescriber can describe the default Kubernetes objects. diff --git a/pkg/kubectl/stop.go b/pkg/kubectl/stop.go index 7d407debe5f..b2d9cf17bbf 100644 --- a/pkg/kubectl/stop.go +++ b/pkg/kubectl/stop.go @@ -52,7 +52,7 @@ func IsNoSuchReaperError(err error) bool { return ok } -func ReaperFor(kind string, c client.Interface) (Reaper, error) { +func ReaperFor(kind string, c client.Interface, ec *client.ExperimentalClient) (Reaper, error) { switch kind { case "ReplicationController": return &ReplicationControllerReaper{c, Interval, Timeout}, nil diff --git a/pkg/kubectl/stop_test.go b/pkg/kubectl/stop_test.go index 42ed6d520a2..c096ac550c8 100644 --- a/pkg/kubectl/stop_test.go +++ b/pkg/kubectl/stop_test.go @@ -156,7 +156,7 @@ func TestSimpleStop(t *testing.T) { } for _, test := range tests { fake := test.fake - reaper, err := ReaperFor(test.kind, fake) + reaper, err := ReaperFor(test.kind, fake, nil) if err != nil { t.Errorf("unexpected error: %v (%s)", err, test.test) } diff --git a/pkg/kubectl/version.go b/pkg/kubectl/version.go index 0f2390f4069..2f53a1976ed 100644 --- a/pkg/kubectl/version.go +++ b/pkg/kubectl/version.go @@ -40,13 +40,3 @@ func GetVersion(w io.Writer, kubeClient client.Interface) { func GetClientVersion(w io.Writer) { fmt.Fprintf(w, "Client Version: %#v\n", version.Get()) } - -func GetApiVersions(w io.Writer, kubeClient client.Interface) { - apiVersions, err := kubeClient.ServerAPIVersions() - if err != nil { - fmt.Printf("Couldn't get available api versions from server: %v\n", err) - os.Exit(1) - } - - fmt.Fprintf(w, "Available Server Api Versions: %#v\n", *apiVersions) -} diff --git a/test/e2e/rc.go b/test/e2e/rc.go index a7480da5c2b..e17c7de3da3 100644 --- a/test/e2e/rc.go +++ b/test/e2e/rc.go @@ -89,7 +89,7 @@ func ServeImageOrFail(f *Framework, test string, image string) { defer func() { // Resize the replication controller to zero to get rid of pods. By("Cleaning up the replication controller") - rcReaper, err := kubectl.ReaperFor("ReplicationController", f.Client) + rcReaper, err := kubectl.ReaperFor("ReplicationController", f.Client, nil) if err != nil { Logf("Failed to cleanup replication controller %v: %v.", controller.Name, err) } diff --git a/test/integration/framework/master_utils.go b/test/integration/framework/master_utils.go index d4d11951c44..9e50f12801e 100644 --- a/test/integration/framework/master_utils.go +++ b/test/integration/framework/master_utils.go @@ -190,7 +190,7 @@ func RCFromManifest(fileName string) *api.ReplicationController { // StopRC stops the rc via kubectl's stop library func StopRC(rc *api.ReplicationController, restClient *client.Client) error { - reaper, err := kubectl.ReaperFor("ReplicationController", restClient) + reaper, err := kubectl.ReaperFor("ReplicationController", restClient, nil) if err != nil || reaper == nil { return err } From 93b14c9a5d580ec3f6d3643c06fa3b6ac29e8df8 Mon Sep 17 00:00:00 2001 From: Muhammed Uluyol Date: Wed, 12 Aug 2015 11:29:05 -0700 Subject: [PATCH 3/3] Document the relationship between groups, versions, kinds, and resources. Link to appropriate issues for multiple api group support. --- pkg/api/meta/interfaces.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/api/meta/interfaces.go b/pkg/api/meta/interfaces.go index eaaec6d9e8d..6fd638f2ff0 100644 --- a/pkg/api/meta/interfaces.go +++ b/pkg/api/meta/interfaces.go @@ -142,8 +142,17 @@ type RESTMapping struct { // RESTMapper allows clients to map resources to kind, and map kind and version // to interfaces for manipulating those objects. It is primarily intended for // consumers of Kubernetes compatible REST APIs as defined in docs/api-conventions.md. +// +// The Kubernetes API provides versioned resources and object kinds which are scoped +// to API groups. In other words, kinds and resources should not be assumed to be +// unique across groups. +// +// TODO(caesarxuchao): Add proper multi-group support so that kinds & resources are +// scoped to groups. See http://issues.k8s.io/12413 and http://issues.k8s.io/10009. type RESTMapper interface { VersionAndKindForResource(resource string) (defaultVersion, kind string, err error) + // TODO(caesarxuchao): Remove GroupForResource when multi-group support is in (since + // group will be part of the version). GroupForResource(resource string) (string, error) RESTMapping(kind string, versions ...string) (*RESTMapping, error) AliasesForResource(resource string) ([]string, bool)