kubernetes/test/integration/apiserver/crd_validation_expressions_test.go
2023-03-09 23:37:59 +00:00

1104 lines
35 KiB
Go

/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apiserver
import (
"context"
"fmt"
"strings"
"testing"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/json"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/dynamic"
featuregatetesting "k8s.io/component-base/featuregate/testing"
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/framework"
)
// TestCustomResourceValidatorsWithDisabledFeatureGate test that x-kubernetes-validations work as expected when the
// feature gate is disabled.
func TestCustomResourceValidatorsWithDisabledFeatureGate(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CustomResourceValidationExpressions, false)()
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
t.Run("x-kubernetes-validations fields MUST be dropped from CRDs that are created when feature gate is disabled", func(t *testing.T) {
schemaWithFeatureGateOff := crdWithSchema(t, "WithFeatureGateOff", structuralSchemaWithValidators)
crdWithFeatureGateOff, err := fixtures.CreateNewV1CustomResourceDefinition(schemaWithFeatureGateOff, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
s := crdWithFeatureGateOff.Spec.Versions[0].Schema.OpenAPIV3Schema
if len(s.XValidations) != 0 {
t.Errorf("Expected CRD to have no x-kubernetes-validatons rules but got: %v", s.XValidations)
}
})
}
// TestCustomResourceValidators tests x-kubernetes-validations compile and validate as expected when the feature gate
// is enabled.
func TestCustomResourceValidators(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CustomResourceValidationExpressions, true)()
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
t.Run("Structural schema", func(t *testing.T) {
structuralWithValidators := crdWithSchema(t, "Structural", structuralSchemaWithValidators)
crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
gvr := schema.GroupVersionResource{
Group: crd.Spec.Group,
Version: crd.Spec.Versions[0].Name,
Resource: crd.Spec.Names.Plural,
}
crClient := dynamicClient.Resource(gvr)
t.Run("CRD creation MUST allow data that is valid according to x-kubernetes-validations", func(t *testing.T) {
name1 := names.SimpleNameGenerator.GenerateName("cr-1")
_, err = crClient.Create(context.TODO(), &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name1,
},
"spec": map[string]interface{}{
"x": int64(2),
"y": int64(2),
"limit": int64(123),
},
}}, metav1.CreateOptions{})
if err != nil {
t.Errorf("Failed to create custom resource: %v", err)
}
})
t.Run("custom resource create and update MUST NOT allow data that is invalid according to x-kubernetes-validations if the feature gate is enabled", func(t *testing.T) {
name1 := names.SimpleNameGenerator.GenerateName("cr-1")
// a spec create that is invalid MUST fail validation
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name1,
},
"spec": map[string]interface{}{
"x": int64(-1),
"y": int64(0),
},
}}
// a spec create that is invalid MUST fail validation
_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err == nil {
t.Fatal("Expected create of invalid custom resource to fail")
} else {
if !strings.Contains(err.Error(), "failed rule: self.spec.x + self.spec.y") {
t.Fatalf("Expected error to contain %s but got %v", "failed rule: self.spec.x + self.spec.y", err.Error())
}
}
// a spec create that is valid MUST pass validation
cr.Object["spec"] = map[string]interface{}{
"x": int64(2),
"y": int64(2),
"extra": "anything?",
"floatMap": map[string]interface{}{
"key1": 0.2,
"key2": 0.3,
},
"assocList": []interface{}{
map[string]interface{}{
"k": "a",
"v": "1",
},
},
"limit": nil,
}
cr, err := crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Unexpected error creating custom resource: %v", err)
}
// spec updates that are invalid MUST fail validation
cases := []struct {
name string
spec map[string]interface{}
}{
{
name: "spec vs. status default value",
spec: map[string]interface{}{
"x": 3,
"y": -4,
},
},
{
name: "nested string field",
spec: map[string]interface{}{
"extra": "something",
},
},
{
name: "nested array",
spec: map[string]interface{}{
"floatMap": map[string]interface{}{
"key1": 0.1,
"key2": 0.2,
},
},
},
{
name: "nested associative list",
spec: map[string]interface{}{
"assocList": []interface{}{
map[string]interface{}{
"k": "a",
"v": "2",
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cr.Object["spec"] = tc.spec
_, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
if err == nil {
t.Fatal("Expected invalid update of custom resource to fail")
} else {
if !strings.Contains(err.Error(), "failed rule") {
t.Fatalf("Expected error to contain %s but got %v", "failed rule", err.Error())
}
}
})
}
// a status update that is invalid MUST fail validation
cr.Object["status"] = map[string]interface{}{
"z": int64(5),
}
_, err = crClient.UpdateStatus(context.TODO(), cr, metav1.UpdateOptions{})
if err == nil {
t.Fatal("Expected invalid update of custom resource status to fail")
} else {
if !strings.Contains(err.Error(), "failed rule: self.spec.x + self.spec.y") {
t.Fatalf("Expected error to contain %s but got %v", "failed rule: self.spec.x + self.spec.y", err.Error())
}
}
// a status update this is valid MUST pass validation
cr.Object["status"] = map[string]interface{}{
"z": int64(3),
}
_, err = crClient.UpdateStatus(context.TODO(), cr, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("Unexpected error updating custom resource status: %v", err)
}
})
})
t.Run("CRD writes MUST fail for a non-structural schema containing x-kubernetes-validations", func(t *testing.T) {
// The only way for a non-structural schema to exist is for it to already be persisted in etcd as a non-structural CRD.
nonStructuralCRD, err := fixtures.CreateCRDUsingRemovedAPI(server.EtcdClient, server.EtcdStoragePrefix, nonStructuralCrdWithValidations(), apiExtensionClient, dynamicClient)
if err != nil {
t.Fatalf("Unexpected error non-structural CRD by writing directly to etcd: %v", err)
}
// Double check that the schema is non-structural
crd, err := apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), nonStructuralCRD.Name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
nonStructural := false
for _, c := range crd.Status.Conditions {
if c.Type == apiextensionsv1.NonStructuralSchema {
nonStructural = true
}
}
if !nonStructural {
t.Fatal("Expected CRD to be non-structural")
}
//Try to change it
crd.Spec.Versions[0].Schema.OpenAPIV3Schema.XValidations = apiextensionsv1.ValidationRules{
{
Rule: "has(self.foo)",
},
}
_, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
if err == nil {
t.Fatal("Expected error")
}
})
t.Run("CRD creation MUST fail if a x-kubernetes-validations rule accesses a metadata field other than name", func(t *testing.T) {
structuralWithValidators := crdWithSchema(t, "InvalidStructuralMetadata", structuralSchemaWithInvalidMetadataValidators)
_, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
if err == nil {
t.Error("Expected error creating custom resource but got none")
} else if !strings.Contains(err.Error(), "undefined field 'labels'") {
t.Errorf("Expected error to contain %s but got %v", "undefined field 'labels'", err.Error())
}
})
t.Run("CRD creation MUST pass if a x-kubernetes-validations rule accesses metadata.name", func(t *testing.T) {
structuralWithValidators := crdWithSchema(t, "ValidStructuralMetadata", structuralSchemaWithValidMetadataValidators)
_, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
if err != nil {
t.Error("Unexpected error creating custom resource but metadata validation rule")
}
})
t.Run("CR creation MUST fail if a x-kubernetes-validations rule exceeds the runtime cost limit", func(t *testing.T) {
structuralWithValidators := crdWithSchema(t, "RuntimeCostLimit", structuralSchemaWithCostLimit)
crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
if err != nil {
t.Errorf("Unexpected error creating custom resource definition: %v", err)
}
gvr := schema.GroupVersionResource{
Group: crd.Spec.Group,
Version: crd.Spec.Versions[0].Name,
Resource: crd.Spec.Names.Plural,
}
crClient := dynamicClient.Resource(gvr)
name1 := names.SimpleNameGenerator.GenerateName("cr-1")
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name1,
},
"spec": map[string]interface{}{
"list": genLargeArray(725, 20),
},
}}
_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err == nil {
t.Fatal("Expected error creating custom resource")
} else if !strings.Contains(err.Error(), "call cost exceeds limit") {
t.Errorf("Expected error to contain %s but got %v", "call cost exceeds limit", err.Error())
}
})
t.Run("Schema with valid transition rule", func(t *testing.T) {
structuralWithValidators := crdWithSchema(t, "ValidTransitionRule", structuralSchemaWithValidTransitionRule)
crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
gvr := schema.GroupVersionResource{
Group: crd.Spec.Group,
Version: crd.Spec.Versions[0].Name,
Resource: crd.Spec.Names.Plural,
}
crClient := dynamicClient.Resource(gvr)
t.Run("custom resource update MUST pass if a x-kubernetes-validations rule contains a valid transition rule", func(t *testing.T) {
name1 := names.SimpleNameGenerator.GenerateName("cr-1")
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name1,
},
"spec": map[string]interface{}{
"someImmutableThing": "original",
"somethingElse": "original",
},
}}
cr, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Unexpected error creating custom resource: %v", err)
}
cr.Object["spec"].(map[string]interface{})["somethingElse"] = "new value"
_, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
if err != nil {
t.Fatalf("Unexpected error updating custom resource: %v", err)
}
})
t.Run("custom resource update MUST fail if a x-kubernetes-validations rule contains an invalid transition rule", func(t *testing.T) {
name1 := names.SimpleNameGenerator.GenerateName("cr-1")
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name1,
},
"spec": map[string]interface{}{
"someImmutableThing": "original",
"somethingElse": "original",
},
}}
cr, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Unexpected error creating custom resource: %v", err)
}
cr.Object["spec"].(map[string]interface{})["someImmutableThing"] = "new value"
_, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
if err == nil {
t.Fatalf("Expected error updating custom resource: %v", err)
} else if !strings.Contains(err.Error(), "failed rule: self.someImmutableThing == oldSelf.someImmutableThing") {
t.Errorf("Expected error to contain %s but got %v", "failed rule: self.someImmutableThing == oldSelf.someImmutableThing", err.Error())
}
})
})
t.Run("CRD creation MUST fail if a x-kubernetes-validations rule contains invalid transition rule", func(t *testing.T) {
structuralWithValidators := crdWithSchema(t, "InvalidTransitionRule", structuralSchemaWithInvalidTransitionRule)
_, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
if err == nil {
t.Error("Expected error creating custom resource but got none")
} else if !strings.Contains(err.Error(), "oldSelf cannot be used on the uncorrelatable portion of the schema") {
t.Errorf("Expected error to contain %s but got %v", "oldSelf cannot be used on the uncorrelatable portion of the schema", err.Error())
}
})
t.Run("Schema with default map key transition rule", func(t *testing.T) {
structuralWithValidators := crdWithSchema(t, "DefaultMapKeyTransitionRule", structuralSchemaWithDefaultMapKeyTransitionRule)
crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
gvr := schema.GroupVersionResource{
Group: crd.Spec.Group,
Version: crd.Spec.Versions[0].Name,
Resource: crd.Spec.Names.Plural,
}
crClient := dynamicClient.Resource(gvr)
t.Run("custom resource update MUST fail if a x-kubernetes-validations if a transition rule contained in a mapList with default map keys fails validation", func(t *testing.T) {
name1 := names.SimpleNameGenerator.GenerateName("cr-1")
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name1,
},
"spec": map[string]interface{}{
"list": []interface{}{
map[string]interface{}{
"k1": "x",
"v": "value",
},
},
},
}}
cr, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Unexpected error creating custom resource: %v", err)
}
item := cr.Object["spec"].(map[string]interface{})["list"].([]interface{})[0].(map[string]interface{})
item["k2"] = "DEFAULT"
item["v"] = "new value"
_, err = crClient.Update(context.TODO(), cr, metav1.UpdateOptions{})
if err == nil {
t.Fatalf("Expected error updating custom resource: %v", err)
} else if !strings.Contains(err.Error(), "failed rule: self.v == oldSelf.v") {
t.Errorf("Expected error to contain %s but got %v", "failed rule: self.v == oldSelf.v", err.Error())
}
})
})
}
// TestCustomResourceValidatorsWithBlockingErrors tests x-kubernetes-validations is skipped when
// blocking errors occurred.
func TestCustomResourceValidatorsWithBlockingErrors(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.CustomResourceValidationExpressions, true)()
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
t.Run("Structural schema", func(t *testing.T) {
structuralWithValidators := crdWithSchema(t, "Structural", structuralSchemaWithBlockingErr)
crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
gvr := schema.GroupVersionResource{
Group: crd.Spec.Group,
Version: crd.Spec.Versions[0].Name,
Resource: crd.Spec.Names.Plural,
}
crClient := dynamicClient.Resource(gvr)
t.Run("CRD creation MUST allow data that is valid according to x-kubernetes-validations", func(t *testing.T) {
name1 := names.SimpleNameGenerator.GenerateName("cr-1")
_, err = crClient.Create(context.TODO(), &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name1,
},
"spec": map[string]interface{}{
"x": int64(2),
"y": int64(2),
"limit": int64(123),
},
}}, metav1.CreateOptions{})
if err != nil {
t.Errorf("Failed to create custom resource: %v", err)
}
})
t.Run("custom resource create and update MUST NOT allow data if failed validation", func(t *testing.T) {
name1 := names.SimpleNameGenerator.GenerateName("cr-1")
// a spec create that is invalid MUST fail validation
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name1,
},
"spec": map[string]interface{}{
"x": int64(-1),
"y": int64(0),
},
}}
// a spec create that is invalid MUST fail validation
_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err == nil {
t.Fatal("Expected create of invalid custom resource to fail")
} else {
if !strings.Contains(err.Error(), "self.spec.x + self.spec.y must be greater than or equal to 0") {
t.Fatalf("Expected error to contain %s but got %v", "self.spec.x + self.spec.y must be greater than or equal to 0", err.Error())
}
}
})
t.Run("custom resource create and update MUST NOT allow data if there is blocking error of MaxLength", func(t *testing.T) {
name2 := names.SimpleNameGenerator.GenerateName("cr-2")
// a spec create that has maxLengh err MUST fail validation
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name2,
},
"spec": map[string]interface{}{
"x": int64(2),
"y": int64(2),
"extra": strings.Repeat("x", 201),
"floatMap": map[string]interface{}{
"key1": 0.2,
"key2": 0.3,
},
"limit": nil,
},
}}
_, err := crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
}
})
t.Run("custom resource create and update MUST NOT allow data if there is blocking error of MaxItems", func(t *testing.T) {
name2 := names.SimpleNameGenerator.GenerateName("cr-2")
// a spec create that has maxItem err MUST fail validation
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name2,
},
"spec": map[string]interface{}{
"x": int64(2),
"y": int64(2),
"floatMap": map[string]interface{}{
"key1": 0.2,
"key2": 0.3,
},
"assocList": []interface{}{},
"limit": nil,
},
}}
assocList := cr.Object["spec"].(map[string]interface{})["assocList"].([]interface{})
for i := 1; i <= 101; i++ {
assocList = append(assocList, map[string]interface{}{
"k": "a",
"v": fmt.Sprintf("%d", i),
})
}
cr.Object["spec"].(map[string]interface{})["assocList"] = assocList
_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
}
})
t.Run("custom resource create and update MUST NOT allow data if there is blocking error of MaxProperties", func(t *testing.T) {
name2 := names.SimpleNameGenerator.GenerateName("cr-2")
// a spec create that has maxItem err MUST fail validation
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name2,
},
"spec": map[string]interface{}{
"x": int64(2),
"y": int64(2),
"floatMap": map[string]interface{}{},
"assocList": []interface{}{
map[string]interface{}{
"k": "a",
"v": "1",
},
},
"limit": nil,
},
}}
floatMap := cr.Object["spec"].(map[string]interface{})["floatMap"].(map[string]interface{})
for i := 1; i <= 101; i++ {
floatMap[fmt.Sprintf("key%d", i)] = float64(i) / 10
}
_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
}
})
t.Run("custom resource create and update MUST NOT allow data if there is blocking error of missing required field", func(t *testing.T) {
name2 := names.SimpleNameGenerator.GenerateName("cr-2")
// a spec create that has required err MUST fail validation
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name2,
},
"spec": map[string]interface{}{
"x": int64(2),
"y": int64(2),
"floatMap": map[string]interface{}{
"key1": 0.2,
"key2": 0.3,
},
"assocList": []interface{}{
map[string]interface{}{
"k": "1",
},
},
"limit": nil,
},
}}
_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
}
})
t.Run("custom resource create and update MUST NOT allow data if there is blocking error of type", func(t *testing.T) {
name2 := names.SimpleNameGenerator.GenerateName("cr-2")
// a spec create that has required err MUST fail validation
cr := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name2,
},
"spec": map[string]interface{}{
"x": int64(2),
"y": int64(2),
"floatMap": map[string]interface{}{
"key1": 0.2,
"key2": 0.3,
},
"assocList": []interface{}{
map[string]interface{}{
"k": "a",
"v": true,
},
},
"limit": nil,
},
}}
_, err = crClient.Create(context.TODO(), cr, metav1.CreateOptions{})
if err == nil || !strings.Contains(err.Error(), "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation") {
t.Fatalf("expect error to contain \"some validation rules were not checked because the object was invalid; correct the existing errors to complete validation\" but get: %v", err)
}
})
})
}
func nonStructuralCrdWithValidations() *apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "foos.nonstructural.cr.bar.com",
},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "nonstructural.cr.bar.com",
Version: "v1",
Scope: apiextensionsv1beta1.NamespaceScoped,
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "foos",
Kind: "Foo",
},
Validation: &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"foo": {},
},
},
},
},
}
}
func genLargeArray(n, x int64) []int64 {
arr := make([]int64, n)
for i := int64(0); i < n; i++ {
arr[i] = x
}
return arr
}
func crdWithSchema(t *testing.T, kind string, schemaJson []byte) *apiextensionsv1.CustomResourceDefinition {
plural := strings.ToLower(kind) + "s"
var c apiextensionsv1.CustomResourceValidation
err := json.Unmarshal(schemaJson, &c)
if err != nil {
t.Fatal(err)
}
return &apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s.mygroup.example.com", plural)},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "mygroup.example.com",
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
Name: "v1beta1",
Served: true,
Storage: true,
Schema: &c,
Subresources: &apiextensionsv1.CustomResourceSubresources{
Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
},
}},
Names: apiextensionsv1.CustomResourceDefinitionNames{
Plural: plural,
Kind: kind,
},
Scope: apiextensionsv1.ClusterScoped,
},
}
}
var structuralSchemaWithValidators = []byte(`
{
"openAPIV3Schema": {
"description": "CRD with CEL validators",
"type": "object",
"x-kubernetes-validations": [
{
"rule": "self.spec.x + self.spec.y >= (has(self.status) ? self.status.z : 0)"
}
],
"properties": {
"spec": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"default": 0
},
"y": {
"type": "integer",
"default": 0
},
"extra": {
"type": "string",
"x-kubernetes-validations": [
{
"rule": "self.startsWith('anything')"
}
]
},
"floatMap": {
"type": "object",
"additionalProperties": { "type": "number" },
"x-kubernetes-validations": [
{
"rule": "self.all(k, self[k] >= 0.2)"
}
]
},
"assocList": {
"type": "array",
"maxItems": 100,
"items": {
"type": "object",
"properties": {
"k": { "type": "string", "maxLength": 200},
"v": { "type": "string", "maxLength": 200}
},
"required": ["k"]
},
"x-kubernetes-list-type": "map",
"x-kubernetes-list-map-keys": ["k"],
"x-kubernetes-validations": [
{
"rule": "self.exists(e, e.k == 'a' && e.v == '1')"
}
]
},
"limit": {
"nullable": true,
"x-kubernetes-validations": [
{
"rule": "type(self) == int && self == 123"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"status": {
"type": "object",
"properties": {
"z": {
"type": "integer",
"default": 0
}
}
}
}
}
}`)
var structuralSchemaWithBlockingErr = []byte(`
{
"openAPIV3Schema": {
"description": "CRD with CEL validators",
"type": "object",
"x-kubernetes-validations": [
{
"rule": "self.spec.x + self.spec.y >= (has(self.status) ? self.status.z : 0)",
"messageExpression": "\"self.spec.x + self.spec.y must be greater than or equal to 0\""
}
],
"properties": {
"spec": {
"type": "object",
"properties": {
"x": {
"type": "integer",
"default": 0
},
"y": {
"type": "integer",
"default": 0
},
"extra": {
"type": "string",
"maxLength": 200,
"x-kubernetes-validations": [
{
"rule": "self.startsWith('anything')"
}
]
},
"floatMap": {
"type": "object",
"maxProperties": 100,
"additionalProperties": { "type": "number" },
"x-kubernetes-validations": [
{
"rule": "self.all(k, self[k] >= 0.2)"
}
]
},
"assocList": {
"type": "array",
"maxItems": 100,
"items": {
"type": "object",
"properties": {
"k": { "type": "string" },
"v": { "type": "string" }
},
"required": ["k", "v"]
},
"x-kubernetes-list-type": "map",
"x-kubernetes-list-map-keys": ["k"],
"x-kubernetes-validations": [
{
"rule": "self.exists(e, e.k == 'a' && e.v == '1')"
}
]
},
"limit": {
"nullable": true,
"x-kubernetes-validations": [
{
"rule": "type(self) == int && self == 123"
}
],
"x-kubernetes-int-or-string": true
}
}
},
"status": {
"type": "object",
"properties": {
"z": {
"type": "integer",
"default": 0
}
}
}
}
}
}`)
var structuralSchemaWithValidMetadataValidators = []byte(`
{
"openAPIV3Schema": {
"description": "CRD with CEL validators",
"type": "object",
"x-kubernetes-validations": [
{
"rule": "self.metadata.name.size() > 3"
}
],
"properties": {
"metadata": {
"type": "object",
"properties": {
"name": { "type": "string" }
}
},
"spec": {
"type": "object",
"properties": {}
},
"status": {
"type": "object",
"properties": {}
}
}
}
}`)
var structuralSchemaWithInvalidMetadataValidators = []byte(`
{
"openAPIV3Schema": {
"description": "CRD with CEL validators",
"type": "object",
"x-kubernetes-validations": [
{
"rule": "self.metadata.labels.size() > 0"
}
],
"properties": {
"metadata": {
"type": "object",
"properties": {
"name": { "type": "string" }
}
},
"spec": {
"type": "object",
"properties": {}
},
"status": {
"type": "object",
"properties": {}
}
}
}
}`)
var structuralSchemaWithValidTransitionRule = []byte(`
{
"openAPIV3Schema": {
"description": "CRD with CEL validators",
"type": "object",
"properties": {
"spec": {
"type": "object",
"properties": {
"someImmutableThing": { "type": "string" },
"somethingElse": { "type": "string" }
},
"x-kubernetes-validations": [
{
"rule": "self.someImmutableThing == oldSelf.someImmutableThing"
}
]
},
"status": {
"type": "object",
"properties": {}
}
}
}
}`)
var structuralSchemaWithInvalidTransitionRule = []byte(`
{
"openAPIV3Schema": {
"description": "CRD with CEL validators",
"type": "object",
"properties": {
"spec": {
"type": "object",
"properties": {
"list": {
"type": "array",
"items": {
"type": "string",
"x-kubernetes-validations": [
{
"rule": "self == oldSelf"
}
]
}
}
}
},
"status": {
"type": "object",
"properties": {}
}
}
}
}`)
var structuralSchemaWithDefaultMapKeyTransitionRule = []byte(`
{
"openAPIV3Schema": {
"description": "CRD with CEL validators",
"type": "object",
"properties": {
"spec": {
"type": "object",
"properties": {
"list": {
"type": "array",
"x-kubernetes-list-map-keys": [
"k1",
"k2"
],
"x-kubernetes-list-type": "map",
"maxItems": 1000,
"items": {
"type": "object",
"properties": {
"k1": { "type": "string" },
"k2": { "type": "string", "default": "DEFAULT" },
"v": { "type": "string", "maxLength": 200 }
},
"required": ["k1"],
"x-kubernetes-validations": [
{
"rule": "self.v == oldSelf.v"
}
]
}
}
}
},
"status": {
"type": "object",
"properties": {}
}
}
}
}`)
var structuralSchemaWithCostLimit = []byte(`
{
"openAPIV3Schema": {
"description": "CRD with CEL validators",
"type": "object",
"properties": {
"spec": {
"type": "object",
"properties": {
"list": {
"type": "array",
"maxItems": 725,
"items": {
"type": "integer"
},
"x-kubernetes-validations": [
{
"rule": "self.all(x, self.all(y, x == y))"
}
]
}
}
},
"status": {
"type": "object",
"properties": {}
}
}
}
}`)