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