mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Add validation rule tests for transition rules
This commit is contained in:
parent
fe38a414f8
commit
f71c4d4cf4
@ -787,8 +787,7 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
|||||||
for property, jsonSchema := range schema.Properties {
|
for property, jsonSchema := range schema.Properties {
|
||||||
subSsv := ssv
|
subSsv := ssv
|
||||||
|
|
||||||
// defensively assumes that a future map type is uncorrelatable
|
if !cel.MapIsCorrelatable(schema.XMapType) {
|
||||||
if schema.XMapType != nil && (*schema.XMapType != "granular" && *schema.XMapType != "atomic") {
|
|
||||||
subSsv = subSsv.withForbidOldSelfValidations(fldPath)
|
subSsv = subSsv.withForbidOldSelfValidations(fldPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1098,7 +1098,7 @@ func TestCelCostStability(t *testing.T) {
|
|||||||
t.Fatal("expected non nil validator")
|
t.Fatal("expected non nil validator")
|
||||||
}
|
}
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
errs, remainingBudegt := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, RuntimeCELCostBudget)
|
errs, remainingBudegt := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget)
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -18,16 +18,16 @@ package cel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/maphash"
|
"strings"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mapList provides a "lookup by key" operation for lists (arrays) with x-kubernetes-list-type=map.
|
// mapList provides a "lookup by key" operation for lists (arrays) with x-kubernetes-list-type=map.
|
||||||
type mapList interface {
|
type mapList interface {
|
||||||
// get returns the unique element having identical values, for all
|
// get returns the first element having given key, for all
|
||||||
// x-kubernetes-list-map-keys, to the provided object. If no such unique element exists, or
|
// x-kubernetes-list-map-keys, to the provided object. If the provided object isn't itself a valid mapList element,
|
||||||
// if the provided object isn't itself a valid mapList element, get returns nil.
|
// get returns nil.
|
||||||
get(interface{}) interface{}
|
get(interface{}) interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,15 +40,14 @@ type keyStrategy interface {
|
|||||||
// singleKeyStrategy is a cheaper strategy for associative lists that have exactly one key.
|
// singleKeyStrategy is a cheaper strategy for associative lists that have exactly one key.
|
||||||
type singleKeyStrategy struct {
|
type singleKeyStrategy struct {
|
||||||
key string
|
key string
|
||||||
defawlt interface{} // default is a keyword
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompositeKeyFor directly returns the value of the single key (or its default value, if absent) to
|
// CompositeKeyFor directly returns the value of the single key to
|
||||||
// use as a composite key.
|
// use as a composite key.
|
||||||
func (ks *singleKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
|
func (ks *singleKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
|
||||||
v, ok := obj[ks.key]
|
v, ok := obj[ks.key]
|
||||||
if !ok {
|
if !ok {
|
||||||
v = ks.defawlt // substitute default value
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v.(type) {
|
switch v.(type) {
|
||||||
@ -59,38 +58,37 @@ func (ks *singleKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hashKeyStrategy computes a hash of all key values.
|
// multiKeyStrategy computes a composite key of all key values.
|
||||||
type hashKeyStrategy struct {
|
type multiKeyStrategy struct {
|
||||||
sts *schema.Structural
|
sts *schema.Structural
|
||||||
hasher maphash.Hash
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompositeKeyFor returns a hash computed from the values (or default values, if absent) of all
|
// CompositeKeyFor returns a composite key computed from the values of all
|
||||||
// keys.
|
// keys.
|
||||||
func (ks *hashKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
|
func (ks *multiKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
|
||||||
const keyDelimiter = "\x00" // 0 byte should never appear in the hash input except as delimiter
|
const keyDelimiter = "\x00" // 0 byte should never appear in the composite key except as delimiter
|
||||||
|
|
||||||
ks.hasher.Reset()
|
var delimited strings.Builder
|
||||||
for _, key := range ks.sts.XListMapKeys {
|
for _, key := range ks.sts.XListMapKeys {
|
||||||
v, ok := obj[key]
|
v, ok := obj[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
v = ks.sts.Properties[key].Default.Object
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
switch v.(type) {
|
switch v.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
fmt.Fprintf(&ks.hasher, keyDelimiter+"%t", v)
|
fmt.Fprintf(&delimited, keyDelimiter+"%t", v)
|
||||||
case float64:
|
case float64:
|
||||||
fmt.Fprintf(&ks.hasher, keyDelimiter+"%f", v)
|
fmt.Fprintf(&delimited, keyDelimiter+"%f", v)
|
||||||
case int64:
|
case int64:
|
||||||
fmt.Fprintf(&ks.hasher, keyDelimiter+"%d", v)
|
fmt.Fprintf(&delimited, keyDelimiter+"%d", v)
|
||||||
case string:
|
case string:
|
||||||
fmt.Fprintf(&ks.hasher, keyDelimiter+"%q", v)
|
fmt.Fprintf(&delimited, keyDelimiter+"%q", v)
|
||||||
default:
|
default:
|
||||||
return nil, false // values must be scalars
|
return nil, false // values must be scalars
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ks.hasher.Sum64(), true
|
return delimited.String(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
// emptyMapList is a mapList containing no elements.
|
// emptyMapList is a mapList containing no elements.
|
||||||
@ -103,7 +101,10 @@ func (emptyMapList) get(interface{}) interface{} {
|
|||||||
type mapListImpl struct {
|
type mapListImpl struct {
|
||||||
sts *schema.Structural
|
sts *schema.Structural
|
||||||
ks keyStrategy
|
ks keyStrategy
|
||||||
elements map[interface{}][]interface{} // composite key -> bucket
|
// keyedItems contains all lazily keyed map items
|
||||||
|
keyedItems map[interface{}]interface{}
|
||||||
|
// unkeyedItems contains all map items that have not yet been keyed
|
||||||
|
unkeyedItems []interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *mapListImpl) get(obj interface{}) interface{} {
|
func (a *mapListImpl) get(obj interface{}) interface{} {
|
||||||
@ -116,37 +117,35 @@ func (a *mapListImpl) get(obj interface{}) interface{} {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if match, ok := a.keyedItems[key]; ok {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
// keep keying items until we either find a match or run out of unkeyed items
|
||||||
|
for len(a.unkeyedItems) > 0 {
|
||||||
|
// dequeue an unkeyed item
|
||||||
|
item := a.unkeyedItems[0]
|
||||||
|
a.unkeyedItems = a.unkeyedItems[1:]
|
||||||
|
|
||||||
// Scan bucket to handle key collisions and duplicate key sets:
|
// key the item
|
||||||
var match interface{}
|
mitem, ok := item.(map[string]interface{})
|
||||||
for _, element := range a.elements[key] {
|
|
||||||
all := true
|
|
||||||
for _, key := range a.sts.XListMapKeys {
|
|
||||||
va, ok := element.(map[string]interface{})[key]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
va = a.sts.Properties[key].Default.Object
|
|
||||||
}
|
|
||||||
|
|
||||||
vb, ok := mobj[key]
|
|
||||||
if !ok {
|
|
||||||
vb = a.sts.Properties[key].Default.Object
|
|
||||||
}
|
|
||||||
|
|
||||||
all = all && (va == vb)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !all {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
itemKey, ok := a.ks.CompositeKeyFor(mitem)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := a.keyedItems[itemKey]; !exists {
|
||||||
|
a.keyedItems[itemKey] = mitem
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it matches, short-circuit
|
||||||
|
if itemKey == key {
|
||||||
|
return mitem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if match != nil {
|
|
||||||
// Duplicate key set / more than one element matches. This condition should
|
|
||||||
// have generated a validation error elsewhere.
|
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
match = element
|
|
||||||
}
|
|
||||||
return match // can be nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeKeyStrategy(sts *schema.Structural) keyStrategy {
|
func makeKeyStrategy(sts *schema.Structural) keyStrategy {
|
||||||
@ -154,38 +153,26 @@ func makeKeyStrategy(sts *schema.Structural) keyStrategy {
|
|||||||
key := sts.XListMapKeys[0]
|
key := sts.XListMapKeys[0]
|
||||||
return &singleKeyStrategy{
|
return &singleKeyStrategy{
|
||||||
key: key,
|
key: key,
|
||||||
defawlt: sts.Properties[key].Default.Object,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &hashKeyStrategy{
|
return &multiKeyStrategy{
|
||||||
sts: sts,
|
sts: sts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeMapList returns a queryable interface over the provided x-kubernetes-list-type=map
|
// makeMapList returns a queryable interface over the provided x-kubernetes-list-type=map
|
||||||
// elements. If the provided schema is _not_ an array with x-kubernetes-list-type=map, returns an
|
// keyedItems. If the provided schema is _not_ an array with x-kubernetes-list-type=map, returns an
|
||||||
// empty mapList.
|
// empty mapList.
|
||||||
func makeMapList(sts *schema.Structural, ks keyStrategy, items []interface{}) (rv mapList) {
|
func makeMapList(sts *schema.Structural, items []interface{}) (rv mapList) {
|
||||||
if sts.Type != "array" || sts.XListType == nil || *sts.XListType != "map" || len(sts.XListMapKeys) == 0 || len(items) == 0 {
|
if sts.Type != "array" || sts.XListType == nil || *sts.XListType != "map" || len(sts.XListMapKeys) == 0 || len(items) == 0 {
|
||||||
return emptyMapList{}
|
return emptyMapList{}
|
||||||
}
|
}
|
||||||
|
ks := makeKeyStrategy(sts)
|
||||||
elements := make(map[interface{}][]interface{}, len(items))
|
|
||||||
|
|
||||||
for _, item := range items {
|
|
||||||
mitem, ok := item.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if key, ok := ks.CompositeKeyFor(mitem); ok {
|
|
||||||
elements[key] = append(elements[key], mitem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &mapListImpl{
|
return &mapListImpl{
|
||||||
sts: sts,
|
sts: sts,
|
||||||
ks: ks,
|
ks: ks,
|
||||||
elements: elements,
|
keyedItems: map[interface{}]interface{}{},
|
||||||
|
unkeyedItems: items,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,8 @@ func TestMapList(t *testing.T) {
|
|||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
sts schema.Structural
|
sts schema.Structural
|
||||||
keyStrategy keyStrategy
|
|
||||||
items []interface{}
|
items []interface{}
|
||||||
|
warmUpQueries []interface{}
|
||||||
query interface{}
|
query interface{}
|
||||||
expected interface{}
|
expected interface{}
|
||||||
}{
|
}{
|
||||||
@ -108,102 +108,6 @@ func TestMapList(t *testing.T) {
|
|||||||
"v1": "b",
|
"v1": "b",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "single key with faked composite key collision",
|
|
||||||
sts: schema.Structural{
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Type: "array",
|
|
||||||
},
|
|
||||||
Extensions: schema.Extensions{
|
|
||||||
XListType: &listTypeMap,
|
|
||||||
XListMapKeys: []string{"k"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
keyStrategy: collisionfulKeyStrategy{},
|
|
||||||
items: []interface{}{
|
|
||||||
map[string]interface{}{
|
|
||||||
"k": "a",
|
|
||||||
"v1": "a",
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"k": "b",
|
|
||||||
"v1": "b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query: map[string]interface{}{
|
|
||||||
"k": "b",
|
|
||||||
"v1": "B",
|
|
||||||
},
|
|
||||||
expected: map[string]interface{}{
|
|
||||||
"k": "b",
|
|
||||||
"v1": "b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single key with default",
|
|
||||||
sts: schema.Structural{
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Type: "array",
|
|
||||||
},
|
|
||||||
Extensions: schema.Extensions{
|
|
||||||
XListType: &listTypeMap,
|
|
||||||
XListMapKeys: []string{"k"},
|
|
||||||
},
|
|
||||||
Properties: map[string]schema.Structural{
|
|
||||||
"k": {
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Default: schema.JSON{Object: "a"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
items: []interface{}{
|
|
||||||
map[string]interface{}{
|
|
||||||
"v1": "a",
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"k": "b",
|
|
||||||
"v1": "b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query: map[string]interface{}{
|
|
||||||
"k": "a",
|
|
||||||
"v1": "A",
|
|
||||||
},
|
|
||||||
expected: map[string]interface{}{
|
|
||||||
"v1": "a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single key with defaulted key missing from query",
|
|
||||||
sts: schema.Structural{
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Type: "array",
|
|
||||||
},
|
|
||||||
Extensions: schema.Extensions{
|
|
||||||
XListType: &listTypeMap,
|
|
||||||
XListMapKeys: []string{"k"},
|
|
||||||
},
|
|
||||||
Properties: map[string]schema.Structural{
|
|
||||||
"k": {
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Default: schema.JSON{Object: "a"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
items: []interface{}{
|
|
||||||
map[string]interface{}{
|
|
||||||
"v1": "a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query: map[string]interface{}{
|
|
||||||
"v1": "A",
|
|
||||||
},
|
|
||||||
expected: map[string]interface{}{
|
|
||||||
"v1": "a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "single key ignoring non-map query",
|
name: "single key ignoring non-map query",
|
||||||
sts: schema.Structural{
|
sts: schema.Structural{
|
||||||
@ -278,7 +182,7 @@ func TestMapList(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ignores items with duplicated key",
|
name: "keep first entry when duplicated keys are encountered",
|
||||||
sts: schema.Structural{
|
sts: schema.Structural{
|
||||||
Generic: schema.Generic{
|
Generic: schema.Generic{
|
||||||
Type: "array",
|
Type: "array",
|
||||||
@ -302,50 +206,52 @@ func TestMapList(t *testing.T) {
|
|||||||
"k": "a",
|
"k": "a",
|
||||||
"v1": "A",
|
"v1": "A",
|
||||||
},
|
},
|
||||||
expected: nil,
|
expected: map[string]interface{}{
|
||||||
|
"k": "a",
|
||||||
|
"v1": "a",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple keys with defaults missing from query",
|
name: "keep first entry when duplicated multi-keys are encountered",
|
||||||
sts: schema.Structural{
|
sts: schema.Structural{
|
||||||
Generic: schema.Generic{
|
Generic: schema.Generic{
|
||||||
Type: "array",
|
Type: "array",
|
||||||
},
|
},
|
||||||
Extensions: schema.Extensions{
|
Extensions: schema.Extensions{
|
||||||
XListType: &listTypeMap,
|
XListType: &listTypeMap,
|
||||||
XListMapKeys: []string{"kb", "kf", "ki", "ks"},
|
XListMapKeys: []string{"k1", "k2"},
|
||||||
},
|
|
||||||
Properties: map[string]schema.Structural{
|
|
||||||
"kb": {
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Default: schema.JSON{Object: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"kf": {
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Default: schema.JSON{Object: float64(2.0)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ki": {
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Default: schema.JSON{Object: int64(42)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ks": {
|
|
||||||
Generic: schema.Generic{
|
|
||||||
Default: schema.JSON{Object: "hello"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
items: []interface{}{
|
items: []interface{}{
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
|
"k1": "a",
|
||||||
|
"k2": "b",
|
||||||
"v1": "a",
|
"v1": "a",
|
||||||
},
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"k1": "a",
|
||||||
|
"k2": "b",
|
||||||
|
"v1": "b",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"k1": "x",
|
||||||
|
"k2": "y",
|
||||||
|
"v1": "z",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
warmUpQueries: []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"k1": "x",
|
||||||
|
"k2": "y",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
query: map[string]interface{}{
|
query: map[string]interface{}{
|
||||||
"v1": "A",
|
"k1": "a",
|
||||||
|
"k2": "b",
|
||||||
},
|
},
|
||||||
expected: map[string]interface{}{
|
expected: map[string]interface{}{
|
||||||
|
"k1": "a",
|
||||||
|
"k2": "b",
|
||||||
"v1": "a",
|
"v1": "a",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -415,20 +321,14 @@ func TestMapList(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
ks := tc.keyStrategy
|
mapList := makeMapList(&tc.sts, tc.items)
|
||||||
if ks == nil {
|
for _, warmUp := range tc.warmUpQueries {
|
||||||
ks = makeKeyStrategy(&tc.sts)
|
mapList.get(warmUp)
|
||||||
}
|
}
|
||||||
actual := makeMapList(&tc.sts, ks, tc.items).get(tc.query)
|
actual := mapList.get(tc.query)
|
||||||
if !reflect.DeepEqual(tc.expected, actual) {
|
if !reflect.DeepEqual(tc.expected, actual) {
|
||||||
t.Errorf("got: %v, expected %v", actual, tc.expected)
|
t.Errorf("got: %v, expected %v", actual, tc.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type collisionfulKeyStrategy struct{}
|
|
||||||
|
|
||||||
func (collisionfulKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
|
|
||||||
return 7, true
|
|
||||||
}
|
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/cel-go/common/types"
|
"github.com/google/cel-go/common/types"
|
||||||
@ -126,6 +127,17 @@ func (s *Validator) Validate(ctx context.Context, fldPath *field.Path, sts *sche
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) {
|
func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj interface{}, costBudget int64) (errs field.ErrorList, remainingBudget int64) {
|
||||||
|
// guard against oldObj being a non-nil interface with a nil value
|
||||||
|
if oldObj != nil {
|
||||||
|
v := reflect.ValueOf(oldObj)
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice:
|
||||||
|
if v.IsNil() {
|
||||||
|
oldObj = nil // +k8s:verify-mutation:reason=clone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
remainingBudget = costBudget
|
remainingBudget = costBudget
|
||||||
if obj == nil {
|
if obj == nil {
|
||||||
// We only validate non-null values. Rules that need to check for the state of a nullable value or the presence of an optional
|
// We only validate non-null values. Rules that need to check for the state of a nullable value or the presence of an optional
|
||||||
@ -147,10 +159,7 @@ func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path
|
|||||||
if s.isResourceRoot {
|
if s.isResourceRoot {
|
||||||
sts = model.WithTypeAndObjectMeta(sts)
|
sts = model.WithTypeAndObjectMeta(sts)
|
||||||
}
|
}
|
||||||
var activation interpreter.Activation = NewValidationActivation(ScopedVarName, obj, sts)
|
var activation interpreter.Activation = NewValidationActivation(obj, oldObj, sts)
|
||||||
if oldObj != nil {
|
|
||||||
activation = interpreter.NewHierarchicalActivation(activation, NewValidationActivation(OldScopedVarName, oldObj, sts))
|
|
||||||
}
|
|
||||||
for i, compiled := range s.compiledRules {
|
for i, compiled := range s.compiledRules {
|
||||||
rule := sts.XValidations[i]
|
rule := sts.XValidations[i]
|
||||||
if compiled.Error != nil {
|
if compiled.Error != nil {
|
||||||
@ -218,23 +227,30 @@ func ruleErrorString(rule apiextensions.ValidationRule) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type validationActivation struct {
|
type validationActivation struct {
|
||||||
name string
|
self, oldSelf ref.Val
|
||||||
val ref.Val
|
hasOldSelf bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewValidationActivation(name string, obj interface{}, structural *schema.Structural) *validationActivation {
|
func NewValidationActivation(obj, oldObj interface{}, structural *schema.Structural) *validationActivation {
|
||||||
va := &validationActivation{
|
va := &validationActivation{
|
||||||
name: name,
|
self: UnstructuredToVal(obj, structural),
|
||||||
val: UnstructuredToVal(obj, structural),
|
}
|
||||||
|
if oldObj != nil {
|
||||||
|
va.oldSelf = UnstructuredToVal(oldObj, structural) // +k8s:verify-mutation:reason=clone
|
||||||
|
va.hasOldSelf = true // +k8s:verify-mutation:reason=clone
|
||||||
}
|
}
|
||||||
return va
|
return va
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *validationActivation) ResolveName(name string) (interface{}, bool) {
|
func (a *validationActivation) ResolveName(name string) (interface{}, bool) {
|
||||||
if name == a.name {
|
switch name {
|
||||||
return a.val, true
|
case ScopedVarName:
|
||||||
}
|
return a.self, true
|
||||||
|
case OldScopedVarName:
|
||||||
|
return a.oldSelf, a.hasOldSelf
|
||||||
|
default:
|
||||||
return nil, false
|
return nil, false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *validationActivation) Parent() interpreter.Activation {
|
func (a *validationActivation) Parent() interpreter.Activation {
|
||||||
@ -250,14 +266,13 @@ func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *s
|
|||||||
return nil, remainingBudget
|
return nil, remainingBudget
|
||||||
}
|
}
|
||||||
|
|
||||||
// if a third map type is introduced, assume it's not correlatable. granular is the default if unspecified.
|
correlatable := MapIsCorrelatable(sts.XMapType)
|
||||||
correlatable := sts.XMapType == nil || *sts.XMapType == "granular" || *sts.XMapType == "atomic"
|
|
||||||
|
|
||||||
if s.AdditionalProperties != nil && sts.AdditionalProperties != nil && sts.AdditionalProperties.Structural != nil {
|
if s.AdditionalProperties != nil && sts.AdditionalProperties != nil && sts.AdditionalProperties.Structural != nil {
|
||||||
for k, v := range obj {
|
for k, v := range obj {
|
||||||
var oldV interface{}
|
var oldV interface{}
|
||||||
if correlatable {
|
if correlatable {
|
||||||
oldV = oldObj[k]
|
oldV = oldObj[k] // +k8s:verify-mutation:reason=clone
|
||||||
}
|
}
|
||||||
|
|
||||||
var err field.ErrorList
|
var err field.ErrorList
|
||||||
@ -275,7 +290,7 @@ func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *s
|
|||||||
if ok && stsOk {
|
if ok && stsOk {
|
||||||
var oldV interface{}
|
var oldV interface{}
|
||||||
if correlatable {
|
if correlatable {
|
||||||
oldV = oldObj[k]
|
oldV = oldObj[k] // +k8s:verify-mutation:reason=clone
|
||||||
}
|
}
|
||||||
|
|
||||||
var err field.ErrorList
|
var err field.ErrorList
|
||||||
@ -297,11 +312,10 @@ func (s *Validator) validateArray(ctx context.Context, fldPath *field.Path, sts
|
|||||||
return errs, remainingBudget
|
return errs, remainingBudget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.Items != nil && sts.Items != nil {
|
||||||
// only map-type lists support self-oldSelf correlation for cel rules. if this isn't a
|
// only map-type lists support self-oldSelf correlation for cel rules. if this isn't a
|
||||||
// map-type list, then makeMapList returns an implementation that always returns nil
|
// map-type list, then makeMapList returns an implementation that always returns nil
|
||||||
correlatableOldItems := makeMapList(sts, makeKeyStrategy(sts), oldObj)
|
correlatableOldItems := makeMapList(sts, oldObj)
|
||||||
|
|
||||||
if s.Items != nil && sts.Items != nil {
|
|
||||||
for i := range obj {
|
for i := range obj {
|
||||||
var err field.ErrorList
|
var err field.ErrorList
|
||||||
err, remainingBudget = s.Items.Validate(ctx, fldPath.Index(i), sts.Items, obj[i], correlatableOldItems.get(obj[i]), remainingBudget)
|
err, remainingBudget = s.Items.Validate(ctx, fldPath.Index(i), sts.Items, obj[i], correlatableOldItems.get(obj[i]), remainingBudget)
|
||||||
@ -314,3 +328,10 @@ func (s *Validator) validateArray(ctx context.Context, fldPath *field.Path, sts
|
|||||||
|
|
||||||
return errs, remainingBudget
|
return errs, remainingBudget
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapIsCorrelatable returns true if the mapType can be used to correlate the data elements of a map after an update
|
||||||
|
// with the data elements of the map from before the updated.
|
||||||
|
func MapIsCorrelatable(mapType *string) bool {
|
||||||
|
// if a third map type is introduced, assume it's not correlatable. granular is the default if unspecified.
|
||||||
|
return mapType == nil || *mapType == "granular" || *mapType == "atomic"
|
||||||
|
}
|
||||||
|
@ -34,11 +34,13 @@ func TestValidationExpressions(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
schema *schema.Structural
|
schema *schema.Structural
|
||||||
obj map[string]interface{}
|
obj interface{}
|
||||||
oldObj map[string]interface{}
|
oldObj interface{}
|
||||||
valid []string
|
valid []string
|
||||||
errors map[string]string // rule -> string that error message must contain
|
errors map[string]string // rule -> string that error message must contain
|
||||||
costBudget int64
|
costBudget int64
|
||||||
|
isRoot bool
|
||||||
|
expectSkipped bool
|
||||||
}{
|
}{
|
||||||
// tests where val1 and val2 are equal but val3 is different
|
// tests where val1 and val2 are equal but val3 is different
|
||||||
// equality, comparisons and type specific functions
|
// equality, comparisons and type specific functions
|
||||||
@ -627,6 +629,7 @@ func TestValidationExpressions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{name: "typemeta and objectmeta access not specified",
|
{name: "typemeta and objectmeta access not specified",
|
||||||
|
isRoot: true,
|
||||||
obj: map[string]interface{}{
|
obj: map[string]interface{}{
|
||||||
"apiVersion": "v1",
|
"apiVersion": "v1",
|
||||||
"kind": "Pod",
|
"kind": "Pod",
|
||||||
@ -1708,6 +1711,44 @@ func TestValidationExpressions(t *testing.T) {
|
|||||||
"oldSelf.v == 'old' && self.v == 'new'",
|
"oldSelf.v == 'old' && self.v == 'new'",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{name: "skipped transition rule for nil old primitive",
|
||||||
|
expectSkipped: true,
|
||||||
|
obj: "exists",
|
||||||
|
oldObj: nil,
|
||||||
|
schema: &stringType,
|
||||||
|
valid: []string{
|
||||||
|
"oldSelf == self",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{name: "skipped transition rule for nil old array",
|
||||||
|
expectSkipped: true,
|
||||||
|
obj: []interface{}{},
|
||||||
|
oldObj: nil,
|
||||||
|
schema: listTypePtr(&stringType),
|
||||||
|
valid: []string{
|
||||||
|
"oldSelf == self",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{name: "skipped transition rule for nil old object",
|
||||||
|
expectSkipped: true,
|
||||||
|
obj: map[string]interface{}{"f": "exists"},
|
||||||
|
oldObj: nil,
|
||||||
|
schema: objectTypePtr(map[string]schema.Structural{
|
||||||
|
"f": stringType,
|
||||||
|
}),
|
||||||
|
valid: []string{
|
||||||
|
"oldSelf.f == self.f",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{name: "skipped transition rule for old with non-nil interface but nil value",
|
||||||
|
expectSkipped: true,
|
||||||
|
obj: []interface{}{},
|
||||||
|
oldObj: nilInterfaceOfStringSlice(),
|
||||||
|
schema: listTypePtr(&stringType),
|
||||||
|
valid: []string{
|
||||||
|
"oldSelf == self",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range tests {
|
for i := range tests {
|
||||||
@ -1722,14 +1763,21 @@ func TestValidationExpressions(t *testing.T) {
|
|||||||
t.Run(validRule, func(t *testing.T) {
|
t.Run(validRule, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
s := withRule(*tt.schema, validRule)
|
s := withRule(*tt.schema, validRule)
|
||||||
celValidator := NewValidator(&s, PerCallLimit)
|
celValidator := validator(&s, tt.isRoot, PerCallLimit)
|
||||||
if celValidator == nil {
|
if celValidator == nil {
|
||||||
t.Fatal("expected non nil validator")
|
t.Fatal("expected non nil validator")
|
||||||
}
|
}
|
||||||
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
|
errs, remainingBudget := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, tt.costBudget)
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
if tt.expectSkipped {
|
||||||
|
// Skipped validations should have no cost. The only possible false positive here would be the CEL expression 'true'.
|
||||||
|
if remainingBudget != tt.costBudget {
|
||||||
|
t.Errorf("expected no cost expended for skipped validation, but got %d remaining from %d budget", remainingBudget, tt.costBudget)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// test with cost budget exceeded
|
// test with cost budget exceeded
|
||||||
errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, 0)
|
errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, tt.oldObj, 0)
|
||||||
@ -2107,3 +2155,8 @@ func withNullablePtr(nullable bool, s schema.Structural) *schema.Structural {
|
|||||||
s.Generic.Nullable = nullable
|
s.Generic.Nullable = nullable
|
||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nilInterfaceOfStringSlice() []interface{} {
|
||||||
|
var slice []interface{} = nil
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
@ -90,7 +90,7 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur
|
|||||||
} else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 {
|
} else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 {
|
||||||
allErrs = append(allErrs, errs...)
|
allErrs = append(allErrs, errs...)
|
||||||
} else if celValidator := cel.NewValidator(s, cel.PerCallLimit); celValidator != nil {
|
} else if celValidator := cel.NewValidator(s, cel.PerCallLimit); celValidator != nil {
|
||||||
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, nil, remainingCost)
|
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost)
|
||||||
remainingCost = rmCost
|
remainingCost = rmCost
|
||||||
allErrs = append(allErrs, celErrs...)
|
allErrs = append(allErrs, celErrs...)
|
||||||
if remainingCost < 0 {
|
if remainingCost < 0 {
|
||||||
@ -115,7 +115,7 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur
|
|||||||
} else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 {
|
} else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 {
|
||||||
allErrs = append(allErrs, errs...)
|
allErrs = append(allErrs, errs...)
|
||||||
} else if celValidator := cel.NewValidator(s, cel.PerCallLimit); celValidator != nil {
|
} else if celValidator := cel.NewValidator(s, cel.PerCallLimit); celValidator != nil {
|
||||||
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, nil, remainingCost)
|
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost)
|
||||||
remainingCost = rmCost
|
remainingCost = rmCost
|
||||||
allErrs = append(allErrs, celErrs...)
|
allErrs = append(allErrs, celErrs...)
|
||||||
if remainingCost < 0 {
|
if remainingCost < 0 {
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -27,16 +28,19 @@ import (
|
|||||||
|
|
||||||
kjson "sigs.k8s.io/json"
|
kjson "sigs.k8s.io/json"
|
||||||
|
|
||||||
|
kubeopenapispec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||||
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
|
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
|
||||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
||||||
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
|
||||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
"k8s.io/apimachinery/pkg/util/json"
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
kubeopenapispec "k8s.io/kube-openapi/pkg/validation/spec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestRoundTrip checks the conversion to go-openapi types.
|
// TestRoundTrip checks the conversion to go-openapi types.
|
||||||
@ -145,6 +149,7 @@ func stripIntOrStringType(x interface{}) interface{} {
|
|||||||
|
|
||||||
type failingObject struct {
|
type failingObject struct {
|
||||||
object interface{}
|
object interface{}
|
||||||
|
oldObject interface{}
|
||||||
expectErrs []string
|
expectErrs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +158,7 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
schema apiextensions.JSONSchemaProps
|
schema apiextensions.JSONSchemaProps
|
||||||
objects []interface{}
|
objects []interface{}
|
||||||
|
oldObjects []interface{}
|
||||||
failingObjects []failingObject
|
failingObjects []failingObject
|
||||||
}{
|
}{
|
||||||
{name: "!nullable",
|
{name: "!nullable",
|
||||||
@ -416,6 +422,119 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{name: "immutability transition rule",
|
||||||
|
schema: apiextensions.JSONSchemaProps{
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"field": {
|
||||||
|
Type: "string",
|
||||||
|
XValidations: []apiextensions.ValidationRule{
|
||||||
|
{
|
||||||
|
Rule: "self == oldSelf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objects: []interface{}{
|
||||||
|
map[string]interface{}{"field": "x"},
|
||||||
|
},
|
||||||
|
oldObjects: []interface{}{
|
||||||
|
map[string]interface{}{"field": "x"},
|
||||||
|
},
|
||||||
|
failingObjects: []failingObject{
|
||||||
|
{
|
||||||
|
object: map[string]interface{}{"field": "y"},
|
||||||
|
oldObject: map[string]interface{}{"field": "x"},
|
||||||
|
expectErrs: []string{
|
||||||
|
`field: Invalid value: "string": failed rule: self == oldSelf`,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{name: "correlatable transition rule",
|
||||||
|
// Ensures a transition rule under a "listMap" is supported.
|
||||||
|
schema: apiextensions.JSONSchemaProps{
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"field": {
|
||||||
|
Type: "array",
|
||||||
|
XListType: &listMapType,
|
||||||
|
XListMapKeys: []string{"k1", "k2"},
|
||||||
|
Items: &apiextensions.JSONSchemaPropsOrArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"k1": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
"k2": {
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
"v1": {
|
||||||
|
Type: "number",
|
||||||
|
XValidations: []apiextensions.ValidationRule{
|
||||||
|
{
|
||||||
|
Rule: "self >= oldSelf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objects: []interface{}{
|
||||||
|
map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 1.2}}},
|
||||||
|
},
|
||||||
|
oldObjects: []interface{}{
|
||||||
|
map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 1.0}}},
|
||||||
|
},
|
||||||
|
failingObjects: []failingObject{
|
||||||
|
{
|
||||||
|
object: map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 0.9}}},
|
||||||
|
oldObject: map[string]interface{}{"field": []interface{}{map[string]interface{}{"k1": "a", "k2": "b", "v1": 1.0}}},
|
||||||
|
expectErrs: []string{
|
||||||
|
`field[0].v1: Invalid value: "number": failed rule: self >= oldSelf`,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{name: "validation rule under non-correlatable field",
|
||||||
|
// The array makes the rule on the nested string non-correlatable
|
||||||
|
// for transition rule purposes. This test ensures that a rule that
|
||||||
|
// does NOT use oldSelf (is not a transition rule), still behaves
|
||||||
|
// as expected under a non-correlatable field.
|
||||||
|
schema: apiextensions.JSONSchemaProps{
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"field": {
|
||||||
|
Type: "array",
|
||||||
|
Items: &apiextensions.JSONSchemaPropsOrArray{
|
||||||
|
Schema: &apiextensions.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||||
|
"x": {
|
||||||
|
Type: "string",
|
||||||
|
XValidations: []apiextensions.ValidationRule{
|
||||||
|
{
|
||||||
|
Rule: "self == 'x'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objects: []interface{}{
|
||||||
|
map[string]interface{}{"field": []interface{}{map[string]interface{}{"x": "x"}}},
|
||||||
|
},
|
||||||
|
failingObjects: []failingObject{
|
||||||
|
{
|
||||||
|
object: map[string]interface{}{"field": []interface{}{map[string]interface{}{"x": "y"}}},
|
||||||
|
expectErrs: []string{
|
||||||
|
`field[0].x: Invalid value: "string": failed rule: self == 'x'`,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -423,13 +542,29 @@ func TestValidateCustomResource(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, obj := range tt.objects {
|
structural, err := structuralschema.NewStructural(&tt.schema)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
celValidator := cel.NewValidator(structural, cel.PerCallLimit)
|
||||||
|
for i, obj := range tt.objects {
|
||||||
|
var oldObject interface{}
|
||||||
|
if len(tt.oldObjects) == len(tt.objects) {
|
||||||
|
oldObject = tt.oldObjects[i]
|
||||||
|
}
|
||||||
if errs := ValidateCustomResource(nil, obj, validator); len(errs) > 0 {
|
if errs := ValidateCustomResource(nil, obj, validator); len(errs) > 0 {
|
||||||
t.Errorf("unexpected validation error for %v: %v", obj, errs)
|
t.Errorf("unexpected validation error for %v: %v", obj, errs)
|
||||||
}
|
}
|
||||||
|
errs, _ := celValidator.Validate(context.TODO(), nil, structural, obj, oldObject, cel.RuntimeCELCostBudget)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Errorf(errs.ToAggregate().Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for i, failingObject := range tt.failingObjects {
|
for i, failingObject := range tt.failingObjects {
|
||||||
if errs := ValidateCustomResource(nil, failingObject.object, validator); len(errs) == 0 {
|
errs := ValidateCustomResource(nil, failingObject.object, validator)
|
||||||
|
celErrs, _ := celValidator.Validate(context.TODO(), nil, structural, failingObject.object, failingObject.oldObject, cel.RuntimeCELCostBudget)
|
||||||
|
errs = append(errs, celErrs...)
|
||||||
|
if len(errs) == 0 {
|
||||||
t.Errorf("missing error for %v", failingObject.object)
|
t.Errorf("missing error for %v", failingObject.object)
|
||||||
} else {
|
} else {
|
||||||
sawErrors := sets.NewString()
|
sawErrors := sets.NewString()
|
||||||
@ -505,3 +640,5 @@ func TestItemsProperty(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var listMapType = "map"
|
||||||
|
@ -19,11 +19,12 @@ package customresource
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
|
|
||||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type statusStrategy struct {
|
type statusStrategy struct {
|
||||||
@ -91,7 +92,7 @@ func (a statusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Obj
|
|||||||
}
|
}
|
||||||
uOld, ok := old.(*unstructured.Unstructured)
|
uOld, ok := old.(*unstructured.Unstructured)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errs
|
uOld = nil // as a safety precaution, continue with validation if uOld self cannot be cast
|
||||||
}
|
}
|
||||||
|
|
||||||
v := obj.GetObjectKind().GroupVersionKind().Version
|
v := obj.GetObjectKind().GroupVersionKind().Version
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
|
||||||
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
"k8s.io/kubernetes/test/integration/framework"
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
)
|
)
|
||||||
@ -301,7 +302,7 @@ func TestCustomResourceValidators(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("Schema with valid transition rule", func(t *testing.T) {
|
t.Run("Schema with valid transition rule", func(t *testing.T) {
|
||||||
structuralWithValidators := crdWithSchema(t, "Structural", structuralSchemaWithValidTransitionRule)
|
structuralWithValidators := crdWithSchema(t, "ValidTransitionRule", structuralSchemaWithValidTransitionRule)
|
||||||
crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
|
crd, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -364,7 +365,7 @@ func TestCustomResourceValidators(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("CRD creation MUST fail if a x-kubernetes-validations rule contains invalid transition rule", func(t *testing.T) {
|
t.Run("CRD creation MUST fail if a x-kubernetes-validations rule contains invalid transition rule", func(t *testing.T) {
|
||||||
structuralWithValidators := crdWithSchema(t, "InvalidStructuralMetadata", structuralSchemaWithInvalidTransitionRule)
|
structuralWithValidators := crdWithSchema(t, "InvalidTransitionRule", structuralSchemaWithInvalidTransitionRule)
|
||||||
_, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
|
_, err := fixtures.CreateNewV1CustomResourceDefinition(structuralWithValidators, apiExtensionClient, dynamicClient)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Expected error creating custom resource but got none")
|
t.Error("Expected error creating custom resource but got none")
|
||||||
@ -372,6 +373,51 @@ func TestCustomResourceValidators(t *testing.T) {
|
|||||||
t.Errorf("Expected error to contain %s but got %v", "oldSelf cannot be used on the uncorrelatable portion of the schema", err.Error())
|
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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func nonStructuralCrdWithValidations() *apiextensionsv1beta1.CustomResourceDefinition {
|
func nonStructuralCrdWithValidations() *apiextensionsv1beta1.CustomResourceDefinition {
|
||||||
@ -623,3 +669,44 @@ var structuralSchemaWithInvalidTransitionRule = []byte(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
|
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",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"k1": { "type": "string" },
|
||||||
|
"k2": { "type": "string", "default": "DEFAULT" },
|
||||||
|
"v": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["k1"],
|
||||||
|
"x-kubernetes-validations": [
|
||||||
|
{
|
||||||
|
"rule": "self.v == oldSelf.v"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
Loading…
Reference in New Issue
Block a user