From e298aa39c3de8ad1059861b7f78d62005ca87f88 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Tue, 14 Nov 2017 22:16:10 -0500 Subject: [PATCH] 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. --- pkg/kubectl/cmd/testing/fake.go | 16 +-- pkg/kubectl/cmd/util/BUILD | 1 + pkg/kubectl/cmd/util/factory.go | 2 +- pkg/kubectl/cmd/util/factory_builder.go | 14 +- .../cmd/util/factory_object_mapping.go | 49 ++++--- pkg/kubectl/cmd/util/factory_test.go | 5 +- pkg/kubectl/cmd/util/shortcut_restmapper.go | 8 +- .../cmd/util/shortcut_restmapper_test.go | 10 +- .../k8s.io/apimachinery/pkg/api/meta/BUILD | 1 + .../k8s.io/apimachinery/pkg/api/meta/lazy.go | 121 ++++++++++++++++++ .../client-go/discovery/unstructured.go | 44 +++++-- .../k8s.io/client-go/dynamic/dynamic_util.go | 2 +- 12 files changed, 202 insertions(+), 71 deletions(-) create mode 100644 staging/src/k8s.io/apimachinery/pkg/api/meta/lazy.go 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 }