k8s.io/kubernetes@v1.29.3/test/integration/dryrun/dryrun_test.go (about) 1 /* 2 Copyright 2018 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 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 // Only add kinds to this list when this a virtual resource with get and create verbs that doesn't actually 40 // store into it's kind. We've used this downstream for mappings before. 41 var kindAllowList = sets.NewString() 42 43 // namespace used for all tests, do not change this 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 // TestDryRun tests dry-run on all types. 210 func TestDryRun(t *testing.T) { 211 212 // start API server 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 // create CRDs so we can make sure that custom resources do not get lost 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 // dry run specific stub overrides 241 for resource, stub := range map[schema.GroupVersionResource]string{ 242 // need to change event's namespace field to match dry run test 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 // gather resources to test 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 }