Skip to content

Commit 1f7bf8c

Browse files
committed
✨ Add DryRunClient wrapper
1 parent bfc9827 commit 1f7bf8c

File tree

4 files changed

+325
-2
lines changed

4 files changed

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

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)