dryrun_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. /*
  2. Copyright 2018 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package dryrun
  14. import (
  15. "testing"
  16. v1 "k8s.io/api/core/v1"
  17. apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
  18. "k8s.io/apimachinery/pkg/api/errors"
  19. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  20. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  21. "k8s.io/apimachinery/pkg/runtime/schema"
  22. "k8s.io/apimachinery/pkg/types"
  23. "k8s.io/apimachinery/pkg/util/sets"
  24. "k8s.io/apiserver/pkg/features"
  25. utilfeature "k8s.io/apiserver/pkg/util/feature"
  26. "k8s.io/client-go/dynamic"
  27. "k8s.io/client-go/kubernetes"
  28. featuregatetesting "k8s.io/component-base/featuregate/testing"
  29. kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
  30. "k8s.io/kubernetes/test/integration/etcd"
  31. "k8s.io/kubernetes/test/integration/framework"
  32. )
  33. // Only add kinds to this list when this a virtual resource with get and create verbs that doesn't actually
  34. // store into it's kind. We've used this downstream for mappings before.
  35. var kindWhiteList = sets.NewString()
  36. // namespace used for all tests, do not change this
  37. const testNamespace = "dryrunnamespace"
  38. func DryRunCreateTest(t *testing.T, rsc dynamic.ResourceInterface, obj *unstructured.Unstructured, gvResource schema.GroupVersionResource) {
  39. createdObj, err := rsc.Create(obj, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
  40. if err != nil {
  41. t.Fatalf("failed to dry-run create stub for %s: %#v", gvResource, err)
  42. }
  43. if obj.GroupVersionKind() != createdObj.GroupVersionKind() {
  44. t.Fatalf("created object doesn't have the same gvk as original object: got %v, expected %v",
  45. createdObj.GroupVersionKind(),
  46. obj.GroupVersionKind())
  47. }
  48. if _, err := rsc.Get(obj.GetName(), metav1.GetOptions{}); !errors.IsNotFound(err) {
  49. t.Fatalf("object shouldn't exist: %v", err)
  50. }
  51. }
  52. func DryRunPatchTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
  53. patch := []byte(`{"metadata":{"annotations":{"patch": "true"}}}`)
  54. obj, err := rsc.Patch(name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}})
  55. if err != nil {
  56. t.Fatalf("failed to dry-run patch object: %v", err)
  57. }
  58. if v := obj.GetAnnotations()["patch"]; v != "true" {
  59. t.Fatalf("dry-run patched annotations should be returned, got: %v", obj.GetAnnotations())
  60. }
  61. obj, err = rsc.Get(obj.GetName(), metav1.GetOptions{})
  62. if err != nil {
  63. t.Fatalf("failed to get object: %v", err)
  64. }
  65. if v := obj.GetAnnotations()["patch"]; v == "true" {
  66. t.Fatalf("dry-run patched annotations should not be persisted, got: %v", obj.GetAnnotations())
  67. }
  68. }
  69. func getReplicasOrFail(t *testing.T, obj *unstructured.Unstructured) int64 {
  70. t.Helper()
  71. replicas, found, err := unstructured.NestedInt64(obj.UnstructuredContent(), "spec", "replicas")
  72. if err != nil {
  73. t.Fatalf("failed to get int64 for replicas: %v", err)
  74. }
  75. if !found {
  76. t.Fatal("object doesn't have spec.replicas")
  77. }
  78. return replicas
  79. }
  80. func DryRunScalePatchTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
  81. obj, err := rsc.Get(name, metav1.GetOptions{}, "scale")
  82. if errors.IsNotFound(err) {
  83. return
  84. }
  85. if err != nil {
  86. t.Fatalf("failed to get object: %v", err)
  87. }
  88. replicas := getReplicasOrFail(t, obj)
  89. patch := []byte(`{"spec":{"replicas":10}}`)
  90. patchedObj, err := rsc.Patch(name, types.MergePatchType, patch, metav1.PatchOptions{DryRun: []string{metav1.DryRunAll}}, "scale")
  91. if err != nil {
  92. t.Fatalf("failed to dry-run patch object: %v", err)
  93. }
  94. if newReplicas := getReplicasOrFail(t, patchedObj); newReplicas != 10 {
  95. t.Fatalf("dry-run patch to replicas didn't return new value: %v", newReplicas)
  96. }
  97. persistedObj, err := rsc.Get(name, metav1.GetOptions{}, "scale")
  98. if err != nil {
  99. t.Fatalf("failed to get scale sub-resource")
  100. }
  101. if newReplicas := getReplicasOrFail(t, persistedObj); newReplicas != replicas {
  102. t.Fatalf("number of replicas changed, expected %v, got %v", replicas, newReplicas)
  103. }
  104. }
  105. func DryRunScaleUpdateTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
  106. obj, err := rsc.Get(name, metav1.GetOptions{}, "scale")
  107. if errors.IsNotFound(err) {
  108. return
  109. }
  110. if err != nil {
  111. t.Fatalf("failed to get object: %v", err)
  112. }
  113. replicas := getReplicasOrFail(t, obj)
  114. if err := unstructured.SetNestedField(obj.Object, int64(10), "spec", "replicas"); err != nil {
  115. t.Fatalf("failed to set spec.replicas: %v", err)
  116. }
  117. updatedObj, err := rsc.Update(obj, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}}, "scale")
  118. if err != nil {
  119. t.Fatalf("failed to dry-run update scale sub-resource: %v", err)
  120. }
  121. if newReplicas := getReplicasOrFail(t, updatedObj); newReplicas != 10 {
  122. t.Fatalf("dry-run update to replicas didn't return new value: %v", newReplicas)
  123. }
  124. persistedObj, err := rsc.Get(name, metav1.GetOptions{}, "scale")
  125. if err != nil {
  126. t.Fatalf("failed to get scale sub-resource")
  127. }
  128. if newReplicas := getReplicasOrFail(t, persistedObj); newReplicas != replicas {
  129. t.Fatalf("number of replicas changed, expected %v, got %v", replicas, newReplicas)
  130. }
  131. }
  132. func DryRunUpdateTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
  133. var err error
  134. var obj *unstructured.Unstructured
  135. for i := 0; i < 3; i++ {
  136. obj, err = rsc.Get(name, metav1.GetOptions{})
  137. if err != nil {
  138. t.Fatalf("failed to retrieve object: %v", err)
  139. }
  140. obj.SetAnnotations(map[string]string{"update": "true"})
  141. obj, err = rsc.Update(obj, metav1.UpdateOptions{DryRun: []string{metav1.DryRunAll}})
  142. if err == nil || !errors.IsConflict(err) {
  143. break
  144. }
  145. }
  146. if err != nil {
  147. t.Fatalf("failed to dry-run update resource: %v", err)
  148. }
  149. if v := obj.GetAnnotations()["update"]; v != "true" {
  150. t.Fatalf("dry-run updated annotations should be returned, got: %v", obj.GetAnnotations())
  151. }
  152. obj, err = rsc.Get(obj.GetName(), metav1.GetOptions{})
  153. if err != nil {
  154. t.Fatalf("failed to get object: %v", err)
  155. }
  156. if v := obj.GetAnnotations()["update"]; v == "true" {
  157. t.Fatalf("dry-run updated annotations should not be persisted, got: %v", obj.GetAnnotations())
  158. }
  159. }
  160. func DryRunDeleteCollectionTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
  161. err := rsc.DeleteCollection(&metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}}, metav1.ListOptions{})
  162. if err != nil {
  163. t.Fatalf("dry-run delete collection failed: %v", err)
  164. }
  165. obj, err := rsc.Get(name, metav1.GetOptions{})
  166. if err != nil {
  167. t.Fatalf("failed to get object: %v", err)
  168. }
  169. ts := obj.GetDeletionTimestamp()
  170. if ts != nil {
  171. t.Fatalf("object has a deletion timestamp after dry-run delete collection")
  172. }
  173. }
  174. func DryRunDeleteTest(t *testing.T, rsc dynamic.ResourceInterface, name string) {
  175. err := rsc.Delete(name, &metav1.DeleteOptions{DryRun: []string{metav1.DryRunAll}})
  176. if err != nil {
  177. t.Fatalf("dry-run delete failed: %v", err)
  178. }
  179. obj, err := rsc.Get(name, metav1.GetOptions{})
  180. if err != nil {
  181. t.Fatalf("failed to get object: %v", err)
  182. }
  183. ts := obj.GetDeletionTimestamp()
  184. if ts != nil {
  185. t.Fatalf("object has a deletion timestamp after dry-run delete")
  186. }
  187. }
  188. // TestDryRun tests dry-run on all types.
  189. func TestDryRun(t *testing.T) {
  190. defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DryRun, true)()
  191. // start API server
  192. s, err := kubeapiservertesting.StartTestServer(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
  193. "--disable-admission-plugins=ServiceAccount,StorageObjectInUseProtection",
  194. "--runtime-config=extensions/v1beta1/deployments=true,extensions/v1beta1/daemonsets=true,extensions/v1beta1/replicasets=true,extensions/v1beta1/podsecuritypolicies=true,extensions/v1beta1/networkpolicies=true",
  195. }, framework.SharedEtcd())
  196. if err != nil {
  197. t.Fatal(err)
  198. }
  199. defer s.TearDownFn()
  200. client, err := kubernetes.NewForConfig(s.ClientConfig)
  201. if err != nil {
  202. t.Fatal(err)
  203. }
  204. dynamicClient, err := dynamic.NewForConfig(s.ClientConfig)
  205. if err != nil {
  206. t.Fatal(err)
  207. }
  208. // create CRDs so we can make sure that custom resources do not get lost
  209. etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(s.ClientConfig), false, etcd.GetCustomResourceDefinitionData()...)
  210. if _, err := client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNamespace}}); err != nil {
  211. t.Fatal(err)
  212. }
  213. dryrunData := etcd.GetEtcdStorageData()
  214. // dry run specific stub overrides
  215. for resource, stub := range map[schema.GroupVersionResource]string{
  216. // need to change event's namespace field to match dry run test
  217. gvr("", "v1", "events"): `{"involvedObject": {"namespace": "dryrunnamespace"}, "message": "some data here", "metadata": {"name": "event1"}}`,
  218. } {
  219. data := dryrunData[resource]
  220. data.Stub = stub
  221. dryrunData[resource] = data
  222. }
  223. // gather resources to test
  224. _, resources, err := client.Discovery().ServerGroupsAndResources()
  225. if err != nil {
  226. t.Fatalf("Failed to get ServerGroupsAndResources with error: %+v", err)
  227. }
  228. for _, resourceToTest := range etcd.GetResources(t, resources) {
  229. t.Run(resourceToTest.Mapping.Resource.String(), func(t *testing.T) {
  230. mapping := resourceToTest.Mapping
  231. gvk := resourceToTest.Mapping.GroupVersionKind
  232. gvResource := resourceToTest.Mapping.Resource
  233. kind := gvk.Kind
  234. if kindWhiteList.Has(kind) {
  235. t.Skip("whitelisted")
  236. }
  237. testData, hasTest := dryrunData[gvResource]
  238. if !hasTest {
  239. t.Fatalf("no test data for %s. Please add a test for your new type to etcd.GetEtcdStorageData().", gvResource)
  240. }
  241. rsc, obj, err := etcd.JSONToUnstructured(testData.Stub, testNamespace, mapping, dynamicClient)
  242. if err != nil {
  243. t.Fatalf("failed to unmarshal stub (%v): %v", testData.Stub, err)
  244. }
  245. name := obj.GetName()
  246. DryRunCreateTest(t, rsc, obj, gvResource)
  247. if _, err := rsc.Create(obj, metav1.CreateOptions{}); err != nil {
  248. t.Fatalf("failed to create stub for %s: %#v", gvResource, err)
  249. }
  250. DryRunUpdateTest(t, rsc, name)
  251. DryRunPatchTest(t, rsc, name)
  252. DryRunScalePatchTest(t, rsc, name)
  253. DryRunScaleUpdateTest(t, rsc, name)
  254. if resourceToTest.HasDeleteCollection {
  255. DryRunDeleteCollectionTest(t, rsc, name)
  256. }
  257. DryRunDeleteTest(t, rsc, name)
  258. if err = rsc.Delete(obj.GetName(), metav1.NewDeleteOptions(0)); err != nil {
  259. t.Fatalf("deleting final object failed: %v", err)
  260. }
  261. })
  262. }
  263. }
  264. func gvr(g, v, r string) schema.GroupVersionResource {
  265. return schema.GroupVersionResource{Group: g, Version: v, Resource: r}
  266. }