Skip to content

Commit a7508de

Browse files
authored
Merge pull request #2771 from ahmetb/ahmet/client-with-fieldmanager
✨ client: Add client-wide fieldManager
2 parents cf69be2 + bf2e192 commit a7508de

File tree

2 files changed

+254
-0
lines changed

2 files changed

+254
-0
lines changed

pkg/client/fieldmanager_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
Copyright 2024 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 client_test
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
corev1 "k8s.io/api/core/v1"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
25+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
26+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
27+
)
28+
29+
func TestWithFieldOwner(t *testing.T) {
30+
calls := 0
31+
fakeClient := testClient(t, "custom-field-mgr", func() { calls++ })
32+
wrappedClient := client.WithFieldOwner(fakeClient, "custom-field-mgr")
33+
34+
ctx := context.Background()
35+
dummyObj := &corev1.Namespace{}
36+
37+
_ = wrappedClient.Create(ctx, dummyObj)
38+
_ = wrappedClient.Update(ctx, dummyObj)
39+
_ = wrappedClient.Patch(ctx, dummyObj, nil)
40+
_ = wrappedClient.Status().Create(ctx, dummyObj, dummyObj)
41+
_ = wrappedClient.Status().Update(ctx, dummyObj)
42+
_ = wrappedClient.Status().Patch(ctx, dummyObj, nil)
43+
_ = wrappedClient.SubResource("some-subresource").Create(ctx, dummyObj, dummyObj)
44+
_ = wrappedClient.SubResource("some-subresource").Update(ctx, dummyObj)
45+
_ = wrappedClient.SubResource("some-subresource").Patch(ctx, dummyObj, nil)
46+
47+
if expectedCalls := 9; calls != expectedCalls {
48+
t.Fatalf("wrong number of calls to assertions: expected=%d; got=%d", expectedCalls, calls)
49+
}
50+
}
51+
52+
func TestWithFieldOwnerOverridden(t *testing.T) {
53+
calls := 0
54+
55+
fakeClient := testClient(t, "new-field-manager", func() { calls++ })
56+
wrappedClient := client.WithFieldOwner(fakeClient, "old-field-manager")
57+
58+
ctx := context.Background()
59+
dummyObj := &corev1.Namespace{}
60+
61+
_ = wrappedClient.Create(ctx, dummyObj, client.FieldOwner("new-field-manager"))
62+
_ = wrappedClient.Update(ctx, dummyObj, client.FieldOwner("new-field-manager"))
63+
_ = wrappedClient.Patch(ctx, dummyObj, nil, client.FieldOwner("new-field-manager"))
64+
_ = wrappedClient.Status().Create(ctx, dummyObj, dummyObj, client.FieldOwner("new-field-manager"))
65+
_ = wrappedClient.Status().Update(ctx, dummyObj, client.FieldOwner("new-field-manager"))
66+
_ = wrappedClient.Status().Patch(ctx, dummyObj, nil, client.FieldOwner("new-field-manager"))
67+
_ = wrappedClient.SubResource("some-subresource").Create(ctx, dummyObj, dummyObj, client.FieldOwner("new-field-manager"))
68+
_ = wrappedClient.SubResource("some-subresource").Update(ctx, dummyObj, client.FieldOwner("new-field-manager"))
69+
_ = wrappedClient.SubResource("some-subresource").Patch(ctx, dummyObj, nil, client.FieldOwner("new-field-manager"))
70+
71+
if expectedCalls := 9; calls != expectedCalls {
72+
t.Fatalf("wrong number of calls to assertions: expected=%d; got=%d", expectedCalls, calls)
73+
}
74+
}
75+
76+
// testClient is a helper function that checks if calls have the expected field manager,
77+
// and calls the callback function on each intercepted call.
78+
func testClient(t *testing.T, expectedFieldManager string, callback func()) client.Client {
79+
// TODO: we could use the dummyClient in interceptor pkg if we move it to an internal pkg
80+
return fake.NewClientBuilder().WithInterceptorFuncs(interceptor.Funcs{
81+
Create: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
82+
callback()
83+
out := &client.CreateOptions{}
84+
for _, f := range opts {
85+
f.ApplyToCreate(out)
86+
}
87+
if got := out.FieldManager; expectedFieldManager != got {
88+
t.Fatalf("wrong field manager: expected=%q; got=%q", expectedFieldManager, got)
89+
}
90+
return nil
91+
},
92+
Update: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.UpdateOption) error {
93+
callback()
94+
out := &client.UpdateOptions{}
95+
for _, f := range opts {
96+
f.ApplyToUpdate(out)
97+
}
98+
if got := out.FieldManager; expectedFieldManager != got {
99+
t.Fatalf("wrong field manager: expected=%q; got=%q", expectedFieldManager, got)
100+
}
101+
return nil
102+
},
103+
Patch: func(ctx context.Context, c client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
104+
callback()
105+
out := &client.PatchOptions{}
106+
for _, f := range opts {
107+
f.ApplyToPatch(out)
108+
}
109+
if got := out.FieldManager; expectedFieldManager != got {
110+
t.Fatalf("wrong field manager: expected=%q; got=%q", expectedFieldManager, got)
111+
}
112+
return nil
113+
},
114+
SubResourceCreate: func(ctx context.Context, c client.Client, subResourceName string, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
115+
callback()
116+
out := &client.SubResourceCreateOptions{}
117+
for _, f := range opts {
118+
f.ApplyToSubResourceCreate(out)
119+
}
120+
if got := out.FieldManager; expectedFieldManager != got {
121+
t.Fatalf("wrong field manager: expected=%q; got=%q", expectedFieldManager, got)
122+
}
123+
return nil
124+
},
125+
SubResourceUpdate: func(ctx context.Context, c client.Client, subResourceName string, obj client.Object, opts ...client.SubResourceUpdateOption) error {
126+
callback()
127+
out := &client.SubResourceUpdateOptions{}
128+
for _, f := range opts {
129+
f.ApplyToSubResourceUpdate(out)
130+
}
131+
if got := out.FieldManager; expectedFieldManager != got {
132+
t.Fatalf("wrong field manager: expected=%q; got=%q", expectedFieldManager, got)
133+
}
134+
return nil
135+
},
136+
SubResourcePatch: func(ctx context.Context, c client.Client, subResourceName string, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
137+
callback()
138+
out := &client.SubResourcePatchOptions{}
139+
for _, f := range opts {
140+
f.ApplyToSubResourcePatch(out)
141+
}
142+
if got := out.FieldManager; expectedFieldManager != got {
143+
t.Fatalf("wrong field manager: expected=%q; got=%q", expectedFieldManager, got)
144+
}
145+
return nil
146+
},
147+
}).Build()
148+
}

pkg/client/fieldowner.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
Copyright 2024 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 client
18+
19+
import (
20+
"context"
21+
22+
"k8s.io/apimachinery/pkg/api/meta"
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
25+
)
26+
27+
// WithFieldOwner wraps a Client and adds the fieldOwner as the field
28+
// manager to all write requests from this client. If additional [FieldOwner]
29+
// options are specified on methods of this client, the value specified here
30+
// will be overridden.
31+
func WithFieldOwner(c Client, fieldOwner string) Client {
32+
return &clientWithFieldManager{
33+
manager: fieldOwner,
34+
c: c,
35+
Reader: c,
36+
}
37+
}
38+
39+
type clientWithFieldManager struct {
40+
manager string
41+
c Client
42+
Reader
43+
}
44+
45+
func (f *clientWithFieldManager) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
46+
return f.c.Create(ctx, obj, append([]CreateOption{FieldOwner(f.manager)}, opts...)...)
47+
}
48+
49+
func (f *clientWithFieldManager) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
50+
return f.c.Update(ctx, obj, append([]UpdateOption{FieldOwner(f.manager)}, opts...)...)
51+
}
52+
53+
func (f *clientWithFieldManager) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
54+
return f.c.Patch(ctx, obj, patch, append([]PatchOption{FieldOwner(f.manager)}, opts...)...)
55+
}
56+
57+
func (f *clientWithFieldManager) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
58+
return f.c.Delete(ctx, obj, opts...)
59+
}
60+
61+
func (f *clientWithFieldManager) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
62+
return f.c.DeleteAllOf(ctx, obj, opts...)
63+
}
64+
65+
func (f *clientWithFieldManager) Scheme() *runtime.Scheme { return f.c.Scheme() }
66+
func (f *clientWithFieldManager) RESTMapper() meta.RESTMapper { return f.c.RESTMapper() }
67+
func (f *clientWithFieldManager) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
68+
return f.c.GroupVersionKindFor(obj)
69+
}
70+
func (f *clientWithFieldManager) IsObjectNamespaced(obj runtime.Object) (bool, error) {
71+
return f.c.IsObjectNamespaced(obj)
72+
}
73+
74+
func (f *clientWithFieldManager) Status() StatusWriter {
75+
return &subresourceClientWithFieldOwner{
76+
owner: f.manager,
77+
subresourceWriter: f.c.Status(),
78+
}
79+
}
80+
81+
func (f *clientWithFieldManager) SubResource(subresource string) SubResourceClient {
82+
c := f.c.SubResource(subresource)
83+
return &subresourceClientWithFieldOwner{
84+
owner: f.manager,
85+
subresourceWriter: c,
86+
SubResourceReader: c,
87+
}
88+
}
89+
90+
type subresourceClientWithFieldOwner struct {
91+
owner string
92+
subresourceWriter SubResourceWriter
93+
SubResourceReader
94+
}
95+
96+
func (f *subresourceClientWithFieldOwner) Create(ctx context.Context, obj Object, subresource Object, opts ...SubResourceCreateOption) error {
97+
return f.subresourceWriter.Create(ctx, obj, subresource, append([]SubResourceCreateOption{FieldOwner(f.owner)}, opts...)...)
98+
}
99+
100+
func (f *subresourceClientWithFieldOwner) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
101+
return f.subresourceWriter.Update(ctx, obj, append([]SubResourceUpdateOption{FieldOwner(f.owner)}, opts...)...)
102+
}
103+
104+
func (f *subresourceClientWithFieldOwner) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
105+
return f.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{FieldOwner(f.owner)}, opts...)...)
106+
}

0 commit comments

Comments
 (0)