diff --git a/pkg/api/meta/unstructured.go b/pkg/api/meta/unstructured.go new file mode 100644 index 00000000000..784cbf05c30 --- /dev/null +++ b/pkg/api/meta/unstructured.go @@ -0,0 +1,31 @@ +/* +Copyright 2016 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 meta + +import ( + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/runtime" +) + +// InterfacesForUnstructured returns VersionInterfaces suitable for +// dealing with runtime.Unstructured objects. +func InterfacesForUnstructured(unversioned.GroupVersion) (*VersionInterfaces, error) { + return &VersionInterfaces{ + ObjectConvertor: &runtime.UnstructuredObjectConverter{}, + MetadataAccessor: NewAccessor(), + }, nil +} diff --git a/pkg/client/typed/discovery/unstructured.go b/pkg/client/typed/discovery/unstructured.go new file mode 100644 index 00000000000..afa74e7dee2 --- /dev/null +++ b/pkg/client/typed/discovery/unstructured.go @@ -0,0 +1,95 @@ +/* +Copyright 2016 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 discovery + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/runtime" +) + +// UnstructuredObjectTyper provides a runtime.ObjectTyper implmentation for +// runtime.Unstructured object based on discovery information. +type UnstructuredObjectTyper struct { + registered map[unversioned.GroupVersionKind]bool +} + +// NewUnstructuredObjectTyper returns a runtime.ObjectTyper for +// unstructred objects based on discovery information. +func NewUnstructuredObjectTyper(groupResources []*APIGroupResources) *UnstructuredObjectTyper { + dot := &UnstructuredObjectTyper{registered: make(map[unversioned.GroupVersionKind]bool)} + for _, group := range groupResources { + for _, discoveryVersion := range group.Group.Versions { + resources, ok := group.VersionedResources[discoveryVersion.Version] + if !ok { + continue + } + + gv := unversioned.GroupVersion{Group: group.Group.Name, Version: discoveryVersion.Version} + for _, resource := range resources { + dot.registered[gv.WithKind(resource.Kind)] = true + } + } + } + return dot +} + +// ObjectKind returns the group,version,kind of the provided object, or an error +// if the object in not *runtime.Unstructured or has no group,version,kind +// information. +func (d *UnstructuredObjectTyper) ObjectKind(obj runtime.Object) (unversioned.GroupVersionKind, error) { + if _, ok := obj.(*runtime.Unstructured); !ok { + return unversioned.GroupVersionKind{}, fmt.Errorf("type %T is invalid for dynamic object typer", obj) + } + + return obj.GetObjectKind().GroupVersionKind(), nil +} + +// ObjectKinds returns a slice of one element with the group,version,kind of the +// provided object, or an error if the object is not *runtime.Unstructured or +// has no group,version,kind information. unversionedType will always be false +// because runtime.Unstructured object should always have group,version,kind +// information set. +func (d *UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) (gvks []unversioned.GroupVersionKind, unversionedType bool, err error) { + gvk, err := d.ObjectKind(obj) + if err != nil { + return nil, false, err + } + + return []unversioned.GroupVersionKind{gvk}, false, nil +} + +// Recognizes returns true if the provided group,version,kind was in the +// discovery information. +func (d *UnstructuredObjectTyper) Recognizes(gvk unversioned.GroupVersionKind) bool { + return d.registered[gvk] +} + +// IsUnversioned returns false always because *runtime.Unstructured objects +// should always have group,version,kind information set. ok will be true if the +// object's group,version,kind is registered. +func (d *UnstructuredObjectTyper) IsUnversioned(obj runtime.Object) (unversioned bool, ok bool) { + gvk, err := d.ObjectKind(obj) + if err != nil { + return false, false + } + + return false, d.registered[gvk] +} + +var _ runtime.ObjectTyper = &UnstructuredObjectTyper{} diff --git a/pkg/kubectl/cmd/cmd_test.go b/pkg/kubectl/cmd/cmd_test.go index 05b8476eb3a..f8af22f68eb 100644 --- a/pkg/kubectl/cmd/cmd_test.go +++ b/pkg/kubectl/cmd/cmd_test.go @@ -34,6 +34,7 @@ import ( "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/client/typed/discovery" client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/client/unversioned/fake" "k8s.io/kubernetes/pkg/kubectl" @@ -275,6 +276,13 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec, runtime.Neg Object: func(discovery bool) (meta.RESTMapper, runtime.ObjectTyper) { return testapi.Default.RESTMapper(), api.Scheme }, + UnstructuredObject: func() (meta.RESTMapper, runtime.ObjectTyper, error) { + groupResources := testDynamicResources() + mapper := discovery.NewRESTMapper(groupResources, meta.InterfacesForUnstructured) + typer := discovery.NewUnstructuredObjectTyper(groupResources) + + return kubectl.ShortcutExpander{RESTMapper: mapper}, typer, nil + }, Client: func() (*client.Client, error) { // Swap out the HTTP client out of the client with the fake's version. fakeClient := t.Client.(*fake.RESTClient) @@ -286,6 +294,9 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec, runtime.Neg ClientForMapping: func(*meta.RESTMapping) (resource.RESTClient, error) { return t.Client, t.Err }, + UnstructuredClientForMapping: func(*meta.RESTMapping) (resource.RESTClient, error) { + return t.Client, t.Err + }, Decoder: func(bool) runtime.Decoder { return testapi.Default.Codec() }, diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index d61ed1004b9..d2518491edc 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -53,6 +53,8 @@ import ( "k8s.io/kubernetes/pkg/apis/policy" "k8s.io/kubernetes/pkg/apis/rbac" "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/client/typed/discovery" + "k8s.io/kubernetes/pkg/client/typed/dynamic" client "k8s.io/kubernetes/pkg/client/unversioned" clientset "k8s.io/kubernetes/pkg/client/unversioned/adapters/internalclientset" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" @@ -83,6 +85,9 @@ type Factory struct { // Returns interfaces for dealing with arbitrary runtime.Objects. If thirdPartyDiscovery is true, performs API calls // to discovery dynamic API objects registered by third parties. Object func(thirdPartyDiscovery bool) (meta.RESTMapper, runtime.ObjectTyper) + // Returns interfaces for dealing with arbitrary + // runtime.Unstructured. This performs API calls to discover types. + UnstructuredObject func() (meta.RESTMapper, runtime.ObjectTyper, error) // Returns interfaces for decoding objects - if toInternal is set, decoded objects will be converted // into their internal form (if possible). Eventually the internal form will be removed as an option, // and only versioned objects will be returned. @@ -96,6 +101,8 @@ type Factory struct { // Returns a RESTClient for working with the specified RESTMapping or an error. This is intended // for working with arbitrary resources and is not guaranteed to point to a Kubernetes APIServer. ClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) + // Returns a RESTClient for working with Unstructured objects. + UnstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) // Returns a Describer for displaying the specified RESTMapping type or an error. Describer func(mapping *meta.RESTMapping) (kubectl.Describer, error) // Returns a Printer for formatting objects of the given type or an error. @@ -360,6 +367,40 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } return priorityRESTMapper, api.Scheme }, + UnstructuredObject: func() (meta.RESTMapper, runtime.ObjectTyper, error) { + cfg, err := clients.ClientConfigForVersion(nil) + if err != nil { + return nil, nil, err + } + + dc, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return nil, nil, err + } + + groupResources, err := discovery.GetAPIGroupResources(dc) + if err != nil { + return nil, nil, err + } + + // Register unknown APIs as third party for now to make + // validation happy. TODO perhaps make a dynamic schema + // validator to avoid this. + for _, group := range groupResources { + for _, version := range group.Group.Versions { + gv := unversioned.GroupVersion{Group: group.Group.Name, Version: version.Version} + if !registered.IsRegisteredVersion(gv) { + registered.AddThirdPartyAPIGroupVersions(gv) + } + } + } + + mapper := discovery.NewRESTMapper(groupResources, meta.InterfacesForUnstructured) + + typer := discovery.NewUnstructuredObjectTyper(groupResources) + + return kubectl.ShortcutExpander{RESTMapper: mapper}, typer, nil + }, Client: func() (*client.Client, error) { return clients.ClientForVersion(nil) }, @@ -391,6 +432,23 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { } return restclient.RESTClientFor(cfg) }, + UnstructuredClientForMapping: func(mapping *meta.RESTMapping) (resource.RESTClient, error) { + cfg, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + if err := restclient.SetKubernetesDefaults(cfg); err != nil { + return nil, err + } + cfg.APIPath = "/apis" + if mapping.GroupVersionKind.Group == api.GroupName { + cfg.APIPath = "/api" + } + gv := mapping.GroupVersionKind.GroupVersion() + cfg.ContentConfig = dynamic.ContentConfig() + cfg.GroupVersion = &gv + return restclient.RESTClientFor(cfg) + }, Describer: func(mapping *meta.RESTMapping) (kubectl.Describer, error) { mappingVersion := mapping.GroupVersionKind.GroupVersion() if mapping.GroupVersionKind.Group == federation.GroupName {