diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index bfb134b2c2b..4729141cb93 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -285,14 +285,14 @@ func (f *FakeFactory) Object() (meta.RESTMapper, runtime.ObjectTyper) { return legacyscheme.Registry.RESTMapper(), f.tf.Typer } -func (f *FakeFactory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper, error) { +func (f *FakeFactory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper) { groupResources := testDynamicResources() mapper := discovery.NewRESTMapper(groupResources, meta.InterfacesForUnstructuredConversion(legacyscheme.Registry.InterfacesFor)) typer := discovery.NewUnstructuredObjectTyper(groupResources) fakeDs := &fakeCachedDiscoveryClient{} - expander, err := cmdutil.NewShortcutExpander(mapper, fakeDs) - return expander, typer, err + expander := cmdutil.NewShortcutExpander(mapper, fakeDs) + return expander, typer } func (f *FakeFactory) CategoryExpander() categories.CategoryExpander { @@ -519,7 +519,7 @@ func (f *FakeFactory) PrinterForMapping(cmd *cobra.Command, isLocal bool, output func (f *FakeFactory) NewBuilder() *resource.Builder { mapper, typer := f.Object() - unstructuredMapper, unstructuredTyper, _ := f.UnstructuredObject() + unstructuredMapper, unstructuredTyper := f.UnstructuredObject() return resource.NewBuilder( &resource.Mapper{ @@ -608,7 +608,7 @@ func (f *fakeAPIFactory) Object() (meta.RESTMapper, runtime.ObjectTyper) { return testapi.Default.RESTMapper(), legacyscheme.Scheme } -func (f *fakeAPIFactory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper, error) { +func (f *fakeAPIFactory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper) { groupResources := testDynamicResources() mapper := discovery.NewRESTMapper( groupResources, @@ -629,8 +629,8 @@ func (f *fakeAPIFactory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTy typer := discovery.NewUnstructuredObjectTyper(groupResources) fakeDs := &fakeCachedDiscoveryClient{} - expander, err := cmdutil.NewShortcutExpander(mapper, fakeDs) - return expander, typer, err + expander := cmdutil.NewShortcutExpander(mapper, fakeDs) + return expander, typer } func (f *fakeAPIFactory) Decoder(bool) runtime.Decoder { @@ -865,7 +865,7 @@ func (f *fakeAPIFactory) PrinterForMapping(cmd *cobra.Command, isLocal bool, out func (f *fakeAPIFactory) NewBuilder() *resource.Builder { mapper, typer := f.Object() - unstructuredMapper, unstructuredTyper, _ := f.UnstructuredObject() + unstructuredMapper, unstructuredTyper := f.UnstructuredObject() return resource.NewBuilder( &resource.Mapper{ diff --git a/pkg/kubectl/cmd/util/BUILD b/pkg/kubectl/cmd/util/BUILD index bf458175abb..b5e6c57548c 100644 --- a/pkg/kubectl/cmd/util/BUILD +++ b/pkg/kubectl/cmd/util/BUILD @@ -9,6 +9,7 @@ go_library( srcs = [ "cached_discovery.go", "clientcache.go", + "discovery.go", "factory.go", "factory_builder.go", "factory_client_access.go", diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index ffa7f2344ec..35cba682508 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -191,7 +191,7 @@ type ObjectMappingFactory interface { Object() (meta.RESTMapper, runtime.ObjectTyper) // Returns interfaces for dealing with arbitrary // runtime.Unstructured. This performs API calls to discover types. - UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper, error) + UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper) // Returns interface for expanding categories like `all`. // TODO: this should probably return an error if the full expander can't be loaded. CategoryExpander() categories.CategoryExpander diff --git a/pkg/kubectl/cmd/util/factory_builder.go b/pkg/kubectl/cmd/util/factory_builder.go index 3fb4a3b47e8..413d764e2a6 100644 --- a/pkg/kubectl/cmd/util/factory_builder.go +++ b/pkg/kubectl/cmd/util/factory_builder.go @@ -51,16 +51,12 @@ func NewBuilderFactory(clientAccessFactory ClientAccessFactory, objectMappingFac func (f *ring2Factory) PrinterForCommand(cmd *cobra.Command, isLocal bool, outputOpts *printers.OutputOptions, options printers.PrintOptions) (printers.ResourcePrinter, error) { var mapper meta.RESTMapper var typer runtime.ObjectTyper - var err error if isLocal { mapper = legacyscheme.Registry.RESTMapper() typer = legacyscheme.Scheme } else { - mapper, typer, err = f.objectMappingFactory.UnstructuredObject() - if err != nil { - return nil, err - } + mapper, typer = f.objectMappingFactory.UnstructuredObject() } // TODO: used by the custom column implementation and the name implementation, break this dependency decoders := []runtime.Decoder{f.clientAccessFactory.Decoder(true), unstructured.UnstructuredJSONScheme} @@ -137,11 +133,7 @@ func (f *ring2Factory) PrintObject(cmd *cobra.Command, isLocal bool, mapper meta // fall back to an unstructured object if we get something unregistered if runtime.IsNotRegisteredError(err) { - _, typer, unstructuredErr := f.objectMappingFactory.UnstructuredObject() - if unstructuredErr != nil { - // if we can't get an unstructured typer, return the original error - return err - } + _, typer := f.objectMappingFactory.UnstructuredObject() gvks, _, err = typer.ObjectKinds(obj) } @@ -186,7 +178,7 @@ func (f *ring2Factory) NewBuilder() *resource.Builder { mapper, typer := f.objectMappingFactory.Object() unstructuredClientMapperFunc := resource.ClientMapperFunc(f.objectMappingFactory.UnstructuredClientForMapping) - unstructuredMapper, unstructuredTyper, _ := f.objectMappingFactory.UnstructuredObject() + unstructuredMapper, unstructuredTyper := f.objectMappingFactory.Object() categoryExpander := f.objectMappingFactory.CategoryExpander() diff --git a/pkg/kubectl/cmd/util/factory_object_mapping.go b/pkg/kubectl/cmd/util/factory_object_mapping.go index 5a01d4840ed..63d3b24ec65 100644 --- a/pkg/kubectl/cmd/util/factory_object_mapping.go +++ b/pkg/kubectl/cmd/util/factory_object_mapping.go @@ -27,6 +27,8 @@ import ( "sync" "time" + "github.com/golang/glog" + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -71,34 +73,35 @@ func NewObjectMappingFactory(clientAccessFactory ClientAccessFactory) ObjectMapp return f } -// TODO: This method should return an error now that it can fail. Alternatively, it needs to -// return lazy implementations of mapper and typer that don't hit the wire until they are -// invoked. -func (f *ring1Factory) Object() (meta.RESTMapper, runtime.ObjectTyper) { +func (f *ring1Factory) objectLoader() (meta.RESTMapper, runtime.ObjectTyper, error) { mapper := legacyscheme.Registry.RESTMapper() discoveryClient, err := f.clientAccessFactory.DiscoveryClient() - if err == nil { - mapper = meta.FirstHitRESTMapper{ - MultiRESTMapper: meta.MultiRESTMapper{ - discovery.NewDeferredDiscoveryRESTMapper(discoveryClient, legacyscheme.Registry.InterfacesFor), - legacyscheme.Registry.RESTMapper(), // hardcoded fall back - }, - } - - // wrap with shortcuts, they require a discoveryClient - mapper, err = NewShortcutExpander(mapper, discoveryClient) - // you only have an error on missing discoveryClient, so this shouldn't fail. Check anyway. - CheckErr(err) + if err != nil { + glog.V(5).Infof("Object loader was unable to retrieve a discovery client: %v", err) + return mapper, legacyscheme.Scheme, nil + } + mapper = meta.FirstHitRESTMapper{ + MultiRESTMapper: meta.MultiRESTMapper{ + discovery.NewDeferredDiscoveryRESTMapper(discoveryClient, legacyscheme.Registry.InterfacesFor), + legacyscheme.Registry.RESTMapper(), // hardcoded fall back + }, } - return mapper, legacyscheme.Scheme + // wrap with shortcuts, they require a discoveryClient + mapper = NewShortcutExpander(mapper, discoveryClient) + return mapper, legacyscheme.Scheme, nil } -func (f *ring1Factory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper, error) { +// TODO: unstructured and structured discovery should both gracefully degrade in the presence +// of errors, and it should be possible to get access to the unstructured object typers no matter +// whether the server responds or not. Make the legacy scheme recognize object typer, then we can +// safely fall back to the built in restmapper as a last resort. +func (f *ring1Factory) unstructuredObjectLoader() (meta.RESTMapper, runtime.ObjectTyper, error) { discoveryClient, err := f.clientAccessFactory.DiscoveryClient() if err != nil { return nil, nil, err } + groupResources, err := discovery.GetAPIGroupResources(discoveryClient) if err != nil && !discoveryClient.Fresh() { discoveryClient.Invalidate() @@ -112,10 +115,18 @@ func (f *ring1Factory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectType interfaces := meta.InterfacesForUnstructuredConversion(legacyscheme.Registry.InterfacesFor) mapper := discovery.NewDeferredDiscoveryRESTMapper(discoveryClient, meta.VersionInterfacesFunc(interfaces)) typer := discovery.NewUnstructuredObjectTyper(groupResources) - expander, err := NewShortcutExpander(mapper, discoveryClient) + expander := NewShortcutExpander(mapper, discoveryClient) return expander, typer, err } +func (f *ring1Factory) Object() (meta.RESTMapper, runtime.ObjectTyper) { + return NewLazyObjectLoader(f.objectLoader) +} + +func (f *ring1Factory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper) { + return NewLazyObjectLoader(f.unstructuredObjectLoader) +} + func (f *ring1Factory) CategoryExpander() categories.CategoryExpander { legacyExpander := categories.LegacyCategoryExpander diff --git a/pkg/kubectl/cmd/util/factory_test.go b/pkg/kubectl/cmd/util/factory_test.go index 364c7d5b385..6acfe4c005c 100644 --- a/pkg/kubectl/cmd/util/factory_test.go +++ b/pkg/kubectl/cmd/util/factory_test.go @@ -539,10 +539,7 @@ func TestDiscoveryReplaceAliases(t *testing.T) { } ds := &fakeDiscoveryClient{} - mapper, err := NewShortcutExpander(testapi.Default.RESTMapper(), ds) - if err != nil { - t.Fatalf("Unable to create shortcut expander, err = %s", err.Error()) - } + mapper := NewShortcutExpander(testapi.Default.RESTMapper(), ds) b := resource.NewBuilder( &resource.Mapper{ RESTMapper: mapper, diff --git a/pkg/kubectl/cmd/util/shortcut_restmapper.go b/pkg/kubectl/cmd/util/shortcut_restmapper.go index 33889b48a50..c5ecfa84b12 100644 --- a/pkg/kubectl/cmd/util/shortcut_restmapper.go +++ b/pkg/kubectl/cmd/util/shortcut_restmapper.go @@ -17,7 +17,6 @@ limitations under the License. package util import ( - "errors" "strings" "github.com/golang/glog" @@ -37,11 +36,8 @@ type shortcutExpander struct { var _ meta.RESTMapper = &shortcutExpander{} -func NewShortcutExpander(delegate meta.RESTMapper, client discovery.DiscoveryInterface) (shortcutExpander, error) { - if client == nil { - return shortcutExpander{}, errors.New("Please provide discovery client to shortcut expander") - } - return shortcutExpander{RESTMapper: delegate, discoveryClient: client}, nil +func NewShortcutExpander(delegate meta.RESTMapper, client discovery.DiscoveryInterface) shortcutExpander { + return shortcutExpander{RESTMapper: delegate, discoveryClient: client} } func (e shortcutExpander) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { diff --git a/pkg/kubectl/cmd/util/shortcut_restmapper_test.go b/pkg/kubectl/cmd/util/shortcut_restmapper_test.go index 689f06994ac..41903643fd4 100644 --- a/pkg/kubectl/cmd/util/shortcut_restmapper_test.go +++ b/pkg/kubectl/cmd/util/shortcut_restmapper_test.go @@ -71,10 +71,7 @@ func TestReplaceAliases(t *testing.T) { } ds := &fakeDiscoveryClient{} - mapper, err := NewShortcutExpander(testapi.Default.RESTMapper(), ds) - if err != nil { - t.Fatalf("Unable to create shortcut expander, err %s", err.Error()) - } + mapper := NewShortcutExpander(testapi.Default.RESTMapper(), ds) for _, test := range tests { ds.serverResourcesHandler = func() ([]*metav1.APIResourceList, error) { @@ -126,10 +123,7 @@ func TestKindFor(t *testing.T) { } ds := &fakeDiscoveryClient{} - mapper, err := NewShortcutExpander(testapi.Default.RESTMapper(), ds) - if err != nil { - t.Fatalf("Unable to create shortcut expander, err %s", err.Error()) - } + mapper := NewShortcutExpander(testapi.Default.RESTMapper(), ds) for i, test := range tests { ds.serverResourcesHandler = func() ([]*metav1.APIResourceList, error) { diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD b/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD index 5e2cb106531..f42cd77fbec 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/BUILD @@ -34,6 +34,7 @@ go_library( "firsthit_restmapper.go", "help.go", "interfaces.go", + "lazy.go", "meta.go", "multirestmapper.go", "priority.go", diff --git a/staging/src/k8s.io/apimachinery/pkg/api/meta/lazy.go b/staging/src/k8s.io/apimachinery/pkg/api/meta/lazy.go new file mode 100644 index 00000000000..7f92f39a43f --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/api/meta/lazy.go @@ -0,0 +1,121 @@ +/* +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 meta + +import ( + "sync" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// lazyObject defers loading the mapper and typer until necessary. +type lazyObject struct { + loader func() (RESTMapper, runtime.ObjectTyper, error) + + lock sync.Mutex + loaded bool + err error + mapper RESTMapper + typer runtime.ObjectTyper +} + +// NewLazyObjectLoader handles unrecoverable errors when creating a RESTMapper / ObjectTyper by +// returning those initialization errors when the interface methods are invoked. This defers the +// initialization and any server calls until a client actually needs to perform the action. +func NewLazyObjectLoader(fn func() (RESTMapper, runtime.ObjectTyper, error)) (RESTMapper, runtime.ObjectTyper) { + obj := &lazyObject{loader: fn} + return obj, obj +} + +// init lazily loads the mapper and typer, returning an error if initialization has failed. +func (o *lazyObject) init() error { + o.lock.Lock() + defer o.lock.Unlock() + if o.loaded { + return o.err + } + o.mapper, o.typer, o.err = o.loader() + o.loaded = true + return o.err +} + +var _ RESTMapper = &lazyObject{} +var _ runtime.ObjectTyper = &lazyObject{} + +func (o *lazyObject) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { + if err := o.init(); err != nil { + return schema.GroupVersionKind{}, err + } + return o.mapper.KindFor(resource) +} + +func (o *lazyObject) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { + if err := o.init(); err != nil { + return []schema.GroupVersionKind{}, err + } + return o.mapper.KindsFor(resource) +} + +func (o *lazyObject) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) { + if err := o.init(); err != nil { + return schema.GroupVersionResource{}, err + } + return o.mapper.ResourceFor(input) +} + +func (o *lazyObject) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { + if err := o.init(); err != nil { + return []schema.GroupVersionResource{}, err + } + return o.mapper.ResourcesFor(input) +} + +func (o *lazyObject) RESTMapping(gk schema.GroupKind, versions ...string) (*RESTMapping, error) { + if err := o.init(); err != nil { + return nil, err + } + return o.mapper.RESTMapping(gk, versions...) +} + +func (o *lazyObject) RESTMappings(gk schema.GroupKind, versions ...string) ([]*RESTMapping, error) { + if err := o.init(); err != nil { + return nil, err + } + return o.mapper.RESTMappings(gk, versions...) +} + +func (o *lazyObject) ResourceSingularizer(resource string) (singular string, err error) { + if err := o.init(); err != nil { + return "", err + } + return o.mapper.ResourceSingularizer(resource) +} + +func (o *lazyObject) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) { + if err := o.init(); err != nil { + return nil, false, err + } + return o.typer.ObjectKinds(obj) +} + +func (o *lazyObject) Recognizes(gvk schema.GroupVersionKind) bool { + if err := o.init(); err != nil { + return false + } + return o.typer.Recognizes(gvk) +} diff --git a/staging/src/k8s.io/client-go/discovery/unstructured.go b/staging/src/k8s.io/client-go/discovery/unstructured.go index 4a0fd643fa6..fa7f2ec061b 100644 --- a/staging/src/k8s.io/client-go/discovery/unstructured.go +++ b/staging/src/k8s.io/client-go/discovery/unstructured.go @@ -17,22 +17,28 @@ limitations under the License. package discovery import ( - "fmt" + "reflect" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) -// UnstructuredObjectTyper provides a runtime.ObjectTyper implmentation for +// UnstructuredObjectTyper provides a runtime.ObjectTyper implementation for // runtime.Unstructured object based on discovery information. type UnstructuredObjectTyper struct { registered map[schema.GroupVersionKind]bool + typers []runtime.ObjectTyper } // NewUnstructuredObjectTyper returns a runtime.ObjectTyper for -// unstructred objects based on discovery information. -func NewUnstructuredObjectTyper(groupResources []*APIGroupResources) *UnstructuredObjectTyper { - dot := &UnstructuredObjectTyper{registered: make(map[schema.GroupVersionKind]bool)} +// unstructured objects based on discovery information. It accepts a list of fallback typers +// for handling objects that are not runtime.Unstructured. It does not delegate the Recognizes +// check, only ObjectKinds. +func NewUnstructuredObjectTyper(groupResources []*APIGroupResources, typers ...runtime.ObjectTyper) *UnstructuredObjectTyper { + dot := &UnstructuredObjectTyper{ + registered: make(map[schema.GroupVersionKind]bool), + typers: typers, + } for _, group := range groupResources { for _, discoveryVersion := range group.Group.Versions { resources, ok := group.VersionedResources[discoveryVersion.Version] @@ -55,17 +61,29 @@ func NewUnstructuredObjectTyper(groupResources []*APIGroupResources) *Unstructur // because runtime.Unstructured object should always have group,version,kind // information set. func (d *UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) (gvks []schema.GroupVersionKind, unversionedType bool, err error) { - if _, ok := obj.(runtime.Unstructured); !ok { - return nil, false, fmt.Errorf("type %T is invalid for dynamic object typer", obj) + if _, ok := obj.(runtime.Unstructured); ok { + gvk := obj.GetObjectKind().GroupVersionKind() + if len(gvk.Kind) == 0 { + return nil, false, runtime.NewMissingKindErr("object has no kind field ") + } + if len(gvk.Version) == 0 { + return nil, false, runtime.NewMissingVersionErr("object has no apiVersion field") + } + return []schema.GroupVersionKind{gvk}, false, nil } - gvk := obj.GetObjectKind().GroupVersionKind() - if len(gvk.Kind) == 0 { - return nil, false, runtime.NewMissingKindErr("unstructured object has no kind") + var lastErr error + for _, typer := range d.typers { + gvks, unversioned, err := typer.ObjectKinds(obj) + if err != nil { + lastErr = err + continue + } + return gvks, unversioned, nil } - if len(gvk.Version) == 0 { - return nil, false, runtime.NewMissingVersionErr("unstructured object has no version") + if lastErr == nil { + lastErr = runtime.NewNotRegisteredErrForType(reflect.TypeOf(obj)) } - return []schema.GroupVersionKind{gvk}, false, nil + return nil, false, lastErr } // Recognizes returns true if the provided group,version,kind was in the diff --git a/staging/src/k8s.io/client-go/dynamic/dynamic_util.go b/staging/src/k8s.io/client-go/dynamic/dynamic_util.go index c834dbb0dff..c2cf0daeae0 100644 --- a/staging/src/k8s.io/client-go/dynamic/dynamic_util.go +++ b/staging/src/k8s.io/client-go/dynamic/dynamic_util.go @@ -84,7 +84,7 @@ func NewObjectTyper(resources []*metav1.APIResourceList) (runtime.ObjectTyper, e // information. func (ot *ObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) { if _, ok := obj.(*unstructured.Unstructured); !ok { - return nil, false, fmt.Errorf("type %T is invalid for dynamic object typer", obj) + return nil, false, fmt.Errorf("type %T is invalid for determining dynamic object types", obj) } return []schema.GroupVersionKind{obj.GetObjectKind().GroupVersionKind()}, false, nil }