Skip to content

Commit 71fe9fd

Browse files
committed
✨ Add DryRunClient wrapper
1 parent bfc9827 commit 71fe9fd

File tree

4 files changed

+364
-2
lines changed

4 files changed

+364
-2
lines changed

pkg/client/client_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ import (
3333
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3434
"k8s.io/apimachinery/pkg/runtime"
3535
"k8s.io/apimachinery/pkg/runtime/schema"
36-
"sigs.k8s.io/controller-runtime/pkg/client"
37-
3836
kscheme "k8s.io/client-go/kubernetes/scheme"
37+
38+
"sigs.k8s.io/controller-runtime/pkg/client"
3939
)
4040

4141
const serverSideTimeoutSeconds = 10

pkg/client/dryrun.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
22+
"k8s.io/apimachinery/pkg/runtime"
23+
)
24+
25+
// NewDryRunClient wraps an existing client and enforces DryRun mode
26+
// on all mutating api calls.
27+
func NewDryRunClient(c Client) Client {
28+
return &dryRunClient{client: c}
29+
}
30+
31+
var _ Client = &dryRunClient{}
32+
33+
// dryRunClient is a Client that wraps another Client in order to enforce DryRun mode.
34+
type dryRunClient struct {
35+
client Client
36+
}
37+
38+
// Create implements client.Client
39+
func (c *dryRunClient) Create(ctx context.Context, obj runtime.Object, opts ...CreateOption) error {
40+
return c.client.Create(ctx, obj, append(opts, DryRunAll)...)
41+
}
42+
43+
// Update implements client.Client
44+
func (c *dryRunClient) Update(ctx context.Context, obj runtime.Object, opts ...UpdateOption) error {
45+
return c.client.Update(ctx, obj, append(opts, DryRunAll)...)
46+
}
47+
48+
// Delete implements client.Client
49+
func (c *dryRunClient) Delete(ctx context.Context, obj runtime.Object, opts ...DeleteOption) error {
50+
return c.client.Delete(ctx, obj, append(opts, DryRunAll)...)
51+
}
52+
53+
// DeleteAllOf implements client.Client
54+
func (c *dryRunClient) DeleteAllOf(ctx context.Context, obj runtime.Object, opts ...DeleteAllOfOption) error {
55+
return c.client.DeleteAllOf(ctx, obj, append(opts, DryRunAll)...)
56+
}
57+
58+
// Patch implements client.Client
59+
func (c *dryRunClient) Patch(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOption) error {
60+
return c.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...)
61+
}
62+
63+
// Get implements client.Client
64+
func (c *dryRunClient) Get(ctx context.Context, key ObjectKey, obj runtime.Object) error {
65+
return c.client.Get(ctx, key, obj)
66+
}
67+
68+
// List implements client.Client
69+
func (c *dryRunClient) List(ctx context.Context, obj runtime.Object, opts ...ListOption) error {
70+
return c.client.List(ctx, obj, opts...)
71+
}
72+
73+
// Status implements client.StatusClient
74+
func (c *dryRunClient) Status() StatusWriter {
75+
return &dryRunStatusWriter{client: c.client.Status()}
76+
}
77+
78+
// ensure dryRunStatusWriter implements client.StatusWriter
79+
var _ StatusWriter = &dryRunStatusWriter{}
80+
81+
// dryRunStatusWriter is client.StatusWriter that writes status subresource with dryRun mode
82+
// enforced.
83+
type dryRunStatusWriter struct {
84+
client StatusWriter
85+
}
86+
87+
// Update implements client.StatusWriter
88+
func (sw *dryRunStatusWriter) Update(ctx context.Context, obj runtime.Object, opts ...UpdateOption) error {
89+
return sw.client.Update(ctx, obj, append(opts, DryRunAll)...)
90+
}
91+
92+
// Patch implements client.StatusWriter
93+
func (sw *dryRunStatusWriter) Patch(ctx context.Context, obj runtime.Object, patch Patch, opts ...PatchOption) error {
94+
return sw.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...)
95+
}

pkg/client/dryrun_test.go

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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_test
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"sync/atomic"
23+
24+
. "github.com/onsi/ginkgo"
25+
. "github.com/onsi/gomega"
26+
appsv1 "k8s.io/api/apps/v1"
27+
corev1 "k8s.io/api/core/v1"
28+
apierrors "k8s.io/apimachinery/pkg/api/errors"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/types"
31+
32+
"sigs.k8s.io/controller-runtime/pkg/client"
33+
)
34+
35+
var _ = Describe("DryRunClient", func() {
36+
var dep *appsv1.Deployment
37+
var count uint64 = 0
38+
var replicaCount int32 = 2
39+
var ns = "default"
40+
ctx := context.Background()
41+
42+
getClient := func() client.Client {
43+
nonDryRunClient, err := client.New(cfg, client.Options{})
44+
Expect(err).NotTo(HaveOccurred())
45+
Expect(nonDryRunClient).NotTo(BeNil())
46+
return client.NewDryRunClient(nonDryRunClient)
47+
}
48+
49+
BeforeEach(func() {
50+
atomic.AddUint64(&count, 1)
51+
dep = &appsv1.Deployment{
52+
ObjectMeta: metav1.ObjectMeta{
53+
Name: fmt.Sprintf("dry-run-deployment-%v", count),
54+
Namespace: ns,
55+
Labels: map[string]string{"name": fmt.Sprintf("dry-run-deployment-%v", count)},
56+
},
57+
Spec: appsv1.DeploymentSpec{
58+
Replicas: &replicaCount,
59+
Selector: &metav1.LabelSelector{
60+
MatchLabels: map[string]string{"foo": "bar"},
61+
},
62+
Template: corev1.PodTemplateSpec{
63+
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}},
64+
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "nginx", Image: "nginx"}}},
65+
},
66+
},
67+
}
68+
69+
var err error
70+
dep, err = clientset.AppsV1().Deployments(ns).Create(dep)
71+
Expect(err).NotTo(HaveOccurred())
72+
})
73+
74+
AfterEach(func() {
75+
deleteDeployment(dep, ns)
76+
})
77+
78+
It("should successfully Get an object", func() {
79+
name := types.NamespacedName{Namespace: ns, Name: dep.Name}
80+
result := &appsv1.Deployment{}
81+
82+
Expect(getClient().Get(ctx, name, result)).NotTo(HaveOccurred())
83+
Expect(result).To(BeEquivalentTo(dep))
84+
})
85+
86+
It("should successfully List objects", func() {
87+
result := &appsv1.DeploymentList{}
88+
opts := client.MatchingLabels(dep.Labels)
89+
90+
Expect(getClient().List(ctx, result, opts)).NotTo(HaveOccurred())
91+
92+
Expect(len(result.Items)).To(BeEquivalentTo(1))
93+
Expect(result.Items[0]).To(BeEquivalentTo(*dep))
94+
})
95+
96+
It("should not create an object", func() {
97+
newDep := dep.DeepCopy()
98+
newDep.Name = "new-deployment"
99+
100+
Expect(getClient().Create(ctx, newDep)).ToNot(HaveOccurred())
101+
102+
_, err := clientset.AppsV1().Deployments(ns).Get(newDep.Name, metav1.GetOptions{})
103+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
104+
})
105+
106+
It("should not create an object with opts", func() {
107+
newDep := dep.DeepCopy()
108+
newDep.Name = "new-deployment"
109+
opts := &client.CreateOptions{DryRun: []string{"Bye", "Pippa"}}
110+
111+
Expect(getClient().Create(ctx, newDep, opts)).ToNot(HaveOccurred())
112+
113+
_, err := clientset.AppsV1().Deployments(ns).Get(newDep.Name, metav1.GetOptions{})
114+
Expect(apierrors.IsNotFound(err)).To(BeTrue())
115+
})
116+
117+
It("should refuse a create request for an invalid object", func() {
118+
changedDep := dep.DeepCopy()
119+
changedDep.Spec.Template.Spec.Containers = nil
120+
121+
err := getClient().Create(ctx, changedDep)
122+
Expect(apierrors.IsInvalid(err)).To(BeTrue())
123+
})
124+
125+
It("should not change objects via update", func() {
126+
changedDep := dep.DeepCopy()
127+
*changedDep.Spec.Replicas = 2
128+
129+
Expect(getClient().Update(ctx, changedDep)).ToNot(HaveOccurred())
130+
131+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
132+
Expect(err).NotTo(HaveOccurred())
133+
Expect(actual).NotTo(BeNil())
134+
Expect(actual).To(BeEquivalentTo(dep))
135+
})
136+
137+
It("should not change objects via update with opts", func() {
138+
changedDep := dep.DeepCopy()
139+
*changedDep.Spec.Replicas = 2
140+
opts := &client.UpdateOptions{DryRun: []string{"Bye", "Pippa"}}
141+
142+
Expect(getClient().Update(ctx, changedDep, opts)).ToNot(HaveOccurred())
143+
144+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
145+
Expect(err).NotTo(HaveOccurred())
146+
Expect(actual).NotTo(BeNil())
147+
Expect(actual).To(BeEquivalentTo(dep))
148+
})
149+
150+
It("should refuse an update request for an invalid change", func() {
151+
changedDep := dep.DeepCopy()
152+
changedDep.Spec.Template.Spec.Containers = nil
153+
154+
err := getClient().Update(ctx, changedDep)
155+
Expect(apierrors.IsInvalid(err)).To(BeTrue())
156+
})
157+
158+
It("should not change objects via patch", func() {
159+
changedDep := dep.DeepCopy()
160+
*changedDep.Spec.Replicas = 2
161+
162+
Expect(getClient().Patch(ctx, changedDep, client.MergeFrom(dep))).ToNot(HaveOccurred())
163+
164+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
165+
Expect(err).NotTo(HaveOccurred())
166+
Expect(actual).NotTo(BeNil())
167+
Expect(actual).To(BeEquivalentTo(dep))
168+
})
169+
170+
It("should not change objects via patch with opts", func() {
171+
changedDep := dep.DeepCopy()
172+
*changedDep.Spec.Replicas = 2
173+
opts := &client.PatchOptions{DryRun: []string{"Bye", "Pippa"}}
174+
175+
Expect(getClient().Patch(ctx, changedDep, client.MergeFrom(dep), opts)).ToNot(HaveOccurred())
176+
177+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
178+
Expect(err).NotTo(HaveOccurred())
179+
Expect(actual).NotTo(BeNil())
180+
Expect(actual).To(BeEquivalentTo(dep))
181+
})
182+
183+
It("should not delete objects", func() {
184+
Expect(getClient().Delete(ctx, dep)).NotTo(HaveOccurred())
185+
186+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
187+
Expect(err).NotTo(HaveOccurred())
188+
Expect(actual).NotTo(BeNil())
189+
Expect(actual).To(BeEquivalentTo(dep))
190+
})
191+
192+
It("should not delete objects with opts", func() {
193+
opts := &client.DeleteOptions{DryRun: []string{"Bye", "Pippa"}}
194+
195+
Expect(getClient().Delete(ctx, dep, opts)).NotTo(HaveOccurred())
196+
197+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
198+
Expect(err).NotTo(HaveOccurred())
199+
Expect(actual).NotTo(BeNil())
200+
Expect(actual).To(BeEquivalentTo(dep))
201+
})
202+
203+
It("should not delete objects via deleteAllOf", func() {
204+
opts := []client.DeleteAllOfOption{client.InNamespace(ns), client.MatchingLabels(dep.Labels)}
205+
206+
Expect(getClient().DeleteAllOf(ctx, dep, opts...)).NotTo(HaveOccurred())
207+
208+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
209+
Expect(err).NotTo(HaveOccurred())
210+
Expect(actual).NotTo(BeNil())
211+
Expect(actual).To(BeEquivalentTo(dep))
212+
})
213+
214+
It("should not change objects via update status", func() {
215+
changedDep := dep.DeepCopy()
216+
changedDep.Status.Replicas = 99
217+
218+
Expect(getClient().Status().Update(ctx, changedDep)).NotTo(HaveOccurred())
219+
220+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
221+
Expect(err).NotTo(HaveOccurred())
222+
Expect(actual).NotTo(BeNil())
223+
Expect(actual).To(BeEquivalentTo(dep))
224+
})
225+
226+
It("should not change objects via update status with opts", func() {
227+
changedDep := dep.DeepCopy()
228+
changedDep.Status.Replicas = 99
229+
opts := &client.UpdateOptions{DryRun: []string{"Bye", "Pippa"}}
230+
231+
Expect(getClient().Status().Update(ctx, changedDep, opts)).NotTo(HaveOccurred())
232+
233+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
234+
Expect(err).NotTo(HaveOccurred())
235+
Expect(actual).NotTo(BeNil())
236+
Expect(actual).To(BeEquivalentTo(dep))
237+
})
238+
239+
It("should not change objects via status patch", func() {
240+
changedDep := dep.DeepCopy()
241+
changedDep.Status.Replicas = 99
242+
243+
Expect(getClient().Status().Patch(ctx, changedDep, client.MergeFrom(dep))).ToNot(HaveOccurred())
244+
245+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
246+
Expect(err).NotTo(HaveOccurred())
247+
Expect(actual).NotTo(BeNil())
248+
Expect(actual).To(BeEquivalentTo(dep))
249+
})
250+
251+
It("should not change objects via status patch with opts", func() {
252+
changedDep := dep.DeepCopy()
253+
changedDep.Status.Replicas = 99
254+
255+
opts := &client.PatchOptions{DryRun: []string{"Bye", "Pippa"}}
256+
257+
Expect(getClient().Status().Patch(ctx, changedDep, client.MergeFrom(dep), opts)).ToNot(HaveOccurred())
258+
259+
actual, err := clientset.AppsV1().Deployments(ns).Get(dep.Name, metav1.GetOptions{})
260+
Expect(err).NotTo(HaveOccurred())
261+
Expect(actual).NotTo(BeNil())
262+
Expect(actual).To(BeEquivalentTo(dep))
263+
})
264+
})

pkg/client/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ func (dryRunAll) ApplyToPatch(opts *PatchOptions) {
8383
func (dryRunAll) ApplyToDelete(opts *DeleteOptions) {
8484
opts.DryRun = []string{metav1.DryRunAll}
8585
}
86+
func (dryRunAll) ApplyToDeleteAllOf(opts *DeleteAllOfOptions) {
87+
opts.DryRun = []string{metav1.DryRunAll}
88+
}
8689

8790
// FieldOwner set the field manager name for the given server-side apply patch.
8891
type FieldOwner string

0 commit comments

Comments
 (0)