Add a lazy discovery interface for Unstructured

Delays the error until the first call and then preserves it for others.
More closely matches the intent of the Object() calls. Loaders are now
lazy and don't need to return errors directly.

Sets the stage for collapsing unstructured and structured builders
together.
This commit is contained in:
Clayton Coleman
2017-11-14 22:16:10 -05:00
parent 287a3ed1e1
commit e298aa39c3
12 changed files with 202 additions and 71 deletions

View File

@@ -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{

View File

@@ -9,6 +9,7 @@ go_library(
srcs = [
"cached_discovery.go",
"clientcache.go",
"discovery.go",
"factory.go",
"factory_builder.go",
"factory_client_access.go",

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -34,6 +34,7 @@ go_library(
"firsthit_restmapper.go",
"help.go",
"interfaces.go",
"lazy.go",
"meta.go",
"multirestmapper.go",
"priority.go",

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}