Skip to content

Commit a1ba84e

Browse files
committed
Implement Apply support in the namespaced client
1 parent 7f46e72 commit a1ba84e

File tree

4 files changed

+149
-14
lines changed

4 files changed

+149
-14
lines changed

pkg/client/applyconfigurations.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ limitations under the License.
1717
package client
1818

1919
import (
20+
"fmt"
21+
2022
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2123
"k8s.io/apimachinery/pkg/runtime"
2224
"k8s.io/apimachinery/pkg/runtime/schema"
25+
"k8s.io/utils/ptr"
2326
)
2427

2528
type unstructuredApplyConfiguration struct {
@@ -57,3 +60,16 @@ func (a *applyconfigurationRuntimeObject) DeepCopyObject() runtime.Object {
5760
func runtimeObjectFromApplyConfiguration(ac runtime.ApplyConfiguration) runtime.Object {
5861
return &applyconfigurationRuntimeObject{ApplyConfiguration: ac}
5962
}
63+
64+
func gvkFromApplyConfiguration(ac applyConfiguration) (schema.GroupVersionKind, error) {
65+
var gvk schema.GroupVersionKind
66+
gv, err := schema.ParseGroupVersion(ptr.Deref(ac.GetAPIVersion(), ""))
67+
if err != nil {
68+
return gvk, fmt.Errorf("failed to parse %q as GroupVersion: %w", ptr.Deref(ac.GetAPIVersion(), ""), err)
69+
}
70+
gvk.Group = gv.Group
71+
gvk.Version = gv.Version
72+
gvk.Kind = ptr.Deref(ac.GetKind(), "")
73+
74+
return gvk, nil
75+
}

pkg/client/client_rest_resources.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,10 @@ func (c *clientRestResources) getResource(obj any) (*resourceMeta, error) {
102102
if !ok {
103103
return nil, fmt.Errorf("%T is a runtime.ApplyConfiguration but not an applyConfiguration", o)
104104
}
105-
gv, err := schema.ParseGroupVersion(ptr.Deref(ac.GetAPIVersion(), ""))
105+
gvk, err = gvkFromApplyConfiguration(ac)
106106
if err != nil {
107-
return nil, fmt.Errorf("failed to parse %q as GroupVersion: %w", ptr.Deref(ac.GetAPIVersion(), ""), err)
107+
return nil, err
108108
}
109-
gvk.Group = gv.Group
110-
gvk.Version = gv.Version
111-
gvk.Kind = ptr.Deref(ac.GetKind(), "")
112109
isApplyConfiguration = true
113110
default:
114111
return nil, fmt.Errorf("bug: %T is neither a runtime.Object nor a runtime.ApplyConfiguration", o)

pkg/client/namespaced_client.go

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ package client
1818

1919
import (
2020
"context"
21-
"errors"
2221
"fmt"
22+
"reflect"
2323

2424
"k8s.io/apimachinery/pkg/api/meta"
2525
"k8s.io/apimachinery/pkg/runtime"
2626
"k8s.io/apimachinery/pkg/runtime/schema"
27+
"k8s.io/utils/ptr"
28+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2729
)
2830

2931
// NewNamespacedClient wraps an existing client enforcing the namespace value.
@@ -149,11 +151,49 @@ func (n *namespacedClient) Patch(ctx context.Context, obj Object, patch Patch, o
149151
}
150152

151153
func (n *namespacedClient) Apply(ctx context.Context, obj runtime.ApplyConfiguration, opts ...ApplyOption) error {
152-
// It is non-trivial to make this work as the current check if an object is namespaces takes a runtime.Object,
153-
// we would need to update that to be able to deal with a runtime.ApplyConfiguration. Additionally, ACs
154-
// do allow setting the namespace through a `WithNamespace(string) T` method, but we would have to use reflect
155-
// due to the `T` return.
156-
return errors.New("Apply is not supported on namespaced client")
154+
var gvk schema.GroupVersionKind
155+
switch o := obj.(type) {
156+
case applyConfiguration:
157+
var err error
158+
gvk, err = gvkFromApplyConfiguration(o)
159+
if err != nil {
160+
return err
161+
}
162+
case *unstructuredApplyConfiguration:
163+
gvk = o.GroupVersionKind()
164+
default:
165+
return fmt.Errorf("object %T is not a valid apply configuration", obj)
166+
}
167+
isNamespaceScoped, err := apiutil.IsGVKNamespaced(gvk, n.RESTMapper())
168+
if err != nil {
169+
return fmt.Errorf("error finding the scope of the object: %w", err)
170+
}
171+
if isNamespaceScoped {
172+
switch o := obj.(type) {
173+
case applyConfiguration:
174+
if o.GetNamespace() != nil && *o.GetNamespace() != "" && *o.GetNamespace() != n.namespace {
175+
return fmt.Errorf("namespace %s provided for the object %s does not match the namespace %s on the client",
176+
*o.GetNamespace(), ptr.Deref(o.GetName(), ""), n.namespace)
177+
}
178+
v := reflect.ValueOf(o)
179+
withNamespace := v.MethodByName("WithNamespace")
180+
if !withNamespace.IsValid() {
181+
return fmt.Errorf("ApplyConfiguration %T does not have a WithNamespace method", o)
182+
}
183+
if tp := withNamespace.Type(); tp.NumIn() != 1 || tp.In(0).Kind() != reflect.String {
184+
return fmt.Errorf("WithNamespace method of ApplyConfiguration %T must take a single string argument", o)
185+
}
186+
withNamespace.Call([]reflect.Value{reflect.ValueOf(n.namespace)})
187+
case *unstructuredApplyConfiguration:
188+
if o.GetNamespace() != "" && o.GetNamespace() != n.namespace {
189+
return fmt.Errorf("namespace %s provided for the object %s does not match the namespace %s on the client",
190+
o.GetNamespace(), o.GetName(), n.namespace)
191+
}
192+
o.SetNamespace(n.namespace)
193+
}
194+
}
195+
196+
return n.client.Apply(ctx, obj, opts...)
157197
}
158198

159199
// Get implements client.Client.

pkg/client/namespaced_client_test.go

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,25 @@ import (
2525
. "github.com/onsi/ginkgo/v2"
2626
. "github.com/onsi/gomega"
2727

28-
rbacv1 "k8s.io/api/rbac/v1"
29-
3028
appsv1 "k8s.io/api/apps/v1"
3129
corev1 "k8s.io/api/core/v1"
30+
rbacv1 "k8s.io/api/rbac/v1"
3231
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3333
"k8s.io/apimachinery/pkg/runtime"
3434
"k8s.io/apimachinery/pkg/runtime/schema"
3535
"k8s.io/apimachinery/pkg/types"
36+
appsv1applyconfigurations "k8s.io/client-go/applyconfigurations/apps/v1"
37+
corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1"
38+
metav1applyconfigurations "k8s.io/client-go/applyconfigurations/meta/v1"
39+
rbacv1applyconfigurations "k8s.io/client-go/applyconfigurations/rbac/v1"
40+
3641
"sigs.k8s.io/controller-runtime/pkg/client"
3742
)
3843

3944
var _ = Describe("NamespacedClient", func() {
4045
var dep *appsv1.Deployment
46+
var acDep *appsv1applyconfigurations.DeploymentApplyConfiguration
4147
var ns = "default"
4248
ctx := context.Background()
4349
var count uint64 = 0
@@ -75,10 +81,25 @@ var _ = Describe("NamespacedClient", func() {
7581
},
7682
},
7783
}
84+
acDep = appsv1applyconfigurations.Deployment(dep.Name, "").
85+
WithLabels(dep.Labels).
86+
WithSpec(appsv1applyconfigurations.DeploymentSpec().
87+
WithReplicas(*dep.Spec.Replicas).
88+
WithSelector(metav1applyconfigurations.LabelSelector().WithMatchLabels(dep.Spec.Selector.MatchLabels)).
89+
WithTemplate(corev1applyconfigurations.PodTemplateSpec().
90+
WithLabels(dep.Spec.Template.Labels).
91+
WithSpec(corev1applyconfigurations.PodSpec().
92+
WithContainers(corev1applyconfigurations.Container().
93+
WithName(dep.Spec.Template.Spec.Containers[0].Name).
94+
WithImage(dep.Spec.Template.Spec.Containers[0].Image),
95+
),
96+
),
97+
),
98+
)
99+
78100
})
79101

80102
Describe("Get", func() {
81-
82103
BeforeEach(func() {
83104
var err error
84105
dep, err = clientset.AppsV1().Deployments(ns).Create(ctx, dep, metav1.CreateOptions{})
@@ -88,6 +109,7 @@ var _ = Describe("NamespacedClient", func() {
88109
AfterEach(func() {
89110
deleteDeployment(ctx, dep, ns)
90111
})
112+
91113
It("should successfully Get a namespace-scoped object", func() {
92114
name := types.NamespacedName{Name: dep.Name}
93115
result := &appsv1.Deployment{}
@@ -135,6 +157,66 @@ var _ = Describe("NamespacedClient", func() {
135157
})
136158
})
137159

160+
Describe("Apply", func() {
161+
AfterEach(func() {
162+
deleteDeployment(ctx, dep, ns)
163+
})
164+
165+
It("should successfully apply an object in the right namespace", func() {
166+
err := getClient().Apply(ctx, acDep, client.FieldOwner("test"))
167+
Expect(err).NotTo(HaveOccurred())
168+
169+
res, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{})
170+
Expect(err).NotTo(HaveOccurred())
171+
Expect(res.GetNamespace()).To(BeEquivalentTo(ns))
172+
})
173+
174+
It("should successfully apply an object in the right namespace through unstructured", func() {
175+
serialized, err := json.Marshal(acDep)
176+
Expect(err).NotTo(HaveOccurred())
177+
u := &unstructured.Unstructured{}
178+
Expect(json.Unmarshal(serialized, &u.Object)).To(Succeed())
179+
err = getClient().Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner("test"))
180+
Expect(err).NotTo(HaveOccurred())
181+
182+
res, err := clientset.AppsV1().Deployments(ns).Get(ctx, dep.Name, metav1.GetOptions{})
183+
Expect(err).NotTo(HaveOccurred())
184+
Expect(res.GetNamespace()).To(BeEquivalentTo(ns))
185+
})
186+
187+
It("should not create an object if the namespace of the object is different", func() {
188+
acDep.WithNamespace("non-default")
189+
err := getClient().Apply(ctx, acDep, client.FieldOwner("test"))
190+
Expect(err).To(HaveOccurred())
191+
Expect(err.Error()).To(ContainSubstring("does not match the namespace"))
192+
})
193+
194+
It("should not create an object through unstructured if the namespace of the object is different", func() {
195+
acDep.WithNamespace("non-default")
196+
serialized, err := json.Marshal(acDep)
197+
Expect(err).NotTo(HaveOccurred())
198+
u := &unstructured.Unstructured{}
199+
Expect(json.Unmarshal(serialized, &u.Object)).To(Succeed())
200+
err = getClient().Apply(ctx, client.ApplyConfigurationFromUnstructured(u), client.FieldOwner("test"))
201+
Expect(err).To(HaveOccurred())
202+
Expect(err.Error()).To(ContainSubstring("does not match the namespace"))
203+
})
204+
205+
It("should create a cluster scoped object", func() {
206+
cr := rbacv1applyconfigurations.ClusterRole(fmt.Sprintf("clusterRole-%v", count))
207+
208+
err := getClient().Apply(ctx, cr, client.FieldOwner("test"))
209+
Expect(err).NotTo(HaveOccurred())
210+
211+
By("checking if the object was created")
212+
res, err := clientset.RbacV1().ClusterRoles().Get(ctx, *cr.Name, metav1.GetOptions{})
213+
Expect(err).NotTo(HaveOccurred())
214+
Expect(res).NotTo(BeNil())
215+
216+
deleteClusterRole(ctx, &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: *cr.Name}})
217+
})
218+
})
219+
138220
Describe("Create", func() {
139221
AfterEach(func() {
140222
deleteDeployment(ctx, dep, ns)

0 commit comments

Comments
 (0)