diff --git a/pkg/cache/informer_cache.go b/pkg/cache/informer_cache.go index ac07be2a6e..08e4e6df59 100644 --- a/pkg/cache/informer_cache.go +++ b/pkg/cache/informer_cache.go @@ -51,7 +51,7 @@ type informerCache struct { } // Get implements Reader. -func (ip *informerCache) Get(ctx context.Context, key client.ObjectKey, out client.Object) error { +func (ip *informerCache) Get(ctx context.Context, key client.ObjectKey, out client.Object, opts ...client.GetOption) error { gvk, err := apiutil.GVKForObject(out, ip.Scheme) if err != nil { return err diff --git a/pkg/cache/informertest/fake_cache.go b/pkg/cache/informertest/fake_cache.go index e115d380f7..da3bf8e0d4 100644 --- a/pkg/cache/informertest/fake_cache.go +++ b/pkg/cache/informertest/fake_cache.go @@ -131,7 +131,7 @@ func (c *FakeInformers) IndexField(ctx context.Context, obj client.Object, field } // Get implements Cache. -func (c *FakeInformers) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { +func (c *FakeInformers) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { return nil } diff --git a/pkg/cache/internal/cache_reader.go b/pkg/cache/internal/cache_reader.go index b95af18d78..9c2255123c 100644 --- a/pkg/cache/internal/cache_reader.go +++ b/pkg/cache/internal/cache_reader.go @@ -54,7 +54,7 @@ type CacheReader struct { } // Get checks the indexer for the object and writes a copy of it if found. -func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Object) error { +func (c *CacheReader) Get(_ context.Context, key client.ObjectKey, out client.Object, opts ...client.GetOption) error { if c.scopeName == apimeta.RESTScopeNameRoot { key.Namespace = "" } diff --git a/pkg/cache/multi_namespace_cache.go b/pkg/cache/multi_namespace_cache.go index 47a5bb3b3f..64514c0c55 100644 --- a/pkg/cache/multi_namespace_cache.go +++ b/pkg/cache/multi_namespace_cache.go @@ -200,7 +200,7 @@ func (c *multiNamespaceCache) IndexField(ctx context.Context, obj client.Object, return nil } -func (c *multiNamespaceCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { +func (c *multiNamespaceCache) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { isNamespaced, err := objectutil.IsAPINamespaced(obj, c.Scheme, c.RESTMapper) if err != nil { return err diff --git a/pkg/client/client.go b/pkg/client/client.go index bbe36c4673..8a61614337 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -241,16 +241,16 @@ func (c *client) Patch(ctx context.Context, obj Object, patch Patch, opts ...Pat } // Get implements client.Client. -func (c *client) Get(ctx context.Context, key ObjectKey, obj Object) error { +func (c *client) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { switch obj.(type) { case *unstructured.Unstructured: - return c.unstructuredClient.Get(ctx, key, obj) + return c.unstructuredClient.Get(ctx, key, obj, opts...) case *metav1.PartialObjectMetadata: // Metadata only object should always preserve the GVK coming in from the caller. defer c.resetGroupVersionKind(obj, obj.GetObjectKind().GroupVersionKind()) - return c.metadataClient.Get(ctx, key, obj) + return c.metadataClient.Get(ctx, key, obj, opts...) default: - return c.typedClient.Get(ctx, key, obj) + return c.typedClient.Get(ctx, key, obj, opts...) } } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 52bec53ff0..b60324eb33 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -2913,6 +2913,24 @@ var _ = Describe("Client", func() { }) }) + Describe("GetOptions", func() { + It("should be convertable to metav1.GetOptions", func() { + o := (&client.GetOptions{}).ApplyOptions([]client.GetOption{ + &client.GetOptions{Raw: &metav1.GetOptions{ResourceVersion: "RV0"}}, + }) + mo := o.AsGetOptions() + Expect(mo).NotTo(BeNil()) + Expect(mo.ResourceVersion).To(Equal("RV0")) + }) + + It("should produce empty metav1.GetOptions if nil", func() { + var o *client.GetOptions + Expect(o.AsGetOptions()).To(Equal(&metav1.GetOptions{})) + o = &client.GetOptions{} + Expect(o.AsGetOptions()).To(Equal(&metav1.GetOptions{})) + }) + }) + Describe("ListOptions", func() { It("should be convertable to metav1.ListOptions", func() { lo := (&client.ListOptions{}).ApplyOptions([]client.ListOption{ @@ -3389,7 +3407,7 @@ type fakeReader struct { Called int } -func (f *fakeReader) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { +func (f *fakeReader) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { f.Called++ return nil } diff --git a/pkg/client/dryrun.go b/pkg/client/dryrun.go index ea25ea2530..14606a5794 100644 --- a/pkg/client/dryrun.go +++ b/pkg/client/dryrun.go @@ -72,8 +72,8 @@ func (c *dryRunClient) Patch(ctx context.Context, obj Object, patch Patch, opts } // Get implements client.Client. -func (c *dryRunClient) Get(ctx context.Context, key ObjectKey, obj Object) error { - return c.client.Get(ctx, key, obj) +func (c *dryRunClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { + return c.client.Get(ctx, key, obj, opts...) } // List implements client.Client. diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 00d5b3bfe2..b7ca2de47a 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -329,7 +329,7 @@ func (t versionedTracker) Update(gvr schema.GroupVersionResource, obj runtime.Ob return t.ObjectTracker.Update(gvr, obj, ns) } -func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { +func (c *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { gvr, err := getGVRFromObject(obj, c.scheme) if err != nil { return err diff --git a/pkg/client/interfaces.go b/pkg/client/interfaces.go index 58c2ece15b..32b6234cf9 100644 --- a/pkg/client/interfaces.go +++ b/pkg/client/interfaces.go @@ -50,7 +50,7 @@ type Reader interface { // Get retrieves an obj for the given object key from the Kubernetes Cluster. // obj must be a struct pointer so that obj can be updated with the response // returned by the Server. - Get(ctx context.Context, key ObjectKey, obj Object) error + Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error // List retrieves list of objects for a given namespace and list options. On a // successful call, Items field in the list will be populated with the diff --git a/pkg/client/metadata_client.go b/pkg/client/metadata_client.go index db495ca46b..2854556f32 100644 --- a/pkg/client/metadata_client.go +++ b/pkg/client/metadata_client.go @@ -116,7 +116,7 @@ func (mc *metadataClient) Patch(ctx context.Context, obj Object, patch Patch, op } // Get implements client.Client. -func (mc *metadataClient) Get(ctx context.Context, key ObjectKey, obj Object) error { +func (mc *metadataClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { metadata, ok := obj.(*metav1.PartialObjectMetadata) if !ok { return fmt.Errorf("metadata client did not understand object: %T", obj) @@ -124,12 +124,15 @@ func (mc *metadataClient) Get(ctx context.Context, key ObjectKey, obj Object) er gvk := metadata.GroupVersionKind() + getOpts := GetOptions{} + getOpts.ApplyOptions(opts) + resInt, err := mc.getResourceInterface(gvk, key.Namespace) if err != nil { return err } - res, err := resInt.Get(ctx, key.Name, metav1.GetOptions{}) + res, err := resInt.Get(ctx, key.Name, *getOpts.AsGetOptions()) if err != nil { return err } diff --git a/pkg/client/namespaced_client.go b/pkg/client/namespaced_client.go index 45a694543e..12f1791c8e 100644 --- a/pkg/client/namespaced_client.go +++ b/pkg/client/namespaced_client.go @@ -138,7 +138,7 @@ func (n *namespacedClient) Patch(ctx context.Context, obj Object, patch Patch, o } // Get implements client.Client. -func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object) error { +func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { isNamespaceScoped, err := objectutil.IsAPINamespaced(obj, n.Scheme(), n.RESTMapper()) if err != nil { return fmt.Errorf("error finding the scope of the object: %w", err) @@ -149,7 +149,7 @@ func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object) e } key.Namespace = n.namespace } - return n.client.Get(ctx, key, obj) + return n.client.Get(ctx, key, obj, opts...) } // List implements client.Client. diff --git a/pkg/client/options.go b/pkg/client/options.go index 7990f56ab0..495b86944c 100644 --- a/pkg/client/options.go +++ b/pkg/client/options.go @@ -37,6 +37,12 @@ type DeleteOption interface { ApplyToDelete(*DeleteOptions) } +// GetOption is some configuration that modifies options for a get request. +type GetOption interface { + // ApplyToGet applies this configuration to the given get options. + ApplyToGet(*GetOptions) +} + // ListOption is some configuration that modifies options for a list request. type ListOption interface { // ApplyToList applies this configuration to the given list options. @@ -311,6 +317,45 @@ func (p PropagationPolicy) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) { // }}} +// {{{ Get Options + +// GetOptions contains options for get operation. +// Now it only has a Raw field, with support for specific resourceVersion. +type GetOptions struct { + // Raw represents raw GetOptions, as passed to the API server. Note + // that these may not be respected by all implementations of interface. + Raw *metav1.GetOptions +} + +var _ GetOption = &GetOptions{} + +// ApplyToGet implements GetOption for GetOptions. +func (o *GetOptions) ApplyToGet(lo *GetOptions) { + if o.Raw != nil { + lo.Raw = o.Raw + } +} + +// AsGetOptions returns these options as a flattened metav1.GetOptions. +// This may mutate the Raw field. +func (o *GetOptions) AsGetOptions() *metav1.GetOptions { + if o == nil || o.Raw == nil { + return &metav1.GetOptions{} + } + return o.Raw +} + +// ApplyOptions applies the given get options on these options, +// and then returns itself (for convenient chaining). +func (o *GetOptions) ApplyOptions(opts []GetOption) *GetOptions { + for _, opt := range opts { + opt.ApplyToGet(o) + } + return o +} + +// }}} + // {{{ List Options // ListOptions contains options for limiting or filtering results. diff --git a/pkg/client/options_test.go b/pkg/client/options_test.go index 36447a3f3c..cb1363ba54 100644 --- a/pkg/client/options_test.go +++ b/pkg/client/options_test.go @@ -74,6 +74,15 @@ var _ = Describe("ListOptions", func() { }) }) +var _ = Describe("GetOptions", func() { + It("Should set Raw", func() { + o := &client.GetOptions{Raw: &metav1.GetOptions{ResourceVersion: "RV0"}} + newGetOpts := &client.GetOptions{} + o.ApplyToGet(newGetOpts) + Expect(newGetOpts).To(Equal(o)) + }) +}) + var _ = Describe("CreateOptions", func() { It("Should set DryRun", func() { o := &client.CreateOptions{DryRun: []string{"Hello", "Theodore"}} diff --git a/pkg/client/split.go b/pkg/client/split.go index bf4b861f39..8717345349 100644 --- a/pkg/client/split.go +++ b/pkg/client/split.go @@ -121,13 +121,13 @@ func (d *delegatingReader) shouldBypassCache(obj runtime.Object) (bool, error) { } // Get retrieves an obj for a given object key from the Kubernetes Cluster. -func (d *delegatingReader) Get(ctx context.Context, key ObjectKey, obj Object) error { +func (d *delegatingReader) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { if isUncached, err := d.shouldBypassCache(obj); err != nil { return err } else if isUncached { - return d.ClientReader.Get(ctx, key, obj) + return d.ClientReader.Get(ctx, key, obj, opts...) } - return d.CacheReader.Get(ctx, key, obj) + return d.CacheReader.Get(ctx, key, obj, opts...) } // List retrieves list of objects for a given namespace and list options. diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go index dde7b21f25..c4e56d9be6 100644 --- a/pkg/client/typed_client.go +++ b/pkg/client/typed_client.go @@ -132,14 +132,17 @@ func (c *typedClient) Patch(ctx context.Context, obj Object, patch Patch, opts . } // Get implements client.Client. -func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object) error { +func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { r, err := c.cache.getResource(obj) if err != nil { return err } + getOpts := GetOptions{} + getOpts.ApplyOptions(opts) return r.Get(). NamespaceIfScoped(key.Namespace, r.isNamespaced()). Resource(r.resource()). + VersionedParams(getOpts.AsGetOptions(), c.paramCodec). Name(key.Name).Do(ctx).Into(obj) } diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go index 819527e700..3d3dbe7b28 100644 --- a/pkg/client/unstructured_client.go +++ b/pkg/client/unstructured_client.go @@ -165,7 +165,7 @@ func (uc *unstructuredClient) Patch(ctx context.Context, obj Object, patch Patch } // Get implements client.Client. -func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object) error { +func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { u, ok := obj.(*unstructured.Unstructured) if !ok { return fmt.Errorf("unstructured client did not understand object: %T", obj) @@ -173,6 +173,9 @@ func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object gvk := u.GroupVersionKind() + getOpts := GetOptions{} + getOpts.ApplyOptions(opts) + r, err := uc.cache.getResource(obj) if err != nil { return err @@ -181,6 +184,7 @@ func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object result := r.Get(). NamespaceIfScoped(key.Namespace, r.isNamespaced()). Resource(r.resource()). + VersionedParams(getOpts.AsGetOptions(), uc.paramCodec). Name(key.Name). Do(ctx). Into(obj) diff --git a/pkg/controller/controllerutil/controllerutil_test.go b/pkg/controller/controllerutil/controllerutil_test.go index e2464b2953..4fd77dc497 100644 --- a/pkg/controller/controllerutil/controllerutil_test.go +++ b/pkg/controller/controllerutil/controllerutil_test.go @@ -832,6 +832,6 @@ type errorReader struct { client.Client } -func (e errorReader) Get(ctx context.Context, key client.ObjectKey, into client.Object) error { +func (e errorReader) Get(ctx context.Context, key client.ObjectKey, into client.Object, opts ...client.GetOption) error { return fmt.Errorf("unexpected error") }