Commit 52cdc336 authored by Haowei Cai's avatar Haowei Cai

Integration test (please review with -w to ignore

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