crd_overlap_storage_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. /*
  2. Copyright 2019 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 etcd
  14. import (
  15. "context"
  16. "encoding/json"
  17. "strings"
  18. "testing"
  19. "time"
  20. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  21. crdclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
  22. "k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
  23. apierrors "k8s.io/apimachinery/pkg/api/errors"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/apimachinery/pkg/runtime/schema"
  26. "k8s.io/apimachinery/pkg/types"
  27. "k8s.io/apimachinery/pkg/util/sets"
  28. "k8s.io/apimachinery/pkg/util/wait"
  29. "k8s.io/client-go/dynamic"
  30. apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
  31. apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1"
  32. )
  33. // TestOverlappingBuiltInResources ensures the list of group-resources the custom resource finalizer should skip is up to date
  34. func TestOverlappingBuiltInResources(t *testing.T) {
  35. // Verify built-in resources that overlap with computed CRD storage paths are listed in OverlappingBuiltInResources()
  36. detectedOverlappingResources := map[schema.GroupResource]bool{}
  37. for gvr, gvrData := range GetEtcdStorageData() {
  38. if !strings.HasSuffix(gvr.Group, ".k8s.io") {
  39. // only fully-qualified group names can exist as CRDs
  40. continue
  41. }
  42. if !strings.Contains(gvrData.ExpectedEtcdPath, "/"+gvr.Group+"/"+gvr.Resource+"/") {
  43. // CRDs persist in storage under .../<group>/<resource>/...
  44. continue
  45. }
  46. detectedOverlappingResources[gvr.GroupResource()] = true
  47. }
  48. for detected := range detectedOverlappingResources {
  49. if !finalizer.OverlappingBuiltInResources()[detected] {
  50. t.Errorf("built-in resource %#v would overlap with custom resource storage if a CRD was created for the same group/resource", detected)
  51. t.Errorf("add %#v to the OverlappingBuiltInResources() list to prevent deletion by the CRD finalizer", detected)
  52. }
  53. }
  54. for skip := range finalizer.OverlappingBuiltInResources() {
  55. if !detectedOverlappingResources[skip] {
  56. t.Errorf("resource %#v does not overlap with any built-in resources in storage, but is skipped for CRD finalization by OverlappingBuiltInResources()", skip)
  57. t.Errorf("remove %#v from OverlappingBuiltInResources() to ensure CRD finalization cleans up stored custom resources", skip)
  58. }
  59. }
  60. }
  61. // TestOverlappingCustomResourceAPIService ensures creating and deleting a custom resource overlapping with APIServices does not destroy APIService data
  62. func TestOverlappingCustomResourceAPIService(t *testing.T) {
  63. master := StartRealMasterOrDie(t)
  64. defer master.Cleanup()
  65. apiServiceClient, err := apiregistrationclient.NewForConfig(master.Config)
  66. if err != nil {
  67. t.Fatal(err)
  68. }
  69. crdClient, err := crdclient.NewForConfig(master.Config)
  70. if err != nil {
  71. t.Fatal(err)
  72. }
  73. dynamicClient, err := dynamic.NewForConfig(master.Config)
  74. if err != nil {
  75. t.Fatal(err)
  76. }
  77. // Verify APIServices can be listed
  78. apiServices, err := apiServiceClient.APIServices().List(context.TODO(), metav1.ListOptions{})
  79. if err != nil {
  80. t.Fatal(err)
  81. }
  82. apiServiceNames := sets.NewString()
  83. for _, s := range apiServices.Items {
  84. apiServiceNames.Insert(s.Name)
  85. }
  86. if len(apiServices.Items) == 0 {
  87. t.Fatal("expected APIService objects, got none")
  88. }
  89. // Create a CRD defining an overlapping apiregistration.k8s.io apiservices resource with an incompatible schema
  90. crdCRD, err := crdClient.CustomResourceDefinitions().Create(context.TODO(), &apiextensionsv1.CustomResourceDefinition{
  91. ObjectMeta: metav1.ObjectMeta{
  92. Name: "apiservices.apiregistration.k8s.io",
  93. Annotations: map[string]string{"api-approved.kubernetes.io": "unapproved, testing only"},
  94. },
  95. Spec: apiextensionsv1.CustomResourceDefinitionSpec{
  96. Group: "apiregistration.k8s.io",
  97. Scope: apiextensionsv1.ClusterScoped,
  98. Names: apiextensionsv1.CustomResourceDefinitionNames{Plural: "apiservices", Singular: "customapiservice", Kind: "CustomAPIService", ListKind: "CustomAPIServiceList"},
  99. Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
  100. {
  101. Name: "v1",
  102. Served: true,
  103. Storage: true,
  104. Schema: &apiextensionsv1.CustomResourceValidation{
  105. OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
  106. Type: "object",
  107. Required: []string{"foo"},
  108. Properties: map[string]apiextensionsv1.JSONSchemaProps{
  109. "foo": {Type: "string"},
  110. "bar": {Type: "string", Default: &apiextensionsv1.JSON{Raw: []byte(`"default"`)}},
  111. },
  112. },
  113. },
  114. },
  115. },
  116. },
  117. }, metav1.CreateOptions{})
  118. if err != nil {
  119. t.Fatal(err)
  120. }
  121. // Wait until it is established
  122. if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
  123. crd, err := crdClient.CustomResourceDefinitions().Get(context.TODO(), crdCRD.Name, metav1.GetOptions{})
  124. if err != nil {
  125. return false, err
  126. }
  127. for _, condition := range crd.Status.Conditions {
  128. if condition.Status == apiextensionsv1.ConditionTrue && condition.Type == apiextensionsv1.Established {
  129. return true, nil
  130. }
  131. }
  132. conditionJSON, _ := json.Marshal(crd.Status.Conditions)
  133. t.Logf("waiting for establishment (conditions: %s)", string(conditionJSON))
  134. return false, nil
  135. }); err != nil {
  136. t.Fatal(err)
  137. }
  138. // Make sure API requests are still handled by the built-in handler (and return built-in kinds)
  139. // Listing v1 succeeds
  140. v1DynamicList, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "apiregistration.k8s.io", Version: "v1", Resource: "apiservices"}).List(metav1.ListOptions{})
  141. if err != nil {
  142. t.Fatal(err)
  143. }
  144. // Result was served by built-in handler, not CR handler
  145. if _, hasDefaultedCRField := v1DynamicList.Items[0].Object["spec"].(map[string]interface{})["bar"]; hasDefaultedCRField {
  146. t.Fatalf("expected no CR defaulting, got %#v", v1DynamicList.Items[0].Object)
  147. }
  148. // Creating v1 succeeds (built-in validation, not CR validation)
  149. testAPIService, err := apiServiceClient.APIServices().Create(context.TODO(), &apiregistrationv1.APIService{
  150. ObjectMeta: metav1.ObjectMeta{Name: "v1.example.com"},
  151. Spec: apiregistrationv1.APIServiceSpec{
  152. Group: "example.com",
  153. Version: "v1",
  154. VersionPriority: 100,
  155. GroupPriorityMinimum: 100,
  156. },
  157. }, metav1.CreateOptions{})
  158. if err != nil {
  159. t.Fatal(err)
  160. }
  161. err = apiServiceClient.APIServices().Delete(context.TODO(), testAPIService.Name, &metav1.DeleteOptions{})
  162. if err != nil {
  163. t.Fatal(err)
  164. }
  165. // discovery is handled by the built-in handler
  166. v1Resources, err := master.Client.Discovery().ServerResourcesForGroupVersion("apiregistration.k8s.io/v1")
  167. if err != nil {
  168. t.Fatal(err)
  169. }
  170. for _, r := range v1Resources.APIResources {
  171. if r.Name == "apiservices" {
  172. if r.Kind != "APIService" {
  173. t.Errorf("expected kind=APIService in discovery, got %s", r.Kind)
  174. }
  175. }
  176. }
  177. v2Resources, err := master.Client.Discovery().ServerResourcesForGroupVersion("apiregistration.k8s.io/v2")
  178. if err == nil {
  179. t.Fatalf("expected error looking up apiregistration.k8s.io/v2 discovery, got %#v", v2Resources)
  180. }
  181. // Delete the overlapping CRD
  182. err = crdClient.CustomResourceDefinitions().Delete(context.TODO(), crdCRD.Name, &metav1.DeleteOptions{})
  183. if err != nil {
  184. t.Fatal(err)
  185. }
  186. // Make sure the CRD deletion succeeds
  187. if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
  188. crd, err := crdClient.CustomResourceDefinitions().Get(context.TODO(), crdCRD.Name, metav1.GetOptions{})
  189. if apierrors.IsNotFound(err) {
  190. return true, nil
  191. }
  192. if err != nil {
  193. return false, err
  194. }
  195. conditionJSON, _ := json.Marshal(crd.Status.Conditions)
  196. t.Logf("waiting for deletion (conditions: %s)", string(conditionJSON))
  197. return false, nil
  198. }); err != nil {
  199. t.Fatal(err)
  200. }
  201. // Make sure APIService objects are not removed
  202. time.Sleep(5 * time.Second)
  203. finalAPIServices, err := apiServiceClient.APIServices().List(context.TODO(), metav1.ListOptions{})
  204. if err != nil {
  205. t.Fatal(err)
  206. }
  207. if len(finalAPIServices.Items) != len(apiServices.Items) {
  208. t.Fatalf("expected %d APIService objects, got %d", len(apiServices.Items), len(finalAPIServices.Items))
  209. }
  210. }
  211. // TestOverlappingCustomResourceCustomResourceDefinition ensures creating and deleting a custom resource overlapping with CustomResourceDefinition does not destroy CustomResourceDefinition data
  212. func TestOverlappingCustomResourceCustomResourceDefinition(t *testing.T) {
  213. master := StartRealMasterOrDie(t)
  214. defer master.Cleanup()
  215. crdClient, err := crdclient.NewForConfig(master.Config)
  216. if err != nil {
  217. t.Fatal(err)
  218. }
  219. dynamicClient, err := dynamic.NewForConfig(master.Config)
  220. if err != nil {
  221. t.Fatal(err)
  222. }
  223. // Verify CustomResourceDefinitions can be listed
  224. crds, err := crdClient.CustomResourceDefinitions().List(context.TODO(), metav1.ListOptions{})
  225. if err != nil {
  226. t.Fatal(err)
  227. }
  228. crdNames := sets.NewString()
  229. for _, s := range crds.Items {
  230. crdNames.Insert(s.Name)
  231. }
  232. if len(crds.Items) == 0 {
  233. t.Fatal("expected CustomResourceDefinition objects, got none")
  234. }
  235. // Create a CRD defining an overlapping apiregistration.k8s.io apiservices resource with an incompatible schema
  236. crdCRD, err := crdClient.CustomResourceDefinitions().Create(context.TODO(), &apiextensionsv1.CustomResourceDefinition{
  237. ObjectMeta: metav1.ObjectMeta{
  238. Name: "customresourcedefinitions.apiextensions.k8s.io",
  239. Annotations: map[string]string{"api-approved.kubernetes.io": "unapproved, testing only"},
  240. },
  241. Spec: apiextensionsv1.CustomResourceDefinitionSpec{
  242. Group: "apiextensions.k8s.io",
  243. Scope: apiextensionsv1.ClusterScoped,
  244. Names: apiextensionsv1.CustomResourceDefinitionNames{
  245. Plural: "customresourcedefinitions",
  246. Singular: "customcustomresourcedefinition",
  247. Kind: "CustomCustomResourceDefinition",
  248. ListKind: "CustomAPIServiceList",
  249. },
  250. Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
  251. {
  252. Name: "v1",
  253. Served: true,
  254. Storage: true,
  255. Schema: &apiextensionsv1.CustomResourceValidation{
  256. OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
  257. Type: "object",
  258. Required: []string{"foo"},
  259. Properties: map[string]apiextensionsv1.JSONSchemaProps{
  260. "foo": {Type: "string"},
  261. "bar": {Type: "string", Default: &apiextensionsv1.JSON{Raw: []byte(`"default"`)}},
  262. },
  263. },
  264. },
  265. },
  266. },
  267. },
  268. }, metav1.CreateOptions{})
  269. if err != nil {
  270. t.Fatal(err)
  271. }
  272. // Wait until it is established
  273. if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
  274. crd, err := crdClient.CustomResourceDefinitions().Get(context.TODO(), crdCRD.Name, metav1.GetOptions{})
  275. if err != nil {
  276. return false, err
  277. }
  278. for _, condition := range crd.Status.Conditions {
  279. if condition.Status == apiextensionsv1.ConditionTrue && condition.Type == apiextensionsv1.Established {
  280. return true, nil
  281. }
  282. }
  283. conditionJSON, _ := json.Marshal(crd.Status.Conditions)
  284. t.Logf("waiting for establishment (conditions: %s)", string(conditionJSON))
  285. return false, nil
  286. }); err != nil {
  287. t.Fatal(err)
  288. }
  289. // Make sure API requests are still handled by the built-in handler (and return built-in kinds)
  290. // Listing v1 succeeds
  291. v1DynamicList, err := dynamicClient.Resource(schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}).List(metav1.ListOptions{})
  292. if err != nil {
  293. t.Fatal(err)
  294. }
  295. // Result was served by built-in handler, not CR handler
  296. if _, hasDefaultedCRField := v1DynamicList.Items[0].Object["spec"].(map[string]interface{})["bar"]; hasDefaultedCRField {
  297. t.Fatalf("expected no CR defaulting, got %#v", v1DynamicList.Items[0].Object)
  298. }
  299. // Updating v1 succeeds (built-in validation, not CR validation)
  300. _, err = crdClient.CustomResourceDefinitions().Patch(context.TODO(), crdCRD.Name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"test":"updated"}}}`), metav1.PatchOptions{})
  301. if err != nil {
  302. t.Fatal(err)
  303. }
  304. // discovery is handled by the built-in handler
  305. v1Resources, err := master.Client.Discovery().ServerResourcesForGroupVersion("apiextensions.k8s.io/v1")
  306. if err != nil {
  307. t.Fatal(err)
  308. }
  309. for _, r := range v1Resources.APIResources {
  310. if r.Name == "customresourcedefinitions" {
  311. if r.Kind != "CustomResourceDefinition" {
  312. t.Errorf("expected kind=CustomResourceDefinition in discovery, got %s", r.Kind)
  313. }
  314. }
  315. }
  316. v2Resources, err := master.Client.Discovery().ServerResourcesForGroupVersion("apiextensions.k8s.io/v2")
  317. if err == nil {
  318. t.Fatalf("expected error looking up apiregistration.k8s.io/v2 discovery, got %#v", v2Resources)
  319. }
  320. // Delete the overlapping CRD
  321. err = crdClient.CustomResourceDefinitions().Delete(context.TODO(), crdCRD.Name, &metav1.DeleteOptions{})
  322. if err != nil {
  323. t.Fatal(err)
  324. }
  325. // Make sure the CRD deletion succeeds
  326. if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
  327. crd, err := crdClient.CustomResourceDefinitions().Get(context.TODO(), crdCRD.Name, metav1.GetOptions{})
  328. if apierrors.IsNotFound(err) {
  329. return true, nil
  330. }
  331. if err != nil {
  332. return false, err
  333. }
  334. conditionJSON, _ := json.Marshal(crd.Status.Conditions)
  335. t.Logf("waiting for deletion (conditions: %s)", string(conditionJSON))
  336. return false, nil
  337. }); err != nil {
  338. t.Fatal(err)
  339. }
  340. // Make sure other CustomResourceDefinition objects are not removed
  341. time.Sleep(5 * time.Second)
  342. finalCRDs, err := crdClient.CustomResourceDefinitions().List(context.TODO(), metav1.ListOptions{})
  343. if err != nil {
  344. t.Fatal(err)
  345. }
  346. if len(finalCRDs.Items) != len(crds.Items) {
  347. t.Fatalf("expected %d APIService objects, got %d", len(crds.Items), len(finalCRDs.Items))
  348. }
  349. }