Skip to content

Commit 8fe9e39

Browse files
authored
Merge pull request #1249 from vincepri/delegating-client-uncached
⚠Add ability for the delegating client to avoid caching objects
2 parents 33f1d80 + 6089977 commit 8fe9e39

File tree

6 files changed

+140
-44
lines changed

6 files changed

+140
-44
lines changed

pkg/client/client_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3065,10 +3065,11 @@ var _ = Describe("DelegatingClient", func() {
30653065
cachedReader := &fakeReader{}
30663066
cl, err := client.New(cfg, client.Options{})
30673067
Expect(err).NotTo(HaveOccurred())
3068-
dReader := client.NewDelegatingClient(client.NewDelegatingClientInput{
3068+
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
30693069
CacheReader: cachedReader,
30703070
Client: cl,
30713071
})
3072+
Expect(err).NotTo(HaveOccurred())
30723073
var actual appsv1.Deployment
30733074
key := client.ObjectKey{Namespace: "ns", Name: "name"}
30743075
Expect(dReader.Get(context.TODO(), key, &actual)).To(Succeed())
@@ -3079,10 +3080,11 @@ var _ = Describe("DelegatingClient", func() {
30793080
cachedReader := &fakeReader{}
30803081
cl, err := client.New(cfg, client.Options{})
30813082
Expect(err).NotTo(HaveOccurred())
3082-
dReader := client.NewDelegatingClient(client.NewDelegatingClientInput{
3083+
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
30833084
CacheReader: cachedReader,
30843085
Client: cl,
30853086
})
3087+
Expect(err).NotTo(HaveOccurred())
30863088
dep := &appsv1.Deployment{
30873089
ObjectMeta: metav1.ObjectMeta{
30883090
Name: "deployment1",
@@ -3123,10 +3125,11 @@ var _ = Describe("DelegatingClient", func() {
31233125
cachedReader := &fakeReader{}
31243126
cl, err := client.New(cfg, client.Options{})
31253127
Expect(err).NotTo(HaveOccurred())
3126-
dReader := client.NewDelegatingClient(client.NewDelegatingClientInput{
3128+
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
31273129
CacheReader: cachedReader,
31283130
Client: cl,
31293131
})
3132+
Expect(err).NotTo(HaveOccurred())
31303133
var actual appsv1.DeploymentList
31313134
Expect(dReader.List(context.Background(), &actual)).To(Succeed())
31323135
Expect(1).To(Equal(cachedReader.Called))
@@ -3136,10 +3139,11 @@ var _ = Describe("DelegatingClient", func() {
31363139
cachedReader := &fakeReader{}
31373140
cl, err := client.New(cfg, client.Options{})
31383141
Expect(err).NotTo(HaveOccurred())
3139-
dReader := client.NewDelegatingClient(client.NewDelegatingClientInput{
3142+
dReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{
31403143
CacheReader: cachedReader,
31413144
Client: cl,
31423145
})
3146+
Expect(err).NotTo(HaveOccurred())
31433147

31443148
actual := &unstructured.UnstructuredList{}
31453149
actual.SetGroupVersionKind(schema.GroupVersionKind{

pkg/client/split.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,44 @@ import (
2222
"k8s.io/apimachinery/pkg/api/meta"
2323
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2424
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2527
)
2628

2729
// NewDelegatingClientInput encapsulates the input parameters to create a new delegating client.
2830
type NewDelegatingClientInput struct {
29-
CacheReader Reader
30-
Client Client
31+
CacheReader Reader
32+
Client Client
33+
UncachedObjects []Object
3134
}
3235

3336
// NewDelegatingClient creates a new delegating client.
3437
//
3538
// A delegating client forms a Client by composing separate reader, writer and
3639
// statusclient interfaces. This way, you can have an Client that reads from a
3740
// cache and writes to the API server.
38-
func NewDelegatingClient(in NewDelegatingClientInput) Client {
41+
func NewDelegatingClient(in NewDelegatingClientInput) (Client, error) {
42+
uncachedGVKs := map[schema.GroupVersionKind]struct{}{}
43+
for _, obj := range in.UncachedObjects {
44+
gvk, err := apiutil.GVKForObject(obj, in.Client.Scheme())
45+
if err != nil {
46+
return nil, err
47+
}
48+
uncachedGVKs[gvk] = struct{}{}
49+
}
50+
3951
return &delegatingClient{
4052
scheme: in.Client.Scheme(),
4153
mapper: in.Client.RESTMapper(),
4254
Reader: &delegatingReader{
4355
CacheReader: in.CacheReader,
4456
ClientReader: in.Client,
57+
scheme: in.Client.Scheme(),
58+
uncachedGVKs: uncachedGVKs,
4559
},
4660
Writer: in.Client,
4761
StatusClient: in.Client,
48-
}
62+
}, nil
4963
}
5064

5165
type delegatingClient struct {
@@ -75,21 +89,37 @@ func (d *delegatingClient) RESTMapper() meta.RESTMapper {
7589
type delegatingReader struct {
7690
CacheReader Reader
7791
ClientReader Reader
92+
93+
uncachedGVKs map[schema.GroupVersionKind]struct{}
94+
scheme *runtime.Scheme
95+
}
96+
97+
func (d *delegatingReader) shouldBypassCache(obj runtime.Object) (bool, error) {
98+
gvk, err := apiutil.GVKForObject(obj, d.scheme)
99+
if err != nil {
100+
return false, err
101+
}
102+
_, isUncached := d.uncachedGVKs[gvk]
103+
_, isUnstructured := obj.(*unstructured.Unstructured)
104+
_, isUnstructuredList := obj.(*unstructured.UnstructuredList)
105+
return isUncached || isUnstructured || isUnstructuredList, nil
78106
}
79107

80108
// Get retrieves an obj for a given object key from the Kubernetes Cluster.
81109
func (d *delegatingReader) Get(ctx context.Context, key ObjectKey, obj Object) error {
82-
_, isUnstructured := obj.(*unstructured.Unstructured)
83-
if isUnstructured {
110+
if isUncached, err := d.shouldBypassCache(obj); err != nil {
111+
return err
112+
} else if isUncached {
84113
return d.ClientReader.Get(ctx, key, obj)
85114
}
86115
return d.CacheReader.Get(ctx, key, obj)
87116
}
88117

89118
// List retrieves list of objects for a given namespace and list options.
90119
func (d *delegatingReader) List(ctx context.Context, list ObjectList, opts ...ListOption) error {
91-
_, isUnstructured := list.(*unstructured.UnstructuredList)
92-
if isUnstructured {
120+
if isUncached, err := d.shouldBypassCache(list); err != nil {
121+
return err
122+
} else if isUncached {
93123
return d.ClientReader.List(ctx, list, opts...)
94124
}
95125
return d.CacheReader.List(ctx, list, opts...)

pkg/manager/client_builder.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manager
18+
19+
import (
20+
"k8s.io/client-go/rest"
21+
"sigs.k8s.io/controller-runtime/pkg/cache"
22+
"sigs.k8s.io/controller-runtime/pkg/client"
23+
)
24+
25+
// ClientBuilder builder is the interface for the client builder.
26+
type ClientBuilder interface {
27+
// WithUncached takes a list of runtime objects (plain or lists) that users don't want to cache
28+
// for this client. This function can be called multiple times, it should append to an internal slice.
29+
WithUncached(objs ...client.Object) ClientBuilder
30+
31+
// Build returns a new client.
32+
Build(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error)
33+
}
34+
35+
// NewClientBuilder returns a builder to build new clients to be passed when creating a Manager.
36+
func NewClientBuilder() ClientBuilder {
37+
return &newClientBuilder{}
38+
}
39+
40+
type newClientBuilder struct {
41+
uncached []client.Object
42+
}
43+
44+
func (n *newClientBuilder) WithUncached(objs ...client.Object) ClientBuilder {
45+
n.uncached = append(n.uncached, objs...)
46+
return n
47+
}
48+
49+
func (n *newClientBuilder) Build(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error) {
50+
// Create the Client for Write operations.
51+
c, err := client.New(config, options)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
return client.NewDelegatingClient(client.NewDelegatingClientInput{
57+
CacheReader: cache,
58+
Client: c,
59+
UncachedObjects: n.uncached,
60+
})
61+
}

pkg/manager/manager.go

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -235,10 +235,14 @@ type Options struct {
235235
// by the manager. If not set this will use the default new cache function.
236236
NewCache cache.NewCacheFunc
237237

238-
// NewClient will create the client to be used by the manager.
238+
// ClientBuilder is the builder that creates the client to be used by the manager.
239239
// If not set this will create the default DelegatingClient that will
240240
// use the cache for reads and the client for writes.
241-
NewClient NewClientFunc
241+
ClientBuilder ClientBuilder
242+
243+
// ClientDisableCacheFor tells the client that, if any cache is used, to bypass it
244+
// for the given objects.
245+
ClientDisableCacheFor []client.Object
242246

243247
// DryRunClient specifies whether the client should be configured to enforce
244248
// dryRun mode.
@@ -270,9 +274,6 @@ type Options struct {
270274
newHealthProbeListener func(addr string) (net.Listener, error)
271275
}
272276

273-
// NewClientFunc allows a user to define how to create a client
274-
type NewClientFunc func(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error)
275-
276277
// Runnable allows a component to be started.
277278
// It's very important that Start blocks until
278279
// it's done running.
@@ -323,12 +324,16 @@ func New(config *rest.Config, options Options) (Manager, error) {
323324
return nil, err
324325
}
325326

326-
apiReader, err := client.New(config, client.Options{Scheme: options.Scheme, Mapper: mapper})
327+
clientOptions := client.Options{Scheme: options.Scheme, Mapper: mapper}
328+
329+
apiReader, err := client.New(config, clientOptions)
327330
if err != nil {
328331
return nil, err
329332
}
330333

331-
writeObj, err := options.NewClient(cache, config, client.Options{Scheme: options.Scheme, Mapper: mapper})
334+
writeObj, err := options.ClientBuilder.
335+
WithUncached(options.ClientDisableCacheFor...).
336+
Build(cache, config, clientOptions)
332337
if err != nil {
333338
return nil, err
334339
}
@@ -503,20 +508,6 @@ func (o Options) setLeaderElectionConfig(obj v1alpha1.ControllerManagerConfigura
503508
return o
504509
}
505510

506-
// DefaultNewClient creates the default caching client
507-
func DefaultNewClient(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error) {
508-
// Create the Client for Write operations.
509-
c, err := client.New(config, options)
510-
if err != nil {
511-
return nil, err
512-
}
513-
514-
return client.NewDelegatingClient(client.NewDelegatingClientInput{
515-
CacheReader: cache,
516-
Client: c,
517-
}), nil
518-
}
519-
520511
// defaultHealthProbeListener creates the default health probes listener bound to the given address
521512
func defaultHealthProbeListener(addr string) (net.Listener, error) {
522513
if addr == "" || addr == "0" {
@@ -543,9 +534,9 @@ func setOptionsDefaults(options Options) Options {
543534
}
544535
}
545536

546-
// Allow newClient to be mocked
547-
if options.NewClient == nil {
548-
options.NewClient = DefaultNewClient
537+
// Allow the client builder to be mocked
538+
if options.ClientBuilder == nil {
539+
options.ClientBuilder = NewClientBuilder()
549540
}
550541

551542
// Allow newCache to be mocked

pkg/manager/manager_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ import (
5454
"sigs.k8s.io/controller-runtime/pkg/runtime/inject"
5555
)
5656

57+
type fakeClientBuilder struct {
58+
err error
59+
}
60+
61+
func (e *fakeClientBuilder) WithUncached(objs ...client.Object) ClientBuilder {
62+
return e
63+
}
64+
65+
func (e *fakeClientBuilder) Build(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error) {
66+
return nil, e.err
67+
}
68+
5769
var _ = Describe("manger.Manager", func() {
5870
Describe("New", func() {
5971
It("should return an error if there is no Config", func() {
@@ -75,9 +87,7 @@ var _ = Describe("manger.Manager", func() {
7587

7688
It("should return an error it can't create a client.Client", func(done Done) {
7789
m, err := New(cfg, Options{
78-
NewClient: func(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error) {
79-
return nil, fmt.Errorf("expected error")
80-
},
90+
ClientBuilder: &fakeClientBuilder{err: fmt.Errorf("expected error")},
8191
})
8292
Expect(m).To(BeNil())
8393
Expect(err).To(HaveOccurred())
@@ -101,9 +111,7 @@ var _ = Describe("manger.Manager", func() {
101111

102112
It("should create a client defined in by the new client function", func(done Done) {
103113
m, err := New(cfg, Options{
104-
NewClient: func(cache cache.Cache, config *rest.Config, options client.Options) (client.Client, error) {
105-
return nil, nil
106-
},
114+
ClientBuilder: &fakeClientBuilder{},
107115
})
108116
Expect(m).ToNot(BeNil())
109117
Expect(err).ToNot(HaveOccurred())

pkg/runtime/inject/inject_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ var _ = Describe("runtime inject", func() {
8787
})
8888

8989
It("should set client", func() {
90-
client := client.NewDelegatingClient(client.NewDelegatingClientInput{Client: fake.NewFakeClient()})
90+
client, err := client.NewDelegatingClient(client.NewDelegatingClientInput{Client: fake.NewFakeClient()})
91+
Expect(err).NotTo(HaveOccurred())
9192

9293
By("Validating injecting client")
9394
res, err := ClientInto(client, instance)
@@ -152,7 +153,8 @@ var _ = Describe("runtime inject", func() {
152153
})
153154

154155
It("should set api reader", func() {
155-
apiReader := client.NewDelegatingClient(client.NewDelegatingClientInput{Client: fake.NewFakeClient()})
156+
apiReader, err := client.NewDelegatingClient(client.NewDelegatingClientInput{Client: fake.NewFakeClient()})
157+
Expect(err).NotTo(HaveOccurred())
156158

157159
By("Validating injecting client")
158160
res, err := APIReaderInto(apiReader, instance)

0 commit comments

Comments
 (0)