1
16
17 package dryrun
18
19 import (
20 "context"
21 "testing"
22
23 v1 "k8s.io/api/core/v1"
24 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
25 apierrors "k8s.io/apimachinery/pkg/api/errors"
26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28 "k8s.io/apimachinery/pkg/runtime/schema"
29 "k8s.io/apimachinery/pkg/types"
30 "k8s.io/apimachinery/pkg/util/sets"
31 "k8s.io/client-go/dynamic"
32 "k8s.io/client-go/kubernetes"
33 "k8s.io/client-go/util/retry"
34 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
35 "k8s.io/kubernetes/test/integration/etcd"
36 "k8s.io/kubernetes/test/integration/framework"
37 )
38
39
40
41 var kindAllowList = sets.NewString()
42
43
44 const testNamespace = "dryrunnamespace"
45
46 func DryRunCreateTest(t *testing.T, rsc dynamic.ResourceInterface, obj *unstructured.Unstructured, gvResource schema.GroupVersionResource) {
47 createdObj, err := rsc.Create(context.TODO(), obj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
48 if err != nil {
49 t.Fatalf("failed to dry-run create stub for %s: %#v", gvResource, err)
50 }
51 if obj.GroupVersionKind() != createdObj.GroupVersionKind() {
52 t.Fatalf("created object doesn't have the same gvk as original object: got %v, expected %v",
53 createdObj.GroupVersionKind(),
54 obj.GroupVersionKind())
55 }
56
57 if _, err := rsc.Get(context.TODO(), obj.GetName(), metav1.GetOptions{}); !apierrors.IsNotFound(err) {
58 t.Fatalf("object shouldn't exist: %v", err)
59 }
60 }
61
62 func DryRunPatchTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
63 patch := []byte(`{"metadata":{"annotations":{"patch": "true"}}}`)
64 obj, err := rsc.Patch(context.TODO(), name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}})
65 if err != nil {
66 t.Fatalf("failed to dry-run patch object: %v", err)
67 }
68 if v := obj.GetAnnotations()["patch"]; v != "true" {
69 t.Fatalf("dry-run patched annotations should be returned, got: %v", obj.GetAnnotations())
70 }
71 obj, err = rsc.Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
72 if err != nil {
73 t.Fatalf("failed to get object: %v", err)
74 }
75 if v := obj.GetAnnotations()["patch"]; v == "true" {
76 t.Fatalf("dry-run patched annotations should not be persisted, got: %v", obj.GetAnnotations())
77 }
78 }
79
80 func getReplicasOrFail(t *testing.T, obj *unstructured.Unstructured) int64 {
81 t.Helper()
82 replicas, found, err := unstructured.NestedInt64(obj.UnstructuredContent(), "spec", "replicas")
83 if err != nil {
84 t.Fatalf("failed to get int64 for replicas: %v", err)
85 }
86 if !found {
87 t.Fatal("object doesn't have spec.replicas")
88 }
89 return replicas
90 }
91
92 func DryRunScalePatchTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
93 obj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{}, "scale")
94 if apierrors.IsNotFound(err) {
95 return
96 }
97 if err != nil {
98 t.Fatalf("failed to get object: %v", err)
99 }
100
101 replicas := getReplicasOrFail(t, obj)
102 patch := []byte(`{"spec":{"replicas":10}}`)
103 patchedObj, err := rsc.Patch(context.TODO(), name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}}, "scale")
104 if err != nil {
105 t.Fatalf("failed to dry-run patch object: %v", err)
106 }
107 if newReplicas := getReplicasOrFail(t, patchedObj); newReplicas != 10 {
108 t.Fatalf("dry-run patch to replicas didn't return new value: %v", newReplicas)
109 }
110 persistedObj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{}, "scale")
111 if err != nil {
112 t.Fatalf("failed to get scale sub-resource")
113 }
114 if newReplicas := getReplicasOrFail(t, persistedObj); newReplicas != replicas {
115 t.Fatalf("number of replicas changed, expected %v, got %v", replicas, newReplicas)
116 }
117 }
118
119 func DryRunScaleUpdateTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
120 obj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{}, "scale")
121 if apierrors.IsNotFound(err) {
122 return
123 }
124 if err != nil {
125 t.Fatalf("failed to get object: %v", err)
126 }
127
128 replicas := getReplicasOrFail(t, obj)
129 if err := unstructured.SetNestedField(obj.Object, int64(10), "spec", "replicas"); err != nil {
130 t.Fatalf("failed to set spec.replicas: %v", err)
131 }
132 updatedObj, err := rsc.Update(context.TODO(), obj, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}}, "scale")
133 if err != nil {
134 t.Fatalf("failed to dry-run update scale sub-resource: %v", err)
135 }
136 if newReplicas := getReplicasOrFail(t, updatedObj); newReplicas != 10 {
137 t.Fatalf("dry-run update to replicas didn't return new value: %v", newReplicas)
138 }
139 persistedObj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{}, "scale")
140 if err != nil {
141 t.Fatalf("failed to get scale sub-resource")
142 }
143 if newReplicas := getReplicasOrFail(t, persistedObj); newReplicas != replicas {
144 t.Fatalf("number of replicas changed, expected %v, got %v", replicas, newReplicas)
145 }
146 }
147
148 func DryRunUpdateTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
149 var err error
150 var obj *unstructured.Unstructured
151 err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
152 obj, err = rsc.Get(context.TODO(), name, metav1.GetOptions{})
153 if err != nil {
154 t.Fatalf("failed to retrieve object: %v", err)
155 }
156 obj.SetAnnotations(map[string]string{"update": "true"})
157 obj, err = rsc.Update(context.TODO(), obj, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}})
158 if apierrors.IsConflict(err) {
159 t.Logf("conflict error: %v", err)
160 }
161 return err
162 })
163 if err != nil {
164 t.Fatalf("failed to dry-run update resource: %v", err)
165 }
166 if v := obj.GetAnnotations()["update"]; v != "true" {
167 t.Fatalf("dry-run updated annotations should be returned, got: %v", obj.GetAnnotations())
168 }
169
170 obj, err = rsc.Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
171 if err != nil {
172 t.Fatalf("failed to get object: %v", err)
173 }
174 if v := obj.GetAnnotations()["update"]; v == "true" {
175 t.Fatalf("dry-run updated annotations should not be persisted, got: %v", obj.GetAnnotations())
176 }
177 }
178
179 func DryRunDeleteCollectionTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
180 err := rsc.DeleteCollection(context.TODO(), metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, metav1.ListOptions{})
181 if err != nil {
182 t.Fatalf("dry-run delete collection failed: %v", err)
183 }
184 obj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{})
185 if err != nil {
186 t.Fatalf("failed to get object: %v", err)
187 }
188 ts := obj.GetDeletionTimestamp()
189 if ts != nil {
190 t.Fatalf("object has a deletion timestamp after dry-run delete collection")
191 }
192 }
193
194 func DryRunDeleteTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
195 err := rsc.Delete(context.TODO(), name, metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}})
196 if err != nil {
197 t.Fatalf("dry-run delete failed: %v", err)
198 }
199 obj, err := rsc.Get(context.TODO(), name, metav1.GetOptions{})
200 if err != nil {
201 t.Fatalf("failed to get object: %v", err)
202 }
203 ts := obj.GetDeletionTimestamp()
204 if ts != nil {
205 t.Fatalf("object has a deletion timestamp after dry-run delete")
206 }
207 }
208
209
210 func TestDryRun(t *testing.T) {
211
212
213 s, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
214 "--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection",
215 "--runtime-config=api/all=true",
216 }, framework.SharedEtcd())
217 if err != nil {
218 t.Fatal(err)
219 }
220 defer s.TearDownFn()
221
222 client, err := kubernetes.NewForConfig(s.ClientConfig)
223 if err != nil {
224 t.Fatal(err)
225 }
226 dynamicClient, err := dynamic.NewForConfig(s.ClientConfig)
227 if err != nil {
228 t.Fatal(err)
229 }
230
231
232 etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(s.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
233
234 if _, err := client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}, metav1.CreateOptions{}); err != nil {
235 t.Fatal(err)
236 }
237
238 dryrunData := etcd.GetEtcdStorageData()
239
240
241 for resource, stub := range map[schema.GroupVersionResource]string{
242
243 gvr("", "v1", "events"): `{"involvedObject": {"namespace": "dryrunnamespace"}, "message": "some data here", "metadata": {"name": "event1"}}`,
244 } {
245 data := dryrunData[resource]
246 data.Stub = stub
247 dryrunData[resource] = data
248 }
249
250
251 _, resources, err := client.Discovery().ServerGroupsAndResources()
252 if err != nil {
253 t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
254 }
255
256 for _, resourceToTest := range etcd.GetResources(t, resources) {
257 t.Run(resourceToTest.Mapping.Resource.String(), func(t *testing.T) {
258 mapping := resourceToTest.Mapping
259 gvk := resourceToTest.Mapping.GroupVersionKind
260 gvResource := resourceToTest.Mapping.Resource
261 kind := gvk.Kind
262
263 if kindAllowList.Has(kind) {
264 t.Skip("allowlisted")
265 }
266
267 testData, hasTest := dryrunData[gvResource]
268
269 if !hasTest {
270 t.Fatalf("no test data for %s. Please add a test for your new type to etcd.GetEtcdStorageData().", gvResource)
271 }
272
273 rsc, obj, err := etcd.JSONToUnstructured(testData.Stub, testNamespace, mapping, dynamicClient)
274 if err != nil {
275 t.Fatalf("failed to unmarshal stub (%v): %v", testData.Stub, err)
276 }
277
278 name := obj.GetName()
279
280 DryRunCreateTest(t, rsc, obj, gvResource)
281
282 if _, err := rsc.Create(context.TODO(), obj, metav1.CreateOptions{}); err != nil {
283 t.Fatalf("failed to create stub for %s: %#v", gvResource, err)
284 }
285
286 DryRunUpdateTest(t, rsc, name)
287 DryRunPatchTest(t, rsc, name)
288 DryRunScalePatchTest(t, rsc, name)
289 DryRunScaleUpdateTest(t, rsc, name)
290 if resourceToTest.HasDeleteCollection {
291 DryRunDeleteCollectionTest(t, rsc, name)
292 }
293 DryRunDeleteTest(t, rsc, name)
294
295 if err = rsc.Delete(context.TODO(), obj.GetName(), *metav1.NewDeleteOptions(0)); err != nil {
296 t.Fatalf("deleting final object failed: %v", err)
297 }
298 })
299 }
300 }
301
302 func gvr(g, v, r string) schema.GroupVersionResource {
303 return schema.GroupVersionResource{Group: g, Version: v, Resource: r}
304 }
305
View as plain text