Skip to content

Commit 32e94b6

Browse files
✨ Add namespace enforcing wrapper for client.Client (kubernetes-sigs#1088)
* This commit adds a namespace enforcing wrapper for client.Client. This helps while dealing with namespace-scoped objects, where the namespace value need not be specified in every operation. * Modify namespaced client to use RestMapper for finding the scope of object. * Address review comments
1 parent bae8fdb commit 32e94b6

File tree

2 files changed

+836
-0
lines changed

2 files changed

+836
-0
lines changed

pkg/client/namespaced_client.go

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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 client
18+
19+
import (
20+
"context"
21+
"errors"
22+
"fmt"
23+
24+
"k8s.io/apimachinery/pkg/api/meta"
25+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
"k8s.io/apimachinery/pkg/runtime/schema"
28+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
29+
)
30+
31+
// NewNamespacedClient wraps an existing client enforcing the namespace value.
32+
// All functions using this client will have the same namespace declared here.
33+
func NewNamespacedClient(c Client, ns string) Client {
34+
return &namespacedClient{
35+
client: c,
36+
namespace: ns,
37+
}
38+
}
39+
40+
var _ Client = &namespacedClient{}
41+
42+
// namespacedClient is a Client that wraps another Client in order to enforce the specified namespace value.
43+
type namespacedClient struct {
44+
namespace string
45+
client Client
46+
}
47+
48+
// Scheme returns the scheme this client is using.
49+
func (n *namespacedClient) Scheme() *runtime.Scheme {
50+
return n.client.Scheme()
51+
}
52+
53+
// RESTMapper returns the scheme this client is using.
54+
func (n *namespacedClient) RESTMapper() meta.RESTMapper {
55+
return n.client.RESTMapper()
56+
}
57+
58+
// isNamespaced returns true if the object is namespace scoped.
59+
// For unstructured objects the gvk is found from the object itself.
60+
func isNamespaced(c Client, obj runtime.Object) (bool, error) {
61+
var gvk schema.GroupVersionKind
62+
var err error
63+
64+
_, isUnstructured := obj.(*unstructured.Unstructured)
65+
_, isUnstructuredList := obj.(*unstructured.UnstructuredList)
66+
67+
isUnstructured = isUnstructured || isUnstructuredList
68+
if isUnstructured {
69+
gvk = obj.GetObjectKind().GroupVersionKind()
70+
} else {
71+
gvk, err = apiutil.GVKForObject(obj, c.Scheme())
72+
if err != nil {
73+
return false, err
74+
}
75+
}
76+
77+
gk := schema.GroupKind{
78+
Group: gvk.Group,
79+
Kind: gvk.Kind,
80+
}
81+
restmapping, err := c.RESTMapper().RESTMapping(gk)
82+
if err != nil {
83+
return false, fmt.Errorf("failed to get restmapping: %w", err)
84+
}
85+
scope := restmapping.Scope.Name()
86+
87+
if scope == "" {
88+
return false, errors.New("Scope cannot be identified. Empty scope returned")
89+
}
90+
91+
if scope != meta.RESTScopeNameRoot {
92+
return true, nil
93+
}
94+
return false, nil
95+
}
96+
97+
// Create implements clinet.Client
98+
func (n *namespacedClient) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
99+
isNamespaceScoped, err := isNamespaced(n.client, obj)
100+
if err != nil {
101+
return fmt.Errorf("error finding the scope of the object: %v", err)
102+
}
103+
104+
objectNamespace := obj.GetNamespace()
105+
if objectNamespace != n.namespace && objectNamespace != "" {
106+
return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace)
107+
}
108+
109+
if isNamespaceScoped && objectNamespace == "" {
110+
obj.SetNamespace(n.namespace)
111+
}
112+
return n.client.Create(ctx, obj, opts...)
113+
}
114+
115+
// Update implements client.Client
116+
func (n *namespacedClient) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
117+
isNamespaceScoped, err := isNamespaced(n.client, obj)
118+
if err != nil {
119+
return fmt.Errorf("error finding the scope of the object: %v", err)
120+
}
121+
122+
objectNamespace := obj.GetNamespace()
123+
if objectNamespace != n.namespace && objectNamespace != "" {
124+
return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace)
125+
}
126+
127+
if isNamespaceScoped && objectNamespace == "" {
128+
obj.SetNamespace(n.namespace)
129+
}
130+
return n.client.Update(ctx, obj, opts...)
131+
}
132+
133+
// Delete implements client.Client
134+
func (n *namespacedClient) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
135+
isNamespaceScoped, err := isNamespaced(n.client, obj)
136+
if err != nil {
137+
return fmt.Errorf("error finding the scope of the object: %v", err)
138+
}
139+
140+
objectNamespace := obj.GetNamespace()
141+
if objectNamespace != n.namespace && objectNamespace != "" {
142+
return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace)
143+
}
144+
145+
if isNamespaceScoped && objectNamespace == "" {
146+
obj.SetNamespace(n.namespace)
147+
}
148+
return n.client.Delete(ctx, obj, opts...)
149+
}
150+
151+
// DeleteAllOf implements client.Client
152+
func (n *namespacedClient) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
153+
isNamespaceScoped, err := isNamespaced(n.client, obj)
154+
if err != nil {
155+
return fmt.Errorf("error finding the scope of the object: %v", err)
156+
}
157+
158+
if isNamespaceScoped {
159+
opts = append(opts, InNamespace(n.namespace))
160+
}
161+
return n.client.DeleteAllOf(ctx, obj, opts...)
162+
}
163+
164+
// Patch implements client.Client
165+
func (n *namespacedClient) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
166+
isNamespaceScoped, err := isNamespaced(n.client, obj)
167+
if err != nil {
168+
return fmt.Errorf("error finding the scope of the object: %v", err)
169+
}
170+
171+
objectNamespace := obj.GetNamespace()
172+
if objectNamespace != n.namespace && objectNamespace != "" {
173+
return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace)
174+
}
175+
176+
if isNamespaceScoped && objectNamespace == "" {
177+
obj.SetNamespace(n.namespace)
178+
}
179+
return n.client.Patch(ctx, obj, patch, opts...)
180+
}
181+
182+
// Get implements client.Client
183+
func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object) error {
184+
isNamespaceScoped, err := isNamespaced(n.client, obj)
185+
if err != nil {
186+
return fmt.Errorf("error finding the scope of the object: %v", err)
187+
}
188+
if isNamespaceScoped {
189+
if key.Namespace != "" && key.Namespace != n.namespace {
190+
return fmt.Errorf("Namespace %s provided for the object %s does not match the namesapce %s on the client", key.Namespace, obj.GetName(), n.namespace)
191+
}
192+
key.Namespace = n.namespace
193+
}
194+
return n.client.Get(ctx, key, obj)
195+
}
196+
197+
// List implements client.Client
198+
func (n *namespacedClient) List(ctx context.Context, obj ObjectList, opts ...ListOption) error {
199+
if n.namespace != "" {
200+
opts = append(opts, InNamespace(n.namespace))
201+
}
202+
return n.client.List(ctx, obj, opts...)
203+
}
204+
205+
// Status implements client.StatusClient
206+
func (n *namespacedClient) Status() StatusWriter {
207+
return &namespacedClientStatusWriter{StatusClient: n.client.Status(), namespace: n.namespace, namespacedclient: n}
208+
}
209+
210+
// ensure namespacedClientStatusWriter implements client.StatusWriter
211+
var _ StatusWriter = &namespacedClientStatusWriter{}
212+
213+
type namespacedClientStatusWriter struct {
214+
StatusClient StatusWriter
215+
namespace string
216+
namespacedclient Client
217+
}
218+
219+
// Update implements client.StatusWriter
220+
func (nsw *namespacedClientStatusWriter) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
221+
isNamespaceScoped, err := isNamespaced(nsw.namespacedclient, obj)
222+
if err != nil {
223+
return fmt.Errorf("error finding the scope of the object: %v", err)
224+
}
225+
226+
objectNamespace := obj.GetNamespace()
227+
if objectNamespace != nsw.namespace && objectNamespace != "" {
228+
return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace)
229+
}
230+
231+
if isNamespaceScoped && objectNamespace == "" {
232+
obj.SetNamespace(nsw.namespace)
233+
}
234+
return nsw.StatusClient.Update(ctx, obj, opts...)
235+
}
236+
237+
// Patch implements client.StatusWriter
238+
func (nsw *namespacedClientStatusWriter) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
239+
isNamespaceScoped, err := isNamespaced(nsw.namespacedclient, obj)
240+
if err != nil {
241+
return fmt.Errorf("error finding the scope of the object: %v", err)
242+
}
243+
244+
objectNamespace := obj.GetNamespace()
245+
if objectNamespace != nsw.namespace && objectNamespace != "" {
246+
return fmt.Errorf("Namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), nsw.namespace)
247+
}
248+
249+
if isNamespaceScoped && objectNamespace == "" {
250+
obj.SetNamespace(nsw.namespace)
251+
}
252+
return nsw.StatusClient.Patch(ctx, obj, patch, opts...)
253+
}

0 commit comments

Comments
 (0)