Add integration tests
Update test schema Add polling for TestCRValidationOnCRDUpdate Add tests for forbidden fields Enable featureGate for CustomResourceValidation
This commit is contained in:
@@ -32,6 +32,7 @@ go_test(
|
|||||||
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
|
||||||
|
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/dynamic:go_default_library",
|
"//vendor/k8s.io/client-go/dynamic:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ func checkForWatchCachePrimed(crd *apiextensionsv1beta1.CustomResourceDefinition
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceName := "foo"
|
instanceName := "setup-instance"
|
||||||
instance := &unstructured.Unstructured{
|
instance := &unstructured.Unstructured{
|
||||||
Object: map[string]interface{}{
|
Object: map[string]interface{}{
|
||||||
"apiVersion": crd.Spec.Group + "/" + crd.Spec.Version,
|
"apiVersion": crd.Spec.Group + "/" + crd.Spec.Version,
|
||||||
@@ -222,6 +222,11 @@ func checkForWatchCachePrimed(crd *apiextensionsv1beta1.CustomResourceDefinition
|
|||||||
"namespace": ns,
|
"namespace": ns,
|
||||||
"name": instanceName,
|
"name": instanceName,
|
||||||
},
|
},
|
||||||
|
"alpha": "foo_123",
|
||||||
|
"beta": 10,
|
||||||
|
"gamma": "bar",
|
||||||
|
"delta": "hello",
|
||||||
|
"epsilon": "foobar",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if _, err := resourceClient.Create(instance); err != nil {
|
if _, err := resourceClient.Create(instance); err != nil {
|
||||||
|
|||||||
@@ -19,10 +19,15 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
|
||||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||||
"k8s.io/apiextensions-apiserver/test/integration/testserver"
|
"k8s.io/apiextensions-apiserver/test/integration/testserver"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestForProperValidationErrors(t *testing.T) {
|
func TestForProperValidationErrors(t *testing.T) {
|
||||||
@@ -79,3 +84,368 @@ func TestForProperValidationErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newNoxuValidationCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
|
||||||
|
return &apiextensionsv1beta1.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
|
||||||
|
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
|
||||||
|
Group: "mygroup.example.com",
|
||||||
|
Version: "v1beta1",
|
||||||
|
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
|
||||||
|
Plural: "noxus",
|
||||||
|
Singular: "nonenglishnoxu",
|
||||||
|
Kind: "WishIHadChosenNoxu",
|
||||||
|
ShortNames: []string{"foo", "bar", "abc", "def"},
|
||||||
|
ListKind: "NoxuItemList",
|
||||||
|
},
|
||||||
|
Scope: apiextensionsv1beta1.NamespaceScoped,
|
||||||
|
Validation: &apiextensionsv1beta1.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
Required: []string{"alpha", "beta"},
|
||||||
|
AdditionalProperties: &apiextensionsv1beta1.JSONSchemaPropsOrBool{
|
||||||
|
Allows: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
"alpha": {
|
||||||
|
Description: "Alpha is an alphanumeric string with underscores",
|
||||||
|
Type: "string",
|
||||||
|
Pattern: "^[a-zA-Z0-9_]*$",
|
||||||
|
},
|
||||||
|
"beta": {
|
||||||
|
Description: "Minimum value of beta is 10",
|
||||||
|
Type: "number",
|
||||||
|
Minimum: float64Ptr(10),
|
||||||
|
},
|
||||||
|
"gamma": {
|
||||||
|
Description: "Gamma is restricted to foo, bar and baz",
|
||||||
|
Type: "string",
|
||||||
|
Enum: []apiextensionsv1beta1.JSON{
|
||||||
|
{
|
||||||
|
Raw: []byte(`"foo"`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Raw: []byte(`"bar"`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Raw: []byte(`"baz"`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"delta": {
|
||||||
|
Description: "Delta is a string with a maximum length of 5 or a number with a minimum value of 0",
|
||||||
|
AnyOf: []apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
{
|
||||||
|
Type: "string",
|
||||||
|
MaxLength: int64Ptr(5),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "number",
|
||||||
|
Minimum: float64Ptr(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructured {
|
||||||
|
return &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "mygroup.example.com/v1beta1",
|
||||||
|
"kind": "WishIHadChosenNoxu",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"namespace": namespace,
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
"alpha": "foo_123",
|
||||||
|
"beta": 10,
|
||||||
|
"gamma": "bar",
|
||||||
|
"delta": "hello",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomResourceValidation(t *testing.T) {
|
||||||
|
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
|
||||||
|
// enable alpha feature CustomResourceValidation
|
||||||
|
err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||||
|
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := "not-the-default"
|
||||||
|
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
|
||||||
|
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create noxu instance: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomResourceUpdateValidation(t *testing.T) {
|
||||||
|
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
|
||||||
|
// enable alpha feature CustomResourceValidation
|
||||||
|
err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||||
|
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := "not-the-default"
|
||||||
|
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
|
||||||
|
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create noxu instance: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalidate the instance
|
||||||
|
gottenNoxuInstance.Object = map[string]interface{}{
|
||||||
|
"apiVersion": "mygroup.example.com/v1beta1",
|
||||||
|
"kind": "WishIHadChosenNoxu",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"namespace": "not-the-default",
|
||||||
|
"name": "foo",
|
||||||
|
},
|
||||||
|
"gamma": "bar",
|
||||||
|
"delta": "hello",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = noxuResourceClient.Update(gottenNoxuInstance)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("unexpected non-error: alpha and beta should be present while updating %v", gottenNoxuInstance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomResourceValidationErrors(t *testing.T) {
|
||||||
|
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
|
||||||
|
// enable alpha feature CustomResourceValidation
|
||||||
|
err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||||
|
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ns := "not-the-default"
|
||||||
|
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
instanceFn func() *unstructured.Unstructured
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bad alpha",
|
||||||
|
instanceFn: func() *unstructured.Unstructured {
|
||||||
|
instance := newNoxuValidationInstance(ns, "foo")
|
||||||
|
instance.Object["alpha"] = "foo_123!"
|
||||||
|
return instance
|
||||||
|
},
|
||||||
|
expectedError: "alpha in body should match '^[a-zA-Z0-9_]*$'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad beta",
|
||||||
|
instanceFn: func() *unstructured.Unstructured {
|
||||||
|
instance := newNoxuValidationInstance(ns, "foo")
|
||||||
|
instance.Object["beta"] = 5
|
||||||
|
return instance
|
||||||
|
},
|
||||||
|
expectedError: "beta in body should be greater than or equal to 10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad gamma",
|
||||||
|
instanceFn: func() *unstructured.Unstructured {
|
||||||
|
instance := newNoxuValidationInstance(ns, "foo")
|
||||||
|
instance.Object["gamma"] = "qux"
|
||||||
|
return instance
|
||||||
|
},
|
||||||
|
expectedError: "gamma in body should be one of [foo bar baz]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bad delta",
|
||||||
|
instanceFn: func() *unstructured.Unstructured {
|
||||||
|
instance := newNoxuValidationInstance(ns, "foo")
|
||||||
|
instance.Object["delta"] = "foobarbaz"
|
||||||
|
return instance
|
||||||
|
},
|
||||||
|
expectedError: "must validate at least one schema (anyOf)\ndelta in body should be at most 5 chars long",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "absent alpha and beta",
|
||||||
|
instanceFn: func() *unstructured.Unstructured {
|
||||||
|
instance := newNoxuValidationInstance(ns, "foo")
|
||||||
|
instance.Object = map[string]interface{}{
|
||||||
|
"apiVersion": "mygroup.example.com/v1beta1",
|
||||||
|
"kind": "WishIHadChosenNoxu",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"namespace": "not-the-default",
|
||||||
|
"name": "foo",
|
||||||
|
},
|
||||||
|
"gamma": "bar",
|
||||||
|
"delta": "hello",
|
||||||
|
}
|
||||||
|
return instance
|
||||||
|
},
|
||||||
|
expectedError: ".alpha in body is required\n.beta in body is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
_, err := noxuResourceClient.Create(tc.instanceFn())
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRValidationOnCRDUpdate(t *testing.T) {
|
||||||
|
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
|
||||||
|
// enable alpha feature CustomResourceValidation
|
||||||
|
err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||||
|
|
||||||
|
// set stricter schema
|
||||||
|
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta", "epsilon"}
|
||||||
|
|
||||||
|
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ns := "not-the-default"
|
||||||
|
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
|
||||||
|
|
||||||
|
// CR is rejected
|
||||||
|
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("unexpected non-error: CR should be rejected")
|
||||||
|
}
|
||||||
|
|
||||||
|
gottenCRD, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get("noxus.mygroup.example.com", metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the CRD to a less stricter schema
|
||||||
|
gottenCRD.Spec.Validation.OpenAPIV3Schema.Required = []string{"alpha", "beta"}
|
||||||
|
|
||||||
|
updatedCRD, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(gottenCRD)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CR is now accepted
|
||||||
|
err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
|
||||||
|
_, err = instantiateCustomResource(t, newNoxuValidationInstance(ns, "foo"), noxuResourceClient, updatedCRD)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForbiddenFieldsInSchema(t *testing.T) {
|
||||||
|
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer close(stopCh)
|
||||||
|
|
||||||
|
// enable alpha feature CustomResourceValidation
|
||||||
|
err = utilfeature.DefaultFeatureGate.Set("CustomResourceValidation=true")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to enable feature gate for CustomResourceValidation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||||
|
noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = false
|
||||||
|
|
||||||
|
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("unexpected non-error: additionalProperties cannot be set to false")
|
||||||
|
}
|
||||||
|
|
||||||
|
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
Type: "array",
|
||||||
|
UniqueItems: true,
|
||||||
|
}
|
||||||
|
noxuDefinition.Spec.Validation.OpenAPIV3Schema.AdditionalProperties.Allows = true
|
||||||
|
|
||||||
|
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("unexpected non-error: uniqueItems cannot be set to true")
|
||||||
|
}
|
||||||
|
|
||||||
|
noxuDefinition.Spec.Validation.OpenAPIV3Schema.Properties["zeta"] = apiextensionsv1beta1.JSONSchemaProps{
|
||||||
|
Type: "array",
|
||||||
|
UniqueItems: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func float64Ptr(f float64) *float64 {
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
func int64Ptr(f int64) *int64 {
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user