diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures/resources.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures/resources.go index 9a1f2d84845..7230725d4db 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures/resources.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures/resources.go @@ -370,13 +370,13 @@ func DeleteCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefi return nil } -// CreateNewScaleClient returns a scale client. -func CreateNewScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, config *rest.Config) (scale.ScalesGetter, error) { +// CreateNewVersionedScaleClient returns a scale client. +func CreateNewVersionedScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, config *rest.Config, version string) (scale.ScalesGetter, error) { discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) if err != nil { return nil, err } - groupResource, err := discoveryClient.ServerResourcesForGroupVersion(crd.Spec.Group + "/" + crd.Spec.Version) + groupResource, err := discoveryClient.ServerResourcesForGroupVersion(crd.Spec.Group + "/" + version) if err != nil { return nil, err } @@ -386,12 +386,12 @@ func CreateNewScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, co Group: metav1.APIGroup{ Name: crd.Spec.Group, Versions: []metav1.GroupVersionForDiscovery{ - {Version: crd.Spec.Version}, + {Version: version}, }, - PreferredVersion: metav1.GroupVersionForDiscovery{Version: crd.Spec.Version}, + PreferredVersion: metav1.GroupVersionForDiscovery{Version: version}, }, VersionedResources: map[string][]metav1.APIResource{ - crd.Spec.Version: groupResource.APIResources, + version: groupResource.APIResources, }, }, } diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/helpers.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/helpers.go index 76344034564..20d63c10007 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/helpers.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/helpers.go @@ -30,6 +30,8 @@ import ( "k8s.io/client-go/dynamic" ) +var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc() + func instantiateCustomResource(t *testing.T, instanceToCreate *unstructured.Unstructured, client dynamic.ResourceInterface, definition *apiextensionsv1beta1.CustomResourceDefinition) (*unstructured.Unstructured, error) { return instantiateVersionedCustomResource(t, instanceToCreate, client, definition, definition.Spec.Versions[0].Name) } @@ -92,3 +94,97 @@ func updateCustomResourceDefinitionWithRetry(client clientset.Interface, name st } return nil, fmt.Errorf("too many retries after conflicts updating CustomResourceDefinition %q", name) } + +// getSchemaForVersion returns the validation schema for given version in given CRD. +func getSchemaForVersion(crd *apiextensionsv1beta1.CustomResourceDefinition, version string) (*apiextensionsv1beta1.CustomResourceValidation, error) { + if !hasPerVersionSchema(crd.Spec.Versions) { + return crd.Spec.Validation, nil + } + if crd.Spec.Validation != nil { + return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version schemas must be mutual exclusive", crd.Name, version) + } + for _, v := range crd.Spec.Versions { + if version == v.Name { + return v.Schema, nil + } + } + return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name) +} + +// getSubresourcesForVersion returns the subresources for given version in given CRD. +func getSubresourcesForVersion(crd *apiextensionsv1beta1.CustomResourceDefinition, version string) (*apiextensionsv1beta1.CustomResourceSubresources, error) { + if !hasPerVersionSubresources(crd.Spec.Versions) { + return crd.Spec.Subresources, nil + } + if crd.Spec.Subresources != nil { + return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version subresources must be mutual exclusive", crd.Name, version) + } + for _, v := range crd.Spec.Versions { + if version == v.Name { + return v.Subresources, nil + } + } + return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name) +} + +// getColumnsForVersion returns the columns for given version in given CRD. +// NOTE: the newly logically-defaulted columns is not pointing to the original CRD object. +// One cannot mutate the original CRD columns using the logically-defaulted columns. Please iterate through +// the original CRD object instead. +func getColumnsForVersion(crd *apiextensionsv1beta1.CustomResourceDefinition, version string) ([]apiextensionsv1beta1.CustomResourceColumnDefinition, error) { + if !hasPerVersionColumns(crd.Spec.Versions) { + return serveDefaultColumnsIfEmpty(crd.Spec.AdditionalPrinterColumns), nil + } + if len(crd.Spec.AdditionalPrinterColumns) > 0 { + return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version additionalPrinterColumns must be mutual exclusive", crd.Name, version) + } + for _, v := range crd.Spec.Versions { + if version == v.Name { + return serveDefaultColumnsIfEmpty(v.AdditionalPrinterColumns), nil + } + } + return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name) +} + +// serveDefaultColumnsIfEmpty applies logically defaulting to columns, if the input columns is empty. +// NOTE: in this way, the newly logically-defaulted columns is not pointing to the original CRD object. +// One cannot mutate the original CRD columns using the logically-defaulted columns. Please iterate through +// the original CRD object instead. +func serveDefaultColumnsIfEmpty(columns []apiextensionsv1beta1.CustomResourceColumnDefinition) []apiextensionsv1beta1.CustomResourceColumnDefinition { + if len(columns) > 0 { + return columns + } + return []apiextensionsv1beta1.CustomResourceColumnDefinition{ + {Name: "Age", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"], JSONPath: ".metadata.creationTimestamp"}, + } +} + +// hasPerVersionSchema returns true if a CRD uses per-version schema. +func hasPerVersionSchema(versions []apiextensionsv1beta1.CustomResourceDefinitionVersion) bool { + for _, v := range versions { + if v.Schema != nil { + return true + } + } + return false +} + +// hasPerVersionSubresources returns true if a CRD uses per-version subresources. +func hasPerVersionSubresources(versions []apiextensionsv1beta1.CustomResourceDefinitionVersion) bool { + for _, v := range versions { + if v.Subresources != nil { + return true + } + } + return false +} + +// hasPerVersionColumns returns true if a CRD uses per-version columns. +func hasPerVersionColumns(versions []apiextensionsv1beta1.CustomResourceDefinitionVersion) bool { + for _, v := range versions { + if len(v.AdditionalPrinterColumns) > 0 { + return true + } + } + return false +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go index 3c0f8976456..c3a7c1cb214 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/subresources_test.go @@ -17,6 +17,7 @@ limitations under the License. package integration import ( + "fmt" "math" "reflect" "sort" @@ -29,49 +30,108 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + utilfeature "k8s.io/apiserver/pkg/util/feature" + utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" "k8s.io/client-go/dynamic" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) var labelSelectorPath = ".status.labelSelector" +var anotherLabelSelectorPath = ".status.anotherLabelSelector" -func NewNoxuSubresourcesCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition { - return &apiextensionsv1beta1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, - Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ - Group: "mygroup.example.com", - Version: "v1beta1", - Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ - Plural: "noxus", - Singular: "nonenglishnoxu", - Kind: "WishIHadChosenNoxu", - ShortNames: []string{"foo", "bar", "abc", "def"}, - ListKind: "NoxuItemList", +func NewNoxuSubresourcesCRDs(scope apiextensionsv1beta1.ResourceScope) []*apiextensionsv1beta1.CustomResourceDefinition { + return []*apiextensionsv1beta1.CustomResourceDefinition{ + // CRD that uses top-level subresources + { + ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, + Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ + Group: "mygroup.example.com", + Version: "v1beta1", + Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ + Plural: "noxus", + Singular: "nonenglishnoxu", + Kind: "WishIHadChosenNoxu", + ShortNames: []string{"foo", "bar", "abc", "def"}, + ListKind: "NoxuItemList", + }, + Scope: scope, + Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ + { + Name: "v1beta1", + Served: true, + Storage: true, + }, + { + Name: "v1", + Served: true, + Storage: false, + }, + }, + Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ + Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, + Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + LabelSelectorPath: &labelSelectorPath, + }, + }, }, - Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ - {Name: "v1beta1", Served: true, Storage: false}, - {Name: "v1", Served: true, Storage: true}, - }, - Scope: scope, - Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ - Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, - Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{ - SpecReplicasPath: ".spec.replicas", - StatusReplicasPath: ".status.replicas", - LabelSelectorPath: &labelSelectorPath, + }, + // CRD that uses per-version subresources + { + ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, + Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ + Group: "mygroup.example.com", + Version: "v1beta1", + Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ + Plural: "noxus", + Singular: "nonenglishnoxu", + Kind: "WishIHadChosenNoxu", + ShortNames: []string{"foo", "bar", "abc", "def"}, + ListKind: "NoxuItemList", + }, + Scope: scope, + Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ + { + Name: "v1beta1", + Served: true, + Storage: true, + Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ + Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, + Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + LabelSelectorPath: &labelSelectorPath, + }, + }, + }, + { + Name: "v1", + Served: true, + Storage: false, + Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ + Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, + Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + LabelSelectorPath: &anotherLabelSelectorPath, + }, + }, + }, }, }, }, } } -func NewNoxuSubresourceInstance(namespace, name string) *unstructured.Unstructured { +func NewNoxuSubresourceInstance(namespace, name, version string) *unstructured.Unstructured { return &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": "mygroup.example.com/v1beta1", + "apiVersion": fmt.Sprintf("mygroup.example.com/%s", version), "kind": "WishIHadChosenNoxu", "metadata": map[string]interface{}{ "namespace": namespace, @@ -89,112 +149,120 @@ func NewNoxuSubresourceInstance(namespace, name string) *unstructured.Unstructur } func TestStatusSubresource(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() - noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } + noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) + for _, noxuDefinition := range noxuDefinitions { + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } - ns := "not-the-default" - noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) - _, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition) - if err != nil { - t.Fatalf("unable to create noxu instance: %v", err) - } + ns := "not-the-default" + for _, v := range noxuDefinition.Spec.Versions { + noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) + _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name) + if err != nil { + t.Fatalf("unable to create noxu instance: %v", err) + } + gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + // status should not be set after creation + if val, ok := gottenNoxuInstance.Object["status"]; ok { + t.Fatalf("status should not be set after creation, got %v", val) + } - gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) - if err != nil { - t.Fatal(err) - } + // .status.num = 20 + err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // status should not be set after creation - if val, ok := gottenNoxuInstance.Object["status"]; ok { - t.Fatalf("status should not be set after creation, got %v", val) - } + // .spec.num = 20 + err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // .status.num = 20 - err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + // UpdateStatus should not update spec. + // Check that .spec.num = 10 and .status.num = 20 + updatedStatusInstance, err := noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{}) + if err != nil { + t.Fatalf("unable to update status: %v", err) + } - // .spec.num = 20 - err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num") + if !found || err != nil { + t.Fatalf("unable to get .spec.num") + } + if specNum != int64(10) { + t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum) + } - // UpdateStatus should not update spec. - // Check that .spec.num = 10 and .status.num = 20 - updatedStatusInstance, err := noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{}) - if err != nil { - t.Fatalf("unable to update status: %v", err) - } + statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num") + if !found || err != nil { + t.Fatalf("unable to get .status.num") + } + if statusNum != int64(20) { + t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum) + } - specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num") - if !found || err != nil { - t.Fatalf("unable to get .spec.num") - } - if specNum != int64(10) { - t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum) - } + gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } - statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num") - if !found || err != nil { - t.Fatalf("unable to get .status.num") - } - if statusNum != int64(20) { - t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum) - } + // .status.num = 40 + err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{}) - if err != nil { - t.Fatal(err) - } + // .spec.num = 40 + err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // .status.num = 40 - err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + // Update should not update status. + // Check that .spec.num = 40 and .status.num = 20 + updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) + if err != nil { + t.Fatalf("unable to update instance: %v", err) + } - // .spec.num = 40 - err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num") + if !found || err != nil { + t.Fatalf("unable to get .spec.num") + } + if specNum != int64(40) { + t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum) + } - // Update should not update status. - // Check that .spec.num = 40 and .status.num = 20 - updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) - if err != nil { - t.Fatalf("unable to update instance: %v", err) - } - - specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num") - if !found || err != nil { - t.Fatalf("unable to get .spec.num") - } - if specNum != int64(40) { - t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum) - } - - statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num") - if !found || err != nil { - t.Fatalf("unable to get .status.num") - } - if statusNum != int64(20) { - t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum) + statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num") + if !found || err != nil { + t.Fatalf("unable to get .status.num") + } + if statusNum != int64(20) { + t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum) + } + noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}) + } + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } } } func TestScaleSubresource(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() groupResource := schema.GroupResource{ Group: "mygroup.example.com", Resource: "noxus", @@ -215,117 +283,132 @@ func TestScaleSubresource(t *testing.T) { t.Fatal(err) } - noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) + noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) + for _, noxuDefinition := range noxuDefinitions { + for _, v := range noxuDefinition.Spec.Versions { + // Start with a new CRD, so that the object doesn't have resourceVersion + noxuDefinition := noxuDefinition.DeepCopy() - // set invalid json path for specReplicasPath - noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = "foo,bar" - _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err == nil { - t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec") - } + subresources, err := getSubresourcesForVersion(noxuDefinition, v.Name) + if err != nil { + t.Fatal(err) + } + // set invalid json path for specReplicasPath + subresources.Scale.SpecReplicasPath = "foo,bar" + _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err == nil { + t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec") + } - noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = ".spec.replicas" - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } + subresources.Scale.SpecReplicasPath = ".spec.replicas" + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } - ns := "not-the-default" - noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) - _, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition) - if err != nil { - t.Fatalf("unable to create noxu instance: %v", err) - } + ns := "not-the-default" + noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) + _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name) + if err != nil { + t.Fatalf("unable to create noxu instance: %v", err) + } - scaleClient, err := fixtures.CreateNewScaleClient(noxuDefinition, config) - if err != nil { - t.Fatal(err) - } + scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name) + if err != nil { + t.Fatal(err) + } - // set .status.labelSelector = bar - gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) - if err != nil { - t.Fatal(err) - } - err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", "status", "labelSelector") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - _, err = noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{}) - if err != nil { - t.Fatalf("unable to update status: %v", err) - } + // set .status.labelSelector = bar + gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + _, err = noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{}) + if err != nil { + t.Fatalf("unable to update status: %v", err) + } - // get the scale object - gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo") - if err != nil { - t.Fatal(err) - } - if gottenScale.Spec.Replicas != 3 { - t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas) - } - if gottenScale.Status.Selector != "bar" { - t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector) - } + // get the scale object + gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo") + if err != nil { + t.Fatal(err) + } + if gottenScale.Spec.Replicas != 3 { + t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas) + } + if gottenScale.Status.Selector != "bar" { + t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector) + } - // check self link - expectedSelfLink := "/apis/mygroup.example.com/v1beta1/namespaces/not-the-default/noxus/foo/scale" - if gottenScale.GetSelfLink() != expectedSelfLink { - t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink()) - } + // check self link + expectedSelfLink := fmt.Sprintf("/apis/mygroup.example.com/%s/namespaces/not-the-default/noxus/foo/scale", v.Name) + if gottenScale.GetSelfLink() != expectedSelfLink { + t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink()) + } - // update the scale object - // check that spec is updated, but status is not - gottenScale.Spec.Replicas = 5 - gottenScale.Status.Selector = "baz" - updatedScale, err := scaleClient.Scales("not-the-default").Update(groupResource, gottenScale) - if err != nil { - t.Fatal(err) - } - if updatedScale.Spec.Replicas != 5 { - t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas) - } - if updatedScale.Status.Selector != "bar" { - t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector) - } + // update the scale object + // check that spec is updated, but status is not + gottenScale.Spec.Replicas = 5 + gottenScale.Status.Selector = "baz" + updatedScale, err := scaleClient.Scales("not-the-default").Update(groupResource, gottenScale) + if err != nil { + t.Fatal(err) + } + if updatedScale.Spec.Replicas != 5 { + t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas) + } + if updatedScale.Status.Selector != "bar" { + t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector) + } - // check that .spec.replicas = 5, but status is not updated - updatedNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) - if err != nil { - t.Fatal(err) - } - specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas") - if !found || err != nil { - t.Fatalf("unable to get .spec.replicas") - } - if specReplicas != 5 { - t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas) - } - statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, "status", "labelSelector") - if !found || err != nil { - t.Fatalf("unable to get .status.labelSelector") - } - if statusLabelSelector != "bar" { - t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector) - } + // check that .spec.replicas = 5, but status is not updated + updatedNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas") + if !found || err != nil { + t.Fatalf("unable to get .spec.replicas") + } + if specReplicas != 5 { + t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas) + } + statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...) + if !found || err != nil { + t.Fatalf("unable to get %s", *subresources.Scale.LabelSelectorPath) + } + if statusLabelSelector != "bar" { + t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector) + } - // validate maximum value - // set .spec.replicas = math.MaxInt64 - gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{}) - if err != nil { - t.Fatal(err) - } - err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - _, err = noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) - if err == nil { - t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647") + // validate maximum value + // set .spec.replicas = math.MaxInt64 + gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + _, err = noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) + if err == nil { + t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647") + } + noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}) + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } + } } } func TestValidationSchemaWithStatus(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) @@ -342,7 +425,7 @@ func TestValidationSchemaWithStatus(t *testing.T) { } // fields other than properties in root schema are not allowed - noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) + noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[0] noxuDefinition.Spec.Subresources = &apiextensionsv1beta1.CustomResourceSubresources{ Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, } @@ -373,6 +456,7 @@ func TestValidationSchemaWithStatus(t *testing.T) { } func TestValidateOnlyStatus(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) @@ -407,59 +491,79 @@ func TestValidateOnlyStatus(t *testing.T) { }, } - noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) - noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{ - OpenAPIV3Schema: schema, - } + noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) + for i, noxuDefinition := range noxuDefinitions { + if i == 0 { + noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{ + OpenAPIV3Schema: schema, + } + } else { + noxuDefinition.Spec.Versions[0].Schema = &apiextensionsv1beta1.CustomResourceValidation{ + OpenAPIV3Schema: schema, + } + schemaWithDescription := schema.DeepCopy() + schemaWithDescription.Description = "test" + noxuDefinition.Spec.Versions[1].Schema = &apiextensionsv1beta1.CustomResourceValidation{ + OpenAPIV3Schema: schemaWithDescription, + } + } - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } - ns := "not-the-default" - noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + ns := "not-the-default" + for _, v := range noxuDefinition.Spec.Versions { + noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) - // set .spec.num = 10 and .status.num = 10 - noxuInstance := NewNoxuSubresourceInstance(ns, "foo") - err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + // set .spec.num = 10 and .status.num = 10 + noxuInstance := NewNoxuSubresourceInstance(ns, "foo", v.Name) + err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - createdNoxuInstance, err := instantiateCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition) - if err != nil { - t.Fatalf("unable to create noxu instance: %v", err) - } + createdNoxuInstance, err := instantiateVersionedCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition, v.Name) + if err != nil { + t.Fatalf("unable to create noxu instance: %v", err) + } - // update the spec with .spec.num = 15, expecting no error - err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "spec", "num") - if err != nil { - t.Fatalf("unexpected error setting .spec.num: %v", err) - } - createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance, metav1.UpdateOptions{}) - if err != nil { - t.Errorf("unexpected error: %v", err) - } + // update the spec with .spec.num = 15, expecting no error + err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "spec", "num") + if err != nil { + t.Fatalf("unexpected error setting .spec.num: %v", err) + } + createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance, metav1.UpdateOptions{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } - // update with .status.num = 15, expecting an error - err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "status", "num") - if err != nil { - t.Fatalf("unexpected error setting .status.num: %v", err) - } - createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance, metav1.UpdateOptions{}) - if err == nil { - t.Fatal("expected error, but got none") - } - statusError, isStatus := err.(*apierrors.StatusError) - if !isStatus || statusError == nil { - t.Fatalf("expected status error, got %T: %v", err, err) - } - if !strings.Contains(statusError.Error(), "Invalid value") { - t.Fatalf("expected 'Invalid value' in error, got: %v", err) + // update with .status.num = 15, expecting an error + err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "status", "num") + if err != nil { + t.Fatalf("unexpected error setting .status.num: %v", err) + } + createdNoxuInstance, err = noxuResourceClient.UpdateStatus(createdNoxuInstance, metav1.UpdateOptions{}) + if err == nil { + t.Fatal("expected error, but got none") + } + statusError, isStatus := err.(*apierrors.StatusError) + if !isStatus || statusError == nil { + t.Fatalf("expected status error, got %T: %v", err, err) + } + if !strings.Contains(statusError.Error(), "Invalid value") { + t.Fatalf("expected 'Invalid value' in error, got: %v", err) + } + noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}) + } + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } } } func TestSubresourcesDiscovery(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) @@ -475,140 +579,157 @@ func TestSubresourcesDiscovery(t *testing.T) { t.Fatal(err) } - noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } + noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) + for _, noxuDefinition := range noxuDefinitions { + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } - group := "mygroup.example.com" - version := "v1beta1" + for _, v := range noxuDefinition.Spec.Versions { + group := "mygroup.example.com" + version := v.Name - resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version) - if err != nil { - t.Fatal(err) - } + resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version) + if err != nil { + t.Fatal(err) + } - if len(resources.APIResources) != 3 { - 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) - } + if len(resources.APIResources) != 3 { + 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) + } - // check discovery info for status - status := resources.APIResources[1] + // check discovery info for status + status := resources.APIResources[1] - if status.Name != "noxus/status" { - t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name) - } + if status.Name != "noxus/status" { + t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name) + } - if status.Namespaced != true { - t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced) - } + if status.Namespaced != true { + t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced) + } - if status.Kind != "WishIHadChosenNoxu" { - t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind) - } + if status.Kind != "WishIHadChosenNoxu" { + t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind) + } - expectedVerbs := []string{"get", "patch", "update"} - sort.Strings(status.Verbs) - if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) { - t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs) - } + expectedVerbs := []string{"get", "patch", "update"} + sort.Strings(status.Verbs) + if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) { + t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs) + } - // check discovery info for scale - scale := resources.APIResources[2] + // check discovery info for scale + scale := resources.APIResources[2] - if scale.Group != autoscaling.GroupName { - t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group) - } + if scale.Group != autoscaling.GroupName { + t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group) + } - if scale.Version != "v1" { - t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version) - } + if scale.Version != "v1" { + t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version) + } - if scale.Name != "noxus/scale" { - t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name) - } + if scale.Name != "noxus/scale" { + t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name) + } - if scale.Namespaced != true { - t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced) - } + if scale.Namespaced != true { + t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced) + } - if scale.Kind != "Scale" { - t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind) - } + if scale.Kind != "Scale" { + t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind) + } - sort.Strings(scale.Verbs) - if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) { - t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs) + sort.Strings(scale.Verbs) + if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) { + t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs) + } + } + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } } } func TestGeneration(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() - noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } + noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) + for _, noxuDefinition := range noxuDefinitions { + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } - ns := "not-the-default" - noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) - _, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition) - if err != nil { - t.Fatalf("unable to create noxu instance: %v", err) - } + ns := "not-the-default" + for _, v := range noxuDefinition.Spec.Versions { + noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) + _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name) + if err != nil { + t.Fatalf("unable to create noxu instance: %v", err) + } - // .metadata.generation = 1 - gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) - if err != nil { - t.Fatal(err) - } - if gottenNoxuInstance.GetGeneration() != 1 { - t.Fatalf(".metadata.generation should be 1 after creation") - } + // .metadata.generation = 1 + gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + if gottenNoxuInstance.GetGeneration() != 1 { + t.Fatalf(".metadata.generation should be 1 after creation") + } - // .status.num = 20 - err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + // .status.num = 20 + err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // UpdateStatus does not increment generation - updatedStatusInstance, err := noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{}) - if err != nil { - t.Fatalf("unable to update status: %v", err) - } - if updatedStatusInstance.GetGeneration() != 1 { - t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration()) - } + // UpdateStatus does not increment generation + updatedStatusInstance, err := noxuResourceClient.UpdateStatus(gottenNoxuInstance, metav1.UpdateOptions{}) + if err != nil { + t.Fatalf("unable to update status: %v", err) + } + if updatedStatusInstance.GetGeneration() != 1 { + t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration()) + } - gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{}) - if err != nil { - t.Fatal(err) - } + gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } - // .spec.num = 20 - err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + // .spec.num = 20 + err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // Update increments generation - updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) - if err != nil { - t.Fatalf("unable to update instance: %v", err) - } - if updatedInstance.GetGeneration() != 2 { - t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration()) + // Update increments generation + updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) + if err != nil { + t.Fatalf("unable to update instance: %v", err) + } + if updatedInstance.GetGeneration() != 2 { + t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration()) + } + noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}) + } + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } } } func TestSubresourcePatch(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() groupResource := schema.GroupResource{ Group: "mygroup.example.com", Resource: "noxus", @@ -629,146 +750,155 @@ func TestSubresourcePatch(t *testing.T) { t.Fatal(err) } - noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped) - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } + noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) + for _, noxuDefinition := range noxuDefinitions { + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } - ns := "not-the-default" - noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) + ns := "not-the-default" + for _, v := range noxuDefinition.Spec.Versions { + noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) - t.Logf("Creating foo") - _, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition) - if err != nil { - t.Fatalf("unable to create noxu instance: %v", err) - } + t.Logf("Creating foo") + _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name) + if err != nil { + t.Fatalf("unable to create noxu instance: %v", err) + } - scaleClient, err := fixtures.CreateNewScaleClient(noxuDefinition, config) - if err != nil { - t.Fatal(err) - } + scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name) + if err != nil { + t.Fatal(err) + } - t.Logf("Patching .status.num to 999") - patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`) - patchedNoxuInstance, err := noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + t.Logf("Patching .status.num to 999") + patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`) + patchedNoxuInstance, err := noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") // .status.num should be 999 - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") // .spec.num should remain 10 - rv, found, err := unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion") - if err != nil { - t.Fatal(err) - } - if !found { - t.Fatalf("metadata.resourceVersion not found") - } + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") // .status.num should be 999 + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") // .spec.num should remain 10 + rv, found, err := unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion") + if err != nil { + t.Fatal(err) + } + if !found { + t.Fatalf("metadata.resourceVersion not found") + } - // this call waits for the resourceVersion to be reached in the cache before returning. - // We need to do this because the patch gets its initial object from the storage, and the cache serves that. - // If it is out of date, then our initial patch is applied to an old resource version, which conflicts - // and then the updated object shows a conflicting diff, which permanently fails the patch. - // This gives expected stability in the patch without retrying on an known number of conflicts below in the test. - // See https://issue.k8s.io/42644 - _, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + // this call waits for the resourceVersion to be reached in the cache before returning. + // We need to do this because the patch gets its initial object from the storage, and the cache serves that. + // If it is out of date, then our initial patch is applied to an old resource version, which conflicts + // and then the updated object shows a conflicting diff, which permanently fails the patch. + // This gives expected stability in the patch without retrying on an known number of conflicts below in the test. + // See https://issue.k8s.io/42644 + _, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // no-op patch - t.Logf("Patching .status.num again to 999") - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - // make sure no-op patch does not increment resourceVersion - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") - expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") + // no-op patch + t.Logf("Patching .status.num again to 999") + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "status") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // make sure no-op patch does not increment resourceVersion + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") + expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") - // empty patch - t.Logf("Applying empty patch") - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "status") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - // an empty patch is a no-op patch. make sure it does not increment resourceVersion - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") - expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") + // empty patch + t.Logf("Applying empty patch") + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "status") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - t.Logf("Patching .spec.replicas to 7") - patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`) - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + // an empty patch is a no-op patch. make sure it does not increment resourceVersion + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") + expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") // .status.replicas should remain 0 - rv, found, err = unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion") - if err != nil { - t.Fatal(err) - } - if !found { - t.Fatalf("metadata.resourceVersion not found") - } + t.Logf("Patching .spec.replicas to 7") + patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`) + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // this call waits for the resourceVersion to be reached in the cache before returning. - // We need to do this because the patch gets its initial object from the storage, and the cache serves that. - // If it is out of date, then our initial patch is applied to an old resource version, which conflicts - // and then the updated object shows a conflicting diff, which permanently fails the patch. - // This gives expected stability in the patch without retrying on an known number of conflicts below in the test. - // See https://issue.k8s.io/42644 - _, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()}) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") // .status.replicas should remain 0 + rv, found, err = unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion") + if err != nil { + t.Fatal(err) + } + if !found { + t.Fatalf("metadata.resourceVersion not found") + } - // Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 0 - gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo") - if err != nil { - t.Fatal(err) - } - if gottenScale.Spec.Replicas != 7 { - t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas) - } - if gottenScale.Status.Replicas != 0 { - t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas) - } + // this call waits for the resourceVersion to be reached in the cache before returning. + // We need to do this because the patch gets its initial object from the storage, and the cache serves that. + // If it is out of date, then our initial patch is applied to an old resource version, which conflicts + // and then the updated object shows a conflicting diff, which permanently fails the patch. + // This gives expected stability in the patch without retrying on an known number of conflicts below in the test. + // See https://issue.k8s.io/42644 + _, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } - // no-op patch - t.Logf("Patching .spec.replicas again to 7") - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - // make sure no-op patch does not increment resourceVersion - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") - expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") + // Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 0 + gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo") + if err != nil { + t.Fatal(err) + } + if gottenScale.Spec.Replicas != 7 { + t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas) + } + if gottenScale.Status.Replicas != 0 { + t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas) + } - // empty patch - t.Logf("Applying empty patch") - patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "scale") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - // an empty patch is a no-op patch. make sure it does not increment resourceVersion - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") - expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") - expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") + // no-op patch + t.Logf("Patching .spec.replicas again to 7") + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, patch, metav1.UpdateOptions{}, "scale") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // make sure no-op patch does not increment resourceVersion + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") + expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") - // make sure strategic merge patch is not supported for both status and scale - _, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "status") - if err == nil { - t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources") - } + // empty patch + t.Logf("Applying empty patch") + patchedNoxuInstance, err = noxuResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`), metav1.UpdateOptions{}, "scale") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // an empty patch is a no-op patch. make sure it does not increment resourceVersion + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") + expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") + expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") - _, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "scale") - if err == nil { - t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources") + // make sure strategic merge patch is not supported for both status and scale + _, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "status") + if err == nil { + t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources") + } + + _, err = noxuResourceClient.Patch("foo", types.StrategicMergePatchType, patch, metav1.UpdateOptions{}, "scale") + if err == nil { + t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources") + } + noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}) + } + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } } } diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go index 7aab4f4086d..a10a72ecca4 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/table_test.go @@ -28,10 +28,14 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" + utilfeature "k8s.io/apiserver/pkg/util/feature" + utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) @@ -48,12 +52,32 @@ func newTableCRD() *apiextensionsv1beta1.CustomResourceDefinition { ListKind: "TablemList", }, Scope: apiextensionsv1beta1.ClusterScoped, - AdditionalPrinterColumns: []apiextensionsv1beta1.CustomResourceColumnDefinition{ - {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"}, - {Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"}, - {Name: "Beta", Type: "integer", Description: "the beta field", Format: "int64", Priority: 42, JSONPath: ".spec.beta"}, - {Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values", JSONPath: ".spec.gamma"}, - {Name: "Epsilon", Type: "string", Description: "an array of integers as string", JSONPath: ".spec.epsilon"}, + Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ + { + Name: "v1beta1", + Served: true, + Storage: false, + AdditionalPrinterColumns: []apiextensionsv1beta1.CustomResourceColumnDefinition{ + {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"}, + {Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"}, + {Name: "Beta", Type: "integer", Description: "the beta field", Format: "int64", Priority: 42, JSONPath: ".spec.beta"}, + {Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values", JSONPath: ".spec.gamma"}, + {Name: "Epsilon", Type: "string", Description: "an array of integers as string", JSONPath: ".spec.epsilon"}, + }, + }, + { + Name: "v1", + Served: true, + Storage: true, + AdditionalPrinterColumns: []apiextensionsv1beta1.CustomResourceColumnDefinition{ + {Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"}, + {Name: "Alpha", Type: "string", JSONPath: ".spec.alpha"}, + {Name: "Beta", Type: "integer", Description: "the beta field", Format: "int64", Priority: 42, JSONPath: ".spec.beta"}, + {Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values", JSONPath: ".spec.gamma"}, + {Name: "Epsilon", Type: "string", Description: "an array of integers as string", JSONPath: ".spec.epsilon"}, + {Name: "Zeta", Type: "integer", Description: "the zeta field", Format: "int64", Priority: 42, JSONPath: ".spec.zeta"}, + }, + }, }, }, } @@ -62,7 +86,7 @@ func newTableCRD() *apiextensionsv1beta1.CustomResourceDefinition { func newTableInstance(name string) *unstructured.Unstructured { return &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": "mygroup.example.com/v1beta1", + "apiVersion": "mygroup.example.com/v1", "kind": "Table", "metadata": map[string]interface{}{ "name": name, @@ -73,12 +97,14 @@ func newTableInstance(name string) *unstructured.Unstructured { "gamma": "bar", "delta": "hello", "epsilon": []int64{1, 2, 3}, + "zeta": 5, }, }, } } func TestTableGet(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) @@ -107,107 +133,219 @@ func TestTableGet(t *testing.T) { } t.Logf("table crd created: %#v", crd) - crClient := newNamespacedCustomResourceClient("", dynamicClient, crd) + crClient := newNamespacedCustomResourceVersionedClient("", dynamicClient, crd, "v1") foo, err := crClient.Create(newTableInstance("foo"), metav1.CreateOptions{}) if err != nil { t.Fatalf("unable to create noxu instance: %v", err) } t.Logf("foo created: %#v", foo.UnstructuredContent()) - gv := schema.GroupVersion{Group: crd.Spec.Group, Version: crd.Spec.Version} - gvk := gv.WithKind(crd.Spec.Names.Kind) + for i, v := range crd.Spec.Versions { + gv := schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name} + gvk := gv.WithKind(crd.Spec.Names.Kind) - scheme := runtime.NewScheme() - codecs := serializer.NewCodecFactory(scheme) - parameterCodec := runtime.NewParameterCodec(scheme) - metav1.AddToGroupVersion(scheme, gv) - scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) - scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + scheme := runtime.NewScheme() + codecs := serializer.NewCodecFactory(scheme) + parameterCodec := runtime.NewParameterCodec(scheme) + metav1.AddToGroupVersion(scheme, gv) + scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) - crConfig := *config - crConfig.GroupVersion = &gv - crConfig.APIPath = "/apis" - crConfig.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs} - crRestClient, err := rest.RESTClientFor(&crConfig) + crConfig := *config + crConfig.GroupVersion = &gv + crConfig.APIPath = "/apis" + crConfig.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: codecs} + crRestClient, err := rest.RESTClientFor(&crConfig) + if err != nil { + t.Fatal(err) + } + + ret, err := crRestClient.Get(). + Resource(crd.Spec.Names.Plural). + SetHeader("Accept", fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)). + VersionedParams(&metav1beta1.TableOptions{}, parameterCodec). + Do(). + Get() + if err != nil { + t.Fatalf("failed to list %v resources: %v", gvk, err) + } + + tbl, ok := ret.(*metav1beta1.Table) + if !ok { + t.Fatalf("expected metav1beta1.Table, got %T", ret) + } + t.Logf("%v table list: %#v", gvk, tbl) + + columns, err := getColumnsForVersion(crd, v.Name) + if err != nil { + t.Fatal(err) + } + expectColumnNum := len(columns) + 1 + if got, expected := len(tbl.ColumnDefinitions), expectColumnNum; got != expected { + t.Errorf("expected %d headers, got %d", expected, got) + } else { + age := metav1beta1.TableColumnDefinition{Name: "Age", Type: "date", Format: "", Description: "Custom resource definition column (in JSONPath format): .metadata.creationTimestamp", Priority: 0} + if got, expected := tbl.ColumnDefinitions[1], age; got != expected { + t.Errorf("expected column definition %#v, got %#v", expected, got) + } + + alpha := metav1beta1.TableColumnDefinition{Name: "Alpha", Type: "string", Format: "", Description: "Custom resource definition column (in JSONPath format): .spec.alpha", Priority: 0} + if got, expected := tbl.ColumnDefinitions[2], alpha; got != expected { + t.Errorf("expected column definition %#v, got %#v", expected, got) + } + + beta := metav1beta1.TableColumnDefinition{Name: "Beta", Type: "integer", Format: "int64", Description: "the beta field", Priority: 42} + if got, expected := tbl.ColumnDefinitions[3], beta; got != expected { + t.Errorf("expected column definition %#v, got %#v", expected, got) + } + + gamma := metav1beta1.TableColumnDefinition{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values"} + if got, expected := tbl.ColumnDefinitions[4], gamma; got != expected { + t.Errorf("expected column definition %#v, got %#v", expected, got) + } + + epsilon := metav1beta1.TableColumnDefinition{Name: "Epsilon", Type: "string", Description: "an array of integers as string"} + if got, expected := tbl.ColumnDefinitions[5], epsilon; got != expected { + t.Errorf("expected column definition %#v, got %#v", expected, got) + } + + // Validate extra column for v1 + if i == 1 { + zeta := metav1beta1.TableColumnDefinition{Name: "Zeta", Type: "integer", Format: "int64", Description: "the zeta field", Priority: 42} + if got, expected := tbl.ColumnDefinitions[6], zeta; got != expected { + t.Errorf("expected column definition %#v, got %#v", expected, got) + } + } + } + if got, expected := len(tbl.Rows), 1; got != expected { + t.Errorf("expected %d rows, got %d", expected, got) + } else if got, expected := len(tbl.Rows[0].Cells), expectColumnNum; got != expected { + t.Errorf("expected %d cells, got %d", expected, got) + } else { + if got, expected := tbl.Rows[0].Cells[0], "foo"; got != expected { + t.Errorf("expected cell[0] to equal %q, got %q", expected, got) + } + if s, ok := tbl.Rows[0].Cells[1].(string); !ok { + t.Errorf("expected cell[1] to be a string, got: %#v", tbl.Rows[0].Cells[1]) + } else { + dur, err := time.ParseDuration(s) + if err != nil { + t.Errorf("expected cell[1] to be a duration: %v", err) + } else if abs(dur.Seconds()) > 30.0 { + t.Errorf("expected cell[1] to be a small age, but got: %v", dur) + } + } + if got, expected := tbl.Rows[0].Cells[2], "foo_123"; got != expected { + t.Errorf("expected cell[2] to equal %q, got %q", expected, got) + } + if got, expected := tbl.Rows[0].Cells[3], int64(10); got != expected { + t.Errorf("expected cell[3] to equal %#v, got %#v", expected, got) + } + if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected { + t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got) + } + if got, expected := tbl.Rows[0].Cells[5], "[1 2 3]"; got != expected { + t.Errorf("expected cell[5] to equal %q, got %q", expected, got) + } + // Validate extra column for v1 + if i == 1 { + if got, expected := tbl.Rows[0].Cells[6], int64(5); got != expected { + t.Errorf("expected cell[6] to equal %q, got %q", expected, got) + } + } + } + } +} + +// TestColumnsPatch tests the case that a CRD was created with no top-level or +// per-version columns. One should be able to PATCH the CRD setting per-version columns. +func TestColumnsPatch(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() + tearDown, config, _, err := fixtures.StartDefaultServer(t) + if err != nil { + t.Fatal(err) + } + defer tearDown() + + apiExtensionClient, err := clientset.NewForConfig(config) if err != nil { t.Fatal(err) } - ret, err := crRestClient.Get(). - Resource(crd.Spec.Names.Plural). - SetHeader("Accept", fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName)). - VersionedParams(&metav1beta1.TableOptions{}, parameterCodec). - Do(). - Get() + dynamicClient, err := dynamic.NewForConfig(config) if err != nil { - t.Fatalf("failed to list %v resources: %v", gvk, err) + t.Fatal(err) } - tbl, ok := ret.(*metav1beta1.Table) - if !ok { - t.Fatalf("expected metav1beta1.Table, got %T", ret) + // CRD with no top-level and per-version columns should be created successfully + crd := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)[0] + crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) } - t.Logf("%v table list: %#v", gvk, tbl) - if got, expected := len(tbl.ColumnDefinitions), 6; got != expected { - t.Errorf("expected %d headers, got %d", expected, got) - } else { - age := metav1beta1.TableColumnDefinition{Name: "Age", Type: "date", Format: "", Description: "Custom resource definition column (in JSONPath format): .metadata.creationTimestamp", Priority: 0} - if got, expected := tbl.ColumnDefinitions[1], age; got != expected { - t.Errorf("expected column definition %#v, got %#v", expected, got) - } + // One should be able to patch the CRD to use per-version columns. The top-level columns + // should not be defaulted during creation, and apiserver should not return validation + // error about top-level and per-version columns being mutual exclusive. + patch := []byte(`{"spec":{"versions":[{"name":"v1beta1","served":true,"storage":true,"additionalPrinterColumns":[{"name":"Age","type":"date","JSONPath":".metadata.creationTimestamp"}]},{"name":"v1","served":true,"storage":false,"additionalPrinterColumns":[{"name":"Age2","type":"date","JSONPath":".metadata.creationTimestamp"}]}]}}`) - alpha := metav1beta1.TableColumnDefinition{Name: "Alpha", Type: "string", Format: "", Description: "Custom resource definition column (in JSONPath format): .spec.alpha", Priority: 0} - if got, expected := tbl.ColumnDefinitions[2], alpha; got != expected { - t.Errorf("expected column definition %#v, got %#v", expected, got) - } - - beta := metav1beta1.TableColumnDefinition{Name: "Beta", Type: "integer", Format: "int64", Description: "the beta field", Priority: 42} - if got, expected := tbl.ColumnDefinitions[3], beta; got != expected { - t.Errorf("expected column definition %#v, got %#v", expected, got) - } - - gamma := metav1beta1.TableColumnDefinition{Name: "Gamma", Type: "integer", Description: "a column with wrongly typed values"} - if got, expected := tbl.ColumnDefinitions[4], gamma; got != expected { - t.Errorf("expected column definition %#v, got %#v", expected, got) - } - - epsilon := metav1beta1.TableColumnDefinition{Name: "Epsilon", Type: "string", Description: "an array of integers as string"} - if got, expected := tbl.ColumnDefinitions[5], epsilon; got != expected { - t.Errorf("expected column definition %#v, got %#v", expected, got) - } + _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Patch(crd.Name, types.MergePatchType, patch) + if err != nil { + t.Fatal(err) } - if got, expected := len(tbl.Rows), 1; got != expected { - t.Errorf("expected %d rows, got %d", expected, got) - } else if got, expected := len(tbl.Rows[0].Cells), 6; got != expected { - t.Errorf("expected %d cells, got %d", expected, got) - } else { - if got, expected := tbl.Rows[0].Cells[0], "foo"; got != expected { - t.Errorf("expected cell[0] to equal %q, got %q", expected, got) - } - if s, ok := tbl.Rows[0].Cells[1].(string); !ok { - t.Errorf("expected cell[1] to be a string, got: %#v", tbl.Rows[0].Cells[1]) - } else { - dur, err := time.ParseDuration(s) - if err != nil { - t.Errorf("expected cell[1] to be a duration: %v", err) - } else if abs(dur.Seconds()) > 30.0 { - t.Errorf("expected cell[1] to be a small age, but got: %v", dur) - } - } - if got, expected := tbl.Rows[0].Cells[2], "foo_123"; got != expected { - t.Errorf("expected cell[2] to equal %q, got %q", expected, got) - } - if got, expected := tbl.Rows[0].Cells[3], int64(10); got != expected { - t.Errorf("expected cell[3] to equal %#v, got %#v", expected, got) - } - if got, expected := tbl.Rows[0].Cells[4], interface{}(nil); got != expected { - t.Errorf("expected cell[4] to equal %#v although the type does not match the column, got %#v", expected, got) - } - if got, expected := tbl.Rows[0].Cells[5], "[1 2 3]"; got != expected { - t.Errorf("expected cell[5] to equal %q, got %q", expected, got) - } + + crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{}) + if err != nil { + t.Fatal(err) } + t.Logf("columns crd patched: %#v", crd) +} + +// TestPatchCleanTopLevelColumns tests the case that a CRD was created with top-level columns. +// One should be able to PATCH the CRD cleaning the top-level columns and setting per-version +// columns. +func TestPatchCleanTopLevelColumns(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() + tearDown, config, _, err := fixtures.StartDefaultServer(t) + if err != nil { + t.Fatal(err) + } + defer tearDown() + + apiExtensionClient, err := clientset.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + crd := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped)[0] + crd.Spec.AdditionalPrinterColumns = []apiextensionsv1beta1.CustomResourceColumnDefinition{ + {Name: "Age", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"], JSONPath: ".metadata.creationTimestamp"}, + } + crd, err = fixtures.CreateNewCustomResourceDefinition(crd, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + t.Logf("columns crd created: %#v", crd) + + // One should be able to patch the CRD to use per-version columns by cleaning + // the top-level columns. + patch := []byte(`{"spec":{"additionalPrinterColumns":null,"versions":[{"name":"v1beta1","served":true,"storage":true,"additionalPrinterColumns":[{"name":"Age","type":"date","JSONPath":".metadata.creationTimestamp"}]},{"name":"v1","served":true,"storage":false,"additionalPrinterColumns":[{"name":"Age2","type":"date","JSONPath":".metadata.creationTimestamp"}]}]}}`) + + _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Patch(crd.Name, types.MergePatchType, patch) + if err != nil { + t.Fatal(err) + } + + crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } + t.Logf("columns crd patched: %#v", crd) } func abs(x float64) float64 { diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go index 37f9d094381..3c3322fcd01 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go @@ -17,6 +17,7 @@ limitations under the License. package integration import ( + "fmt" "strings" "testing" "time" @@ -25,8 +26,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/wait" + utilfeature "k8s.io/apiserver/pkg/util/feature" + utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) @@ -84,64 +88,114 @@ func TestForProperValidationErrors(t *testing.T) { } } -func newNoxuValidationCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition { - return &apiextensionsv1beta1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, - Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ - Group: "mygroup.example.com", - Version: "v1beta1", - Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ - Plural: "noxus", - Singular: "nonenglishnoxu", - Kind: "WishIHadChosenNoxu", - ShortNames: []string{"foo", "bar", "abc", "def"}, - ListKind: "NoxuItemList", +func newNoxuValidationCRDs(scope apiextensionsv1beta1.ResourceScope) []*apiextensionsv1beta1.CustomResourceDefinition { + validationSchema := &apiextensionsv1beta1.JSONSchemaProps{ + Required: []string{"alpha", "beta"}, + AdditionalProperties: &apiextensionsv1beta1.JSONSchemaPropsOrBool{ + Allows: true, + }, + Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ + "alpha": { + Description: "Alpha is an alphanumeric string with underscores", + Type: "string", + Pattern: "^[a-zA-Z0-9_]*$", }, - Scope: apiextensionsv1beta1.NamespaceScoped, - Validation: &apiextensionsv1beta1.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{ - Required: []string{"alpha", "beta"}, - AdditionalProperties: &apiextensionsv1beta1.JSONSchemaPropsOrBool{ - Allows: true, + "beta": { + Description: "Minimum value of beta is 10", + Type: "number", + Minimum: float64Ptr(10), + }, + "gamma": { + Description: "Gamma is restricted to foo, bar and baz", + Type: "string", + Enum: []apiextensionsv1beta1.JSON{ + { + Raw: []byte(`"foo"`), }, - Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ - "alpha": { - Description: "Alpha is an alphanumeric string with underscores", - Type: "string", - Pattern: "^[a-zA-Z0-9_]*$", + { + Raw: []byte(`"bar"`), + }, + { + Raw: []byte(`"baz"`), + }, + }, + }, + "delta": { + Description: "Delta is a string with a maximum length of 5 or a number with a minimum value of 0", + AnyOf: []apiextensionsv1beta1.JSONSchemaProps{ + { + Type: "string", + MaxLength: int64Ptr(5), + }, + { + Type: "number", + Minimum: float64Ptr(0), + }, + }, + }, + }, + } + validationSchemaWithDescription := validationSchema.DeepCopy() + validationSchemaWithDescription.Description = "test" + return []*apiextensionsv1beta1.CustomResourceDefinition{ + { + ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, + Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ + Group: "mygroup.example.com", + Version: "v1beta1", + Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ + Plural: "noxus", + Singular: "nonenglishnoxu", + Kind: "WishIHadChosenNoxu", + ShortNames: []string{"foo", "bar", "abc", "def"}, + ListKind: "NoxuItemList", + }, + Scope: apiextensionsv1beta1.NamespaceScoped, + Validation: &apiextensionsv1beta1.CustomResourceValidation{ + OpenAPIV3Schema: validationSchema, + }, + Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ + { + Name: "v1beta1", + Served: true, + Storage: true, + }, + { + Name: "v1", + Served: true, + Storage: false, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, + Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ + Group: "mygroup.example.com", + Version: "v1beta1", + Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ + Plural: "noxus", + Singular: "nonenglishnoxu", + Kind: "WishIHadChosenNoxu", + ShortNames: []string{"foo", "bar", "abc", "def"}, + ListKind: "NoxuItemList", + }, + Scope: apiextensionsv1beta1.NamespaceScoped, + Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ + { + Name: "v1beta1", + Served: true, + Storage: true, + Schema: &apiextensionsv1beta1.CustomResourceValidation{ + OpenAPIV3Schema: validationSchema, }, - "beta": { - Description: "Minimum value of beta is 10", - Type: "number", - Minimum: float64Ptr(10), - }, - "gamma": { - Description: "Gamma is restricted to foo, bar and baz", - Type: "string", - Enum: []apiextensionsv1beta1.JSON{ - { - Raw: []byte(`"foo"`), - }, - { - Raw: []byte(`"bar"`), - }, - { - Raw: []byte(`"baz"`), - }, - }, - }, - "delta": { - Description: "Delta is a string with a maximum length of 5 or a number with a minimum value of 0", - AnyOf: []apiextensionsv1beta1.JSONSchemaProps{ - { - Type: "string", - MaxLength: int64Ptr(5), - }, - { - Type: "number", - Minimum: float64Ptr(0), - }, - }, + }, + { + Name: "v1", + Served: true, + Storage: false, + Schema: &apiextensionsv1beta1.CustomResourceValidation{ + OpenAPIV3Schema: validationSchemaWithDescription, }, }, }, @@ -168,253 +222,320 @@ func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructure } func TestCustomResourceValidation(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() - noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } + noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) + for _, noxuDefinition := range noxuDefinitions { + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } - ns := "not-the-default" - noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) - _, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition) - if err != nil { - t.Fatalf("unable to create noxu instance: %v", err) + ns := "not-the-default" + for _, v := range noxuDefinition.Spec.Versions { + noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) + instanceToCreate := newNoxuValidationInstance(ns, "foo") + instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name) + _, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name) + if err != nil { + t.Fatalf("unable to create noxu instance: %v", err) + } + noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}) + } + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } } } func TestCustomResourceUpdateValidation(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() - noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } + noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) + for _, noxuDefinition := range noxuDefinitions { + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } - ns := "not-the-default" - noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) - _, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition) - if err != nil { - t.Fatalf("unable to create noxu instance: %v", err) - } + ns := "not-the-default" + for _, v := range noxuDefinition.Spec.Versions { + noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) + instanceToCreate := newNoxuValidationInstance(ns, "foo") + instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name) + _, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name) + if err != nil { + t.Fatalf("unable to create noxu instance: %v", err) + } - gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) - if err != nil { - t.Fatal(err) - } + gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{}) + if err != nil { + t.Fatal(err) + } - // invalidate the instance - gottenNoxuInstance.Object = map[string]interface{}{ - "apiVersion": "mygroup.example.com/v1beta1", - "kind": "WishIHadChosenNoxu", - "metadata": map[string]interface{}{ - "namespace": "not-the-default", - "name": "foo", - }, - "gamma": "bar", - "delta": "hello", - } + // invalidate the instance + gottenNoxuInstance.Object = map[string]interface{}{ + "apiVersion": "mygroup.example.com/v1beta1", + "kind": "WishIHadChosenNoxu", + "metadata": map[string]interface{}{ + "namespace": "not-the-default", + "name": "foo", + }, + "gamma": "bar", + "delta": "hello", + } - _, err = noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) - if err == nil { - t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance) + _, err = noxuResourceClient.Update(gottenNoxuInstance, metav1.UpdateOptions{}) + if err == nil { + t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance) + } + noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}) + } + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } } } func TestCustomResourceValidationErrors(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() - noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } - - ns := "not-the-default" - noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) - - tests := []struct { - name string - instanceFn func() *unstructured.Unstructured - expectedError string - }{ - { - name: "bad alpha", - instanceFn: func() *unstructured.Unstructured { - instance := newNoxuValidationInstance(ns, "foo") - instance.Object["alpha"] = "foo_123!" - return instance - }, - expectedError: "alpha in body should match '^[a-zA-Z0-9_]*$'", - }, - { - name: "bad beta", - instanceFn: func() *unstructured.Unstructured { - instance := newNoxuValidationInstance(ns, "foo") - instance.Object["beta"] = 5 - return instance - }, - expectedError: "beta in body should be greater than or equal to 10", - }, - { - name: "bad gamma", - instanceFn: func() *unstructured.Unstructured { - instance := newNoxuValidationInstance(ns, "foo") - instance.Object["gamma"] = "qux" - return instance - }, - expectedError: "gamma in body should be one of [foo bar baz]", - }, - { - name: "bad delta", - instanceFn: func() *unstructured.Unstructured { - instance := newNoxuValidationInstance(ns, "foo") - instance.Object["delta"] = "foobarbaz" - return instance - }, - expectedError: "must validate at least one schema (anyOf)\ndelta in body should be at most 5 chars long", - }, - { - name: "absent alpha and beta", - instanceFn: func() *unstructured.Unstructured { - instance := newNoxuValidationInstance(ns, "foo") - instance.Object = map[string]interface{}{ - "apiVersion": "mygroup.example.com/v1beta1", - "kind": "WishIHadChosenNoxu", - "metadata": map[string]interface{}{ - "namespace": "not-the-default", - "name": "foo", - }, - "gamma": "bar", - "delta": "hello", - } - return instance - }, - expectedError: ".alpha in body is required\n.beta in body is required", - }, - } - - for _, tc := range tests { - _, err := noxuResourceClient.Create(tc.instanceFn(), metav1.CreateOptions{}) - if err == nil { - t.Errorf("%v: expected %v", tc.name, tc.expectedError) - continue + noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) + for _, noxuDefinition := range noxuDefinitions { + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) } - // this only works when status errors contain the expect kind and version, so this effectively tests serializations too - if !strings.Contains(err.Error(), tc.expectedError) { - t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err) - continue + + ns := "not-the-default" + + tests := []struct { + name string + instanceFn func() *unstructured.Unstructured + expectedError string + }{ + { + name: "bad alpha", + instanceFn: func() *unstructured.Unstructured { + instance := newNoxuValidationInstance(ns, "foo") + instance.Object["alpha"] = "foo_123!" + return instance + }, + expectedError: "alpha in body should match '^[a-zA-Z0-9_]*$'", + }, + { + name: "bad beta", + instanceFn: func() *unstructured.Unstructured { + instance := newNoxuValidationInstance(ns, "foo") + instance.Object["beta"] = 5 + return instance + }, + expectedError: "beta in body should be greater than or equal to 10", + }, + { + name: "bad gamma", + instanceFn: func() *unstructured.Unstructured { + instance := newNoxuValidationInstance(ns, "foo") + instance.Object["gamma"] = "qux" + return instance + }, + expectedError: "gamma in body should be one of [foo bar baz]", + }, + { + name: "bad delta", + instanceFn: func() *unstructured.Unstructured { + instance := newNoxuValidationInstance(ns, "foo") + instance.Object["delta"] = "foobarbaz" + return instance + }, + expectedError: "must validate at least one schema (anyOf)\ndelta in body should be at most 5 chars long", + }, + { + name: "absent alpha and beta", + instanceFn: func() *unstructured.Unstructured { + instance := newNoxuValidationInstance(ns, "foo") + instance.Object = map[string]interface{}{ + "apiVersion": "mygroup.example.com/v1beta1", + "kind": "WishIHadChosenNoxu", + "metadata": map[string]interface{}{ + "namespace": "not-the-default", + "name": "foo", + }, + "gamma": "bar", + "delta": "hello", + } + return instance + }, + expectedError: ".alpha in body is required\n.beta in body is required", + }, + } + + for _, tc := range tests { + for _, v := range noxuDefinition.Spec.Versions { + noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) + instanceToCreate := tc.instanceFn() + instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name) + _, err := noxuResourceClient.Create(instanceToCreate, metav1.CreateOptions{}) + if err == nil { + t.Errorf("%v: expected %v", tc.name, tc.expectedError) + continue + } + // this only works when status errors contain the expect kind and version, so this effectively tests serializations too + if !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("%v: expected %v, got %v", tc.name, tc.expectedError, err) + continue + } + } + } + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) } } } func TestCRValidationOnCRDUpdate(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() - noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) + noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) + for i, noxuDefinition := range noxuDefinitions { + for _, v := range noxuDefinition.Spec.Versions { + // Re-define the CRD to make sure we start with a clean CRD + noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[i] + validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name) + if err != nil { + t.Fatal(err) + } - // set stricter schema - noxuDefinition.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta", "epsilon"} + // set stricter schema + validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta", "epsilon"} - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) - } - ns := "not-the-default" - noxuResourceClient := newNamespacedCustomResourceClient(ns, dynamicClient, noxuDefinition) + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + ns := "not-the-default" + noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) + instanceToCreate := newNoxuValidationInstance(ns, "foo") + instanceToCreate.Object["apiVersion"] = fmt.Sprintf("%s/%s", noxuDefinition.Spec.Group, v.Name) - // CR is rejected - _, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition) - if err == nil { - t.Fatalf("unexpected non-error: CR should be rejected") - } + // CR is rejected + _, err = instantiateVersionedCustomResource(t, instanceToCreate, noxuResourceClient, noxuDefinition, v.Name) + if err == nil { + t.Fatalf("unexpected non-error: CR should be rejected") + } - // update the CRD to a less stricter schema - _, err = updateCustomResourceDefinitionWithRetry(apiExtensionClient, "noxus.mygroup.example.com", func(crd *apiextensionsv1beta1.CustomResourceDefinition) { - crd.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta"} - }) - if err != nil { - t.Fatal(err) - } + // update the CRD to a less stricter schema + _, err = updateCustomResourceDefinitionWithRetry(apiExtensionClient, "noxus.mygroup.example.com", func(crd *apiextensionsv1beta1.CustomResourceDefinition) { + validationSchema, err := getSchemaForVersion(crd, v.Name) + if err != nil { + t.Fatal(err) + } + validationSchema.OpenAPIV3Schema.Required = []string{"alpha", "beta"} + }) + if err != nil { + t.Fatal(err) + } - // CR is now accepted - err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { - _, err := noxuResourceClient.Create(newNoxuValidationInstance(ns, "foo"), metav1.CreateOptions{}) - if statusError, isStatus := err.(*apierrors.StatusError); isStatus { - if strings.Contains(statusError.Error(), "is invalid") { - return false, nil + // CR is now accepted + err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { + _, err := noxuResourceClient.Create(instanceToCreate, metav1.CreateOptions{}) + if _, isStatus := err.(*apierrors.StatusError); isStatus { + if apierrors.IsInvalid(err) { + return false, nil + } + } + if err != nil { + return false, err + } + return true, nil + }) + if err != nil { + t.Fatal(err) + } + noxuResourceClient.Delete("foo", &metav1.DeleteOptions{}) + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) } } - if err != nil { - return false, err - } - return true, nil - }) - if err != nil { - t.Fatal(err) } } func TestForbiddenFieldsInSchema(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) if err != nil { t.Fatal(err) } defer tearDown() - noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) - noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = false + noxuDefinitions := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped) + for i, noxuDefinition := range noxuDefinitions { + for _, v := range noxuDefinition.Spec.Versions { + // Re-define the CRD to make sure we start with a clean CRD + noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[i] + validationSchema, err := getSchemaForVersion(noxuDefinition, v.Name) + if err != nil { + t.Fatal(err) + } + validationSchema.OpenAPIV3Schema.AdditionalProperties.Allows = false - _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err == nil { - t.Fatalf("unexpected non-error: additionalProperties cannot be set to false") - } + _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err == nil { + t.Fatalf("unexpected non-error: additionalProperties cannot be set to false") + } - noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ - Type: "array", - UniqueItems: true, - } - noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = true + validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ + Type: "array", + UniqueItems: true, + } + validationSchema.OpenAPIV3Schema.AdditionalProperties.Allows = true - _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err == nil { - t.Fatalf("unexpected non-error: uniqueItems cannot be set to true") - } + _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err == nil { + t.Fatalf("unexpected non-error: uniqueItems cannot be set to true") + } - noxuDefinition.Spec.Validation.OpenAPIV3Schema.Ref = strPtr("#/definition/zeta") - noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ - Type: "array", - UniqueItems: false, - } + validationSchema.OpenAPIV3Schema.Ref = strPtr("#/definition/zeta") + validationSchema.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{ + Type: "array", + UniqueItems: false, + } - _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err == nil { - t.Fatal("unexpected non-error: $ref cannot be non-empty string") - } + _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err == nil { + t.Fatal("unexpected non-error: $ref cannot be non-empty string") + } - noxuDefinition.Spec.Validation.OpenAPIV3Schema.Ref = nil + validationSchema.OpenAPIV3Schema.Ref = nil - noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) - if err != nil { - t.Fatal(err) + noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { + t.Fatal(err) + } + } } } diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go index 45e5176b000..1d3154d997f 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/yaml_test.go @@ -27,10 +27,13 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" + utilfeature "k8s.io/apiserver/pkg/util/feature" + utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing" "k8s.io/client-go/dynamic" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" "k8s.io/apiextensions-apiserver/test/integration/fixtures" ) @@ -354,6 +357,7 @@ values: } func TestYAMLSubresource(t *testing.T) { + defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() tearDown, config, _, err := fixtures.StartDefaultServer(t) if err != nil { t.Fatal(err) @@ -369,7 +373,7 @@ func TestYAMLSubresource(t *testing.T) { t.Fatal(err) } - noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.ClusterScoped) + noxuDefinition := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.ClusterScoped)[0] noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) if err != nil { t.Fatal(err)