1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package integration
18
19import (
20	"context"
21	"fmt"
22	"math"
23	"reflect"
24	"sort"
25	"strings"
26	"testing"
27
28	autoscaling "k8s.io/api/autoscaling/v1"
29	apierrors "k8s.io/apimachinery/pkg/api/errors"
30	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
32	"k8s.io/apimachinery/pkg/runtime/schema"
33	"k8s.io/apimachinery/pkg/types"
34	"k8s.io/client-go/dynamic"
35
36	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
37	"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
38	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
39)
40
41var labelSelectorPath = ".status.labelSelector"
42var anotherLabelSelectorPath = ".status.anotherLabelSelector"
43
44func NewNoxuSubresourcesCRDs(scope apiextensionsv1beta1.ResourceScope) []*apiextensionsv1beta1.CustomResourceDefinition {
45	return []*apiextensionsv1beta1.CustomResourceDefinition{
46		// CRD that uses top-level subresources
47		{
48			ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
49			Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
50				Group:   "mygroup.example.com",
51				Version: "v1beta1",
52				Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
53					Plural:     "noxus",
54					Singular:   "nonenglishnoxu",
55					Kind:       "WishIHadChosenNoxu",
56					ShortNames: []string{"foo", "bar", "abc", "def"},
57					ListKind:   "NoxuItemList",
58				},
59				Scope: scope,
60				Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
61					{
62						Name:    "v1beta1",
63						Served:  true,
64						Storage: true,
65					},
66					{
67						Name:    "v1",
68						Served:  true,
69						Storage: false,
70					},
71				},
72				Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
73					Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
74					Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
75						SpecReplicasPath:   ".spec.replicas",
76						StatusReplicasPath: ".status.replicas",
77						LabelSelectorPath:  &labelSelectorPath,
78					},
79				},
80			},
81		},
82		// CRD that uses per-version subresources
83		{
84			ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
85			Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
86				Group:   "mygroup.example.com",
87				Version: "v1beta1",
88				Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
89					Plural:     "noxus",
90					Singular:   "nonenglishnoxu",
91					Kind:       "WishIHadChosenNoxu",
92					ShortNames: []string{"foo", "bar", "abc", "def"},
93					ListKind:   "NoxuItemList",
94				},
95				Scope: scope,
96				Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{
97					{
98						Name:    "v1beta1",
99						Served:  true,
100						Storage: true,
101						Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
102							Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
103							Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
104								SpecReplicasPath:   ".spec.replicas",
105								StatusReplicasPath: ".status.replicas",
106								LabelSelectorPath:  &labelSelectorPath,
107							},
108						},
109					},
110					{
111						Name:    "v1",
112						Served:  true,
113						Storage: false,
114						Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
115							Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
116							Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
117								SpecReplicasPath:   ".spec.replicas",
118								StatusReplicasPath: ".status.replicas",
119								LabelSelectorPath:  &anotherLabelSelectorPath,
120							},
121						},
122					},
123				},
124			},
125		},
126	}
127}
128
129func NewNoxuSubresourceInstance(namespace, name, version string) *unstructured.Unstructured {
130	return &unstructured.Unstructured{
131		Object: map[string]interface{}{
132			"apiVersion": fmt.Sprintf("mygroup.example.com/%s", version),
133			"kind":       "WishIHadChosenNoxu",
134			"metadata": map[string]interface{}{
135				"namespace": namespace,
136				"name":      name,
137			},
138			"spec": map[string]interface{}{
139				"num":      int64(10),
140				"replicas": int64(3),
141			},
142			"status": map[string]interface{}{
143				"replicas": int64(7),
144			},
145		},
146	}
147}
148
149func TestStatusSubresource(t *testing.T) {
150	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
151	if err != nil {
152		t.Fatal(err)
153	}
154	defer tearDown()
155
156	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
157	for _, noxuDefinition := range noxuDefinitions {
158		noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
159		if err != nil {
160			t.Fatal(err)
161		}
162
163		ns := "not-the-default"
164		for _, v := range noxuDefinition.Spec.Versions {
165			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
166			_, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
167			if err != nil {
168				t.Fatalf("unable to create noxu instance: %v", err)
169			}
170			gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
171			if err != nil {
172				t.Fatal(err)
173			}
174			// status should not be set after creation
175			if val, ok := gottenNoxuInstance.Object["status"]; ok {
176				t.Fatalf("status should not be set after creation, got %v", val)
177			}
178
179			// .status.num = 20
180			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
181			if err != nil {
182				t.Fatalf("unexpected error: %v", err)
183			}
184
185			// .spec.num = 20
186			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
187			if err != nil {
188				t.Fatalf("unexpected error: %v", err)
189			}
190
191			// UpdateStatus should not update spec.
192			// Check that .spec.num = 10 and .status.num = 20
193			updatedStatusInstance, err := noxuResourceClient.UpdateStatus(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
194			if err != nil {
195				t.Fatalf("unable to update status: %v", err)
196			}
197
198			specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num")
199			if !found || err != nil {
200				t.Fatalf("unable to get .spec.num")
201			}
202			if specNum != int64(10) {
203				t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum)
204			}
205
206			statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num")
207			if !found || err != nil {
208				t.Fatalf("unable to get .status.num")
209			}
210			if statusNum != int64(20) {
211				t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
212			}
213
214			gottenNoxuInstance, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
215			if err != nil {
216				t.Fatal(err)
217			}
218
219			// .status.num = 40
220			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num")
221			if err != nil {
222				t.Fatalf("unexpected error: %v", err)
223			}
224
225			// .spec.num = 40
226			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num")
227			if err != nil {
228				t.Fatalf("unexpected error: %v", err)
229			}
230
231			// Update should not update status.
232			// Check that .spec.num = 40 and .status.num = 20
233			updatedInstance, err := noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
234			if err != nil {
235				t.Fatalf("unable to update instance: %v", err)
236			}
237
238			specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num")
239			if !found || err != nil {
240				t.Fatalf("unable to get .spec.num")
241			}
242			if specNum != int64(40) {
243				t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum)
244			}
245
246			statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num")
247			if !found || err != nil {
248				t.Fatalf("unable to get .status.num")
249			}
250			if statusNum != int64(20) {
251				t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
252			}
253			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
254		}
255		if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
256			t.Fatal(err)
257		}
258	}
259}
260
261func TestScaleSubresource(t *testing.T) {
262	groupResource := schema.GroupResource{
263		Group:    "mygroup.example.com",
264		Resource: "noxus",
265	}
266
267	tearDown, config, _, err := fixtures.StartDefaultServer(t)
268	if err != nil {
269		t.Fatal(err)
270	}
271	defer tearDown()
272
273	apiExtensionClient, err := clientset.NewForConfig(config)
274	if err != nil {
275		t.Fatal(err)
276	}
277	dynamicClient, err := dynamic.NewForConfig(config)
278	if err != nil {
279		t.Fatal(err)
280	}
281
282	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
283	for _, noxuDefinition := range noxuDefinitions {
284		for _, v := range noxuDefinition.Spec.Versions {
285			// Start with a new CRD, so that the object doesn't have resourceVersion
286			noxuDefinition := noxuDefinition.DeepCopy()
287
288			subresources, err := getSubresourcesForVersion(noxuDefinition, v.Name)
289			if err != nil {
290				t.Fatal(err)
291			}
292			// set invalid json path for specReplicasPath
293			subresources.Scale.SpecReplicasPath = "foo,bar"
294			_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
295			if err == nil {
296				t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec")
297			}
298
299			subresources.Scale.SpecReplicasPath = ".spec.replicas"
300			noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
301			if err != nil {
302				t.Fatal(err)
303			}
304
305			ns := "not-the-default"
306			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
307			_, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
308			if err != nil {
309				t.Fatalf("unable to create noxu instance: %v", err)
310			}
311
312			scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name)
313			if err != nil {
314				t.Fatal(err)
315			}
316
317			// set .status.labelSelector = bar
318			gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
319			if err != nil {
320				t.Fatal(err)
321			}
322			err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...)
323			if err != nil {
324				t.Fatalf("unexpected error: %v", err)
325			}
326			_, err = noxuResourceClient.UpdateStatus(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
327			if err != nil {
328				t.Fatalf("unable to update status: %v", err)
329			}
330
331			// get the scale object
332			gottenScale, err := scaleClient.Scales("not-the-default").Get(context.TODO(), groupResource, "foo", metav1.GetOptions{})
333			if err != nil {
334				t.Fatal(err)
335			}
336			if gottenScale.Spec.Replicas != 3 {
337				t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas)
338			}
339			if gottenScale.Status.Selector != "bar" {
340				t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector)
341			}
342
343			// check self link
344			expectedSelfLink := fmt.Sprintf("/apis/mygroup.example.com/%s/namespaces/not-the-default/noxus/foo/scale", v.Name)
345			if gottenScale.GetSelfLink() != expectedSelfLink {
346				t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink())
347			}
348
349			// update the scale object
350			// check that spec is updated, but status is not
351			gottenScale.Spec.Replicas = 5
352			gottenScale.Status.Selector = "baz"
353			updatedScale, err := scaleClient.Scales("not-the-default").Update(context.TODO(), groupResource, gottenScale, metav1.UpdateOptions{})
354			if err != nil {
355				t.Fatal(err)
356			}
357			if updatedScale.Spec.Replicas != 5 {
358				t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas)
359			}
360			if updatedScale.Status.Selector != "bar" {
361				t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector)
362			}
363
364			// check that .spec.replicas = 5, but status is not updated
365			updatedNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
366			if err != nil {
367				t.Fatal(err)
368			}
369			specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas")
370			if !found || err != nil {
371				t.Fatalf("unable to get .spec.replicas")
372			}
373			if specReplicas != 5 {
374				t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas)
375			}
376			statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...)
377			if !found || err != nil {
378				t.Fatalf("unable to get %s", *subresources.Scale.LabelSelectorPath)
379			}
380			if statusLabelSelector != "bar" {
381				t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector)
382			}
383
384			// validate maximum value
385			// set .spec.replicas = math.MaxInt64
386			gottenNoxuInstance, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
387			if err != nil {
388				t.Fatal(err)
389			}
390			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas")
391			if err != nil {
392				t.Fatalf("unexpected error: %v", err)
393			}
394			_, err = noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
395			if err == nil {
396				t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647")
397			}
398			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
399			if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
400				t.Fatal(err)
401			}
402		}
403	}
404}
405
406func TestValidationSchemaWithStatus(t *testing.T) {
407	tearDown, config, _, err := fixtures.StartDefaultServer(t)
408	if err != nil {
409		t.Fatal(err)
410	}
411	defer tearDown()
412
413	apiExtensionClient, err := clientset.NewForConfig(config)
414	if err != nil {
415		t.Fatal(err)
416	}
417	dynamicClient, err := dynamic.NewForConfig(config)
418	if err != nil {
419		t.Fatal(err)
420	}
421
422	// fields other than properties in root schema are not allowed
423	noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[0]
424	noxuDefinition.Spec.Subresources = &apiextensionsv1beta1.CustomResourceSubresources{
425		Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
426	}
427	_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
428	if err == nil {
429		t.Fatalf(`unexpected non-error, expected: must not have "additionalProperties" at the root of the schema if the status subresource is enabled`)
430	}
431
432	// make sure we are not restricting fields to properties even in subschemas
433	noxuDefinition.Spec.Validation.OpenAPIV3Schema = &apiextensionsv1beta1.JSONSchemaProps{
434		Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
435			"spec": {
436				Description: "Validation for spec",
437				Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
438					"replicas": {
439						Type: "integer",
440					},
441				},
442			},
443		},
444		Required:    []string{"spec"},
445		Description: "This is a description at the root of the schema",
446	}
447	_, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
448	if err != nil {
449		t.Fatalf("unable to created crd %v: %v", noxuDefinition.Name, err)
450	}
451}
452
453func TestValidateOnlyStatus(t *testing.T) {
454	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
455	if err != nil {
456		t.Fatal(err)
457	}
458	defer tearDown()
459
460	// UpdateStatus should validate only status
461	// 1. create a crd with max value of .spec.num = 10 and .status.num = 10
462	// 2. create a cr with .spec.num = 10 and .status.num = 10 (valid)
463	// 3. update the spec of the cr with .spec.num = 15 (spec is invalid), expect no error
464	// 4. update the spec of the cr with .spec.num = 15 (spec is invalid), expect error
465
466	// max value of spec.num = 10 and status.num = 10
467	schema := &apiextensionsv1beta1.JSONSchemaProps{
468		Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
469			"spec": {
470				Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
471					"num": {
472						Type:    "integer",
473						Maximum: float64Ptr(10),
474					},
475				},
476			},
477			"status": {
478				Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
479					"num": {
480						Type:    "integer",
481						Maximum: float64Ptr(10),
482					},
483				},
484			},
485		},
486	}
487
488	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
489	for i, noxuDefinition := range noxuDefinitions {
490		if i == 0 {
491			noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
492				OpenAPIV3Schema: schema,
493			}
494		} else {
495			noxuDefinition.Spec.Versions[0].Schema = &apiextensionsv1beta1.CustomResourceValidation{
496				OpenAPIV3Schema: schema,
497			}
498			schemaWithDescription := schema.DeepCopy()
499			schemaWithDescription.Description = "test"
500			noxuDefinition.Spec.Versions[1].Schema = &apiextensionsv1beta1.CustomResourceValidation{
501				OpenAPIV3Schema: schemaWithDescription,
502			}
503		}
504
505		noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
506		if err != nil {
507			t.Fatal(err)
508		}
509		ns := "not-the-default"
510		for _, v := range noxuDefinition.Spec.Versions {
511			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
512
513			// set .spec.num = 10 and .status.num = 10
514			noxuInstance := NewNoxuSubresourceInstance(ns, "foo", v.Name)
515			err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num")
516			if err != nil {
517				t.Fatalf("unexpected error: %v", err)
518			}
519
520			createdNoxuInstance, err := instantiateVersionedCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition, v.Name)
521			if err != nil {
522				t.Fatalf("unable to create noxu instance: %v", err)
523			}
524
525			// update the spec with .spec.num = 15, expecting no error
526			err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "spec", "num")
527			if err != nil {
528				t.Fatalf("unexpected error setting .spec.num: %v", err)
529			}
530			createdNoxuInstance, err = noxuResourceClient.UpdateStatus(context.TODO(), createdNoxuInstance, metav1.UpdateOptions{})
531			if err != nil {
532				t.Fatalf("unexpected error: %v", err)
533			}
534
535			// update with .status.num = 15, expecting an error
536			err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "status", "num")
537			if err != nil {
538				t.Fatalf("unexpected error setting .status.num: %v", err)
539			}
540			_, err = noxuResourceClient.UpdateStatus(context.TODO(), createdNoxuInstance, metav1.UpdateOptions{})
541			if err == nil {
542				t.Fatal("expected error, but got none")
543			}
544			statusError, isStatus := err.(*apierrors.StatusError)
545			if !isStatus || statusError == nil {
546				t.Fatalf("expected status error, got %T: %v", err, err)
547			}
548			if !strings.Contains(statusError.Error(), "Invalid value") {
549				t.Fatalf("expected 'Invalid value' in error, got: %v", err)
550			}
551			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
552		}
553		if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
554			t.Fatal(err)
555		}
556	}
557}
558
559func TestSubresourcesDiscovery(t *testing.T) {
560	tearDown, config, _, err := fixtures.StartDefaultServer(t)
561	if err != nil {
562		t.Fatal(err)
563	}
564	defer tearDown()
565
566	apiExtensionClient, err := clientset.NewForConfig(config)
567	if err != nil {
568		t.Fatal(err)
569	}
570	dynamicClient, err := dynamic.NewForConfig(config)
571	if err != nil {
572		t.Fatal(err)
573	}
574
575	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
576	for _, noxuDefinition := range noxuDefinitions {
577		noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
578		if err != nil {
579			t.Fatal(err)
580		}
581
582		for _, v := range noxuDefinition.Spec.Versions {
583			group := "mygroup.example.com"
584			version := v.Name
585
586			resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version)
587			if err != nil {
588				t.Fatal(err)
589			}
590
591			if len(resources.APIResources) != 3 {
592				t.Fatalf("Expected exactly the resources \"noxus\", \"noxus/status\" and \"noxus/scale\" in group version %v/%v via discovery, got: %v", group, version, resources.APIResources)
593			}
594
595			// check discovery info for status
596			status := resources.APIResources[1]
597
598			if status.Name != "noxus/status" {
599				t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name)
600			}
601
602			if status.Namespaced != true {
603				t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced)
604			}
605
606			if status.Kind != "WishIHadChosenNoxu" {
607				t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind)
608			}
609
610			expectedVerbs := []string{"get", "patch", "update"}
611			sort.Strings(status.Verbs)
612			if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) {
613				t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs)
614			}
615
616			// check discovery info for scale
617			scale := resources.APIResources[2]
618
619			if scale.Group != autoscaling.GroupName {
620				t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group)
621			}
622
623			if scale.Version != "v1" {
624				t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version)
625			}
626
627			if scale.Name != "noxus/scale" {
628				t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name)
629			}
630
631			if scale.Namespaced != true {
632				t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced)
633			}
634
635			if scale.Kind != "Scale" {
636				t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind)
637			}
638
639			sort.Strings(scale.Verbs)
640			if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) {
641				t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs)
642			}
643		}
644		if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
645			t.Fatal(err)
646		}
647	}
648}
649
650func TestGeneration(t *testing.T) {
651	tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
652	if err != nil {
653		t.Fatal(err)
654	}
655	defer tearDown()
656
657	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
658	for _, noxuDefinition := range noxuDefinitions {
659		noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
660		if err != nil {
661			t.Fatal(err)
662		}
663
664		ns := "not-the-default"
665		for _, v := range noxuDefinition.Spec.Versions {
666			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
667			_, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
668			if err != nil {
669				t.Fatalf("unable to create noxu instance: %v", err)
670			}
671
672			// .metadata.generation = 1
673			gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
674			if err != nil {
675				t.Fatal(err)
676			}
677			if gottenNoxuInstance.GetGeneration() != 1 {
678				t.Fatalf(".metadata.generation should be 1 after creation")
679			}
680
681			// .status.num = 20
682			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
683			if err != nil {
684				t.Fatalf("unexpected error: %v", err)
685			}
686
687			// UpdateStatus does not increment generation
688			updatedStatusInstance, err := noxuResourceClient.UpdateStatus(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
689			if err != nil {
690				t.Fatalf("unable to update status: %v", err)
691			}
692			if updatedStatusInstance.GetGeneration() != 1 {
693				t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration())
694			}
695
696			gottenNoxuInstance, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
697			if err != nil {
698				t.Fatal(err)
699			}
700
701			// .spec.num = 20
702			err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
703			if err != nil {
704				t.Fatalf("unexpected error: %v", err)
705			}
706
707			// Update increments generation
708			updatedInstance, err := noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{})
709			if err != nil {
710				t.Fatalf("unable to update instance: %v", err)
711			}
712			if updatedInstance.GetGeneration() != 2 {
713				t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration())
714			}
715			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
716		}
717		if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
718			t.Fatal(err)
719		}
720	}
721}
722
723func TestSubresourcePatch(t *testing.T) {
724	groupResource := schema.GroupResource{
725		Group:    "mygroup.example.com",
726		Resource: "noxus",
727	}
728
729	tearDown, config, _, err := fixtures.StartDefaultServer(t)
730	if err != nil {
731		t.Fatal(err)
732	}
733	defer tearDown()
734
735	apiExtensionClient, err := clientset.NewForConfig(config)
736	if err != nil {
737		t.Fatal(err)
738	}
739	dynamicClient, err := dynamic.NewForConfig(config)
740	if err != nil {
741		t.Fatal(err)
742	}
743
744	noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)
745	for _, noxuDefinition := range noxuDefinitions {
746		noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
747		if err != nil {
748			t.Fatal(err)
749		}
750
751		ns := "not-the-default"
752		for _, v := range noxuDefinition.Spec.Versions {
753			noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name)
754
755			t.Logf("Creating foo")
756			_, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name)
757			if err != nil {
758				t.Fatalf("unable to create noxu instance: %v", err)
759			}
760
761			scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name)
762			if err != nil {
763				t.Fatal(err)
764			}
765
766			t.Logf("Patching .status.num to 999")
767			patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`)
768			patchedNoxuInstance, err := noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "status")
769			if err != nil {
770				t.Fatalf("unexpected error: %v", err)
771			}
772
773			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") // .status.num should be 999
774			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num")    // .spec.num should remain 10
775			rv, found, err := unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion")
776			if err != nil {
777				t.Fatal(err)
778			}
779			if !found {
780				t.Fatalf("metadata.resourceVersion not found")
781			}
782
783			// this call waits for the resourceVersion to be reached in the cache before returning.
784			// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
785			// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
786			// and then the updated object shows a conflicting diff, which permanently fails the patch.
787			// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
788			// See https://issue.k8s.io/42644
789			_, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
790			if err != nil {
791				t.Fatalf("unexpected error: %v", err)
792			}
793
794			// no-op patch
795			t.Logf("Patching .status.num again to 999")
796			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "status")
797			if err != nil {
798				t.Fatalf("unexpected error: %v", err)
799			}
800			// make sure no-op patch does not increment resourceVersion
801			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num")
802			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num")
803			expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
804
805			// empty patch
806			t.Logf("Applying empty patch")
807			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}, "status")
808			if err != nil {
809				t.Fatalf("unexpected error: %v", err)
810			}
811
812			// an empty patch is a no-op patch. make sure it does not increment resourceVersion
813			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num")
814			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num")
815			expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
816
817			t.Logf("Patching .spec.replicas to 7")
818			patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`)
819			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "scale")
820			if err != nil {
821				t.Fatalf("unexpected error: %v", err)
822			}
823
824			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas")
825			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") // .status.replicas should remain 0
826			rv, found, err = unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion")
827			if err != nil {
828				t.Fatal(err)
829			}
830			if !found {
831				t.Fatalf("metadata.resourceVersion not found")
832			}
833
834			// this call waits for the resourceVersion to be reached in the cache before returning.
835			// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
836			// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
837			// and then the updated object shows a conflicting diff, which permanently fails the patch.
838			// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
839			// See https://issue.k8s.io/42644
840			_, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
841			if err != nil {
842				t.Fatalf("unexpected error: %v", err)
843			}
844
845			// Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 0
846			gottenScale, err := scaleClient.Scales("not-the-default").Get(context.TODO(), groupResource, "foo", metav1.GetOptions{})
847			if err != nil {
848				t.Fatal(err)
849			}
850			if gottenScale.Spec.Replicas != 7 {
851				t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas)
852			}
853			if gottenScale.Status.Replicas != 0 {
854				t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas)
855			}
856
857			// no-op patch
858			t.Logf("Patching .spec.replicas again to 7")
859			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "scale")
860			if err != nil {
861				t.Fatalf("unexpected error: %v", err)
862			}
863			// make sure no-op patch does not increment resourceVersion
864			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas")
865			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas")
866			expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
867
868			// empty patch
869			t.Logf("Applying empty patch")
870			patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}, "scale")
871			if err != nil {
872				t.Fatalf("unexpected error: %v", err)
873			}
874			// an empty patch is a no-op patch. make sure it does not increment resourceVersion
875			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas")
876			expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas")
877			expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion")
878
879			// make sure strategic merge patch is not supported for both status and scale
880			_, err = noxuResourceClient.Patch(context.TODO(), "foo", types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "status")
881			if err == nil {
882				t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
883			}
884
885			_, err = noxuResourceClient.Patch(context.TODO(), "foo", types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "scale")
886			if err == nil {
887				t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
888			}
889			noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{})
890		}
891		if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil {
892			t.Fatal(err)
893		}
894	}
895}
896