Merge pull request #113312 from jiahuif-forks/feature/cel/builtins

OpenAPI-based CEL type library
This commit is contained in:
Kubernetes Prow Robot 2023-02-18 00:31:36 -08:00 committed by GitHub
commit 70b2e4aa3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 3931 additions and 1035 deletions

2
go.mod
View File

@ -39,6 +39,7 @@ require (
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.2 github.com/golang/protobuf v1.5.2
github.com/google/cadvisor v0.47.1 github.com/google/cadvisor v0.47.1
github.com/google/cel-go v0.12.6
github.com/google/gnostic v0.5.7-v3refs github.com/google/gnostic v0.5.7-v3refs
github.com/google/go-cmp v0.5.9 github.com/google/go-cmp v0.5.9
github.com/google/gofuzz v1.1.0 github.com/google/gofuzz v1.1.0
@ -172,7 +173,6 @@ require (
github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/gofrs/uuid v4.0.0+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
github.com/google/btree v1.0.1 // indirect github.com/google/btree v1.0.1 // indirect
github.com/google/cel-go v0.12.6 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect github.com/googleapis/gax-go/v2 v2.1.1 // indirect

View File

@ -27,7 +27,6 @@ import (
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
celmodel "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
apiservercel "k8s.io/apiserver/pkg/cel" apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library" "k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/metrics" "k8s.io/apiserver/pkg/cel/metrics"
@ -53,6 +52,10 @@ const (
// checkFrequency configures the number of iterations within a comprehension to evaluate // checkFrequency configures the number of iterations within a comprehension to evaluate
// before checking whether the function evaluation has been interrupted // before checking whether the function evaluation has been interrupted
checkFrequency = 100 checkFrequency = 100
// maxRequestSizeBytes is the maximum size of a request to the API server
// TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable
maxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
) )
// CompilationResult represents the cel compilation result for one rule // CompilationResult represents the cel compilation result for one rule
@ -149,7 +152,7 @@ func Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit
estimator := newCostEstimator(root) estimator := newCostEstimator(root)
// compResults is the return value which saves a list of compilation results in the same order as x-kubernetes-validations rules. // compResults is the return value which saves a list of compilation results in the same order as x-kubernetes-validations rules.
compResults := make([]CompilationResult, len(celRules)) compResults := make([]CompilationResult, len(celRules))
maxCardinality := celmodel.MaxCardinality(root.MinSerializedSize) maxCardinality := maxCardinality(root.MinSerializedSize)
for i, rule := range celRules { for i, rule := range celRules {
compResults[i] = compileRule(rule, env, perCallLimit, estimator, maxCardinality) compResults[i] = compileRule(rule, env, perCallLimit, estimator, maxCardinality)
} }
@ -262,3 +265,14 @@ func (c *sizeEstimator) EstimateSize(element checker.AstNode) *checker.SizeEstim
func (c *sizeEstimator) EstimateCallCost(function, overloadID string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { func (c *sizeEstimator) EstimateCallCost(function, overloadID string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate {
return nil return nil
} }
// maxCardinality returns the maximum number of times data conforming to the minimum size given could possibly exist in
// an object serialized to JSON. For cases where a schema is contained under map or array schemas of unbounded
// size, this can be used as an estimate as the worst case number of times data matching the schema could be repeated.
// Note that this only assumes a single comma between data elements, so if the schema is contained under only maps,
// this estimates a higher cardinality that would be possible. DeclType.MinSerializedSize is meant to be passed to
// this function.
func maxCardinality(minSize int64) uint64 {
sz := minSize + 1 // assume at least one comma between elements
return uint64(maxRequestSizeBytes / sz)
}

View File

@ -17,162 +17,14 @@ limitations under the License.
package cel package cel
import ( import (
"fmt"
"strings"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
"k8s.io/apiserver/pkg/cel/common"
) )
// mapList provides a "lookup by key" operation for lists (arrays) with x-kubernetes-list-type=map.
type mapList interface {
// get returns the first element having given key, for all
// x-kubernetes-list-map-keys, to the provided object. If the provided object isn't itself a valid mapList element,
// get returns nil.
get(interface{}) interface{}
}
type keyStrategy interface {
// CompositeKeyFor returns a composite key for the provided object, if possible, and a
// boolean that indicates whether or not a key could be generated for the provided object.
CompositeKeyFor(map[string]interface{}) (interface{}, bool)
}
// singleKeyStrategy is a cheaper strategy for associative lists that have exactly one key.
type singleKeyStrategy struct {
key string
}
// CompositeKeyFor directly returns the value of the single key to
// use as a composite key.
func (ks *singleKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
v, ok := obj[ks.key]
if !ok {
return nil, false
}
switch v.(type) {
case bool, float64, int64, string:
return v, true
default:
return nil, false // non-scalar
}
}
// multiKeyStrategy computes a composite key of all key values.
type multiKeyStrategy struct {
sts *schema.Structural
}
// CompositeKeyFor returns a composite key computed from the values of all
// keys.
func (ks *multiKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
const keyDelimiter = "\x00" // 0 byte should never appear in the composite key except as delimiter
var delimited strings.Builder
for _, key := range ks.sts.XListMapKeys {
v, ok := obj[key]
if !ok {
return nil, false
}
switch v.(type) {
case bool:
fmt.Fprintf(&delimited, keyDelimiter+"%t", v)
case float64:
fmt.Fprintf(&delimited, keyDelimiter+"%f", v)
case int64:
fmt.Fprintf(&delimited, keyDelimiter+"%d", v)
case string:
fmt.Fprintf(&delimited, keyDelimiter+"%q", v)
default:
return nil, false // values must be scalars
}
}
return delimited.String(), true
}
// emptyMapList is a mapList containing no elements.
type emptyMapList struct{}
func (emptyMapList) get(interface{}) interface{} {
return nil
}
type mapListImpl struct {
sts *schema.Structural
ks keyStrategy
// 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{} {
mobj, ok := obj.(map[string]interface{})
if !ok {
return nil
}
key, ok := a.ks.CompositeKeyFor(mobj)
if !ok {
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:]
// key the item
mitem, ok := item.(map[string]interface{})
if !ok {
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
}
}
return nil
}
func makeKeyStrategy(sts *schema.Structural) keyStrategy {
if len(sts.XListMapKeys) == 1 {
key := sts.XListMapKeys[0]
return &singleKeyStrategy{
key: key,
}
}
return &multiKeyStrategy{
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
// keyedItems. 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, items []interface{}) (rv mapList) { func makeMapList(sts *schema.Structural, items []interface{}) (rv common.MapList) {
if sts.Type != "array" || sts.XListType == nil || *sts.XListType != "map" || len(sts.XListMapKeys) == 0 || len(items) == 0 { return common.MakeMapList(&model.Structural{Structural: sts}, items)
return emptyMapList{}
}
ks := makeKeyStrategy(sts)
return &mapListImpl{
sts: sts,
ks: ks,
keyedItems: map[interface{}]interface{}{},
unkeyedItems: items,
}
} }

View File

@ -323,9 +323,9 @@ func TestMapList(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
mapList := makeMapList(&tc.sts, tc.items) mapList := makeMapList(&tc.sts, tc.items)
for _, warmUp := range tc.warmUpQueries { for _, warmUp := range tc.warmUpQueries {
mapList.get(warmUp) mapList.Get(warmUp)
} }
actual := mapList.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)
} }

View File

@ -0,0 +1,152 @@
/*
Copyright 2023 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 model
import (
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiserver/pkg/cel/common"
)
var _ common.Schema = (*Structural)(nil)
var _ common.SchemaOrBool = (*StructuralOrBool)(nil)
type Structural struct {
Structural *schema.Structural
}
type StructuralOrBool struct {
StructuralOrBool *schema.StructuralOrBool
}
func (sb *StructuralOrBool) Schema() common.Schema {
if sb.StructuralOrBool.Structural == nil {
return nil
}
return &Structural{Structural: sb.StructuralOrBool.Structural}
}
func (sb *StructuralOrBool) Allows() bool {
return sb.StructuralOrBool.Bool
}
func (s *Structural) Type() string {
return s.Structural.Type
}
func (s *Structural) Format() string {
if s.Structural.ValueValidation == nil {
return ""
}
return s.Structural.ValueValidation.Format
}
func (s *Structural) Items() common.Schema {
return &Structural{Structural: s.Structural.Items}
}
func (s *Structural) Properties() map[string]common.Schema {
if s.Structural.Properties == nil {
return nil
}
res := make(map[string]common.Schema, len(s.Structural.Properties))
for n, prop := range s.Structural.Properties {
s := prop
res[n] = &Structural{Structural: &s}
}
return res
}
func (s *Structural) AdditionalProperties() common.SchemaOrBool {
if s.Structural.AdditionalProperties == nil {
return nil
}
return &StructuralOrBool{StructuralOrBool: s.Structural.AdditionalProperties}
}
func (s *Structural) Default() any {
return s.Structural.Default.Object
}
func (s *Structural) MaxItems() *int64 {
if s.Structural.ValueValidation == nil {
return nil
}
return s.Structural.ValueValidation.MaxItems
}
func (s *Structural) MaxLength() *int64 {
if s.Structural.ValueValidation == nil {
return nil
}
return s.Structural.ValueValidation.MaxLength
}
func (s *Structural) MaxProperties() *int64 {
if s.Structural.ValueValidation == nil {
return nil
}
return s.Structural.ValueValidation.MaxProperties
}
func (s *Structural) Required() []string {
if s.Structural.ValueValidation == nil {
return nil
}
return s.Structural.ValueValidation.Required
}
func (s *Structural) Enum() []any {
if s.Structural.ValueValidation == nil {
return nil
}
ret := make([]any, 0, len(s.Structural.ValueValidation.Enum))
for _, e := range s.Structural.ValueValidation.Enum {
ret = append(ret, e.Object)
}
return ret
}
func (s *Structural) Nullable() bool {
return s.Structural.Nullable
}
func (s *Structural) IsXIntOrString() bool {
return s.Structural.XIntOrString
}
func (s *Structural) IsXEmbeddedResource() bool {
return s.Structural.XEmbeddedResource
}
func (s *Structural) IsXPreserveUnknownFields() bool {
return s.Structural.XPreserveUnknownFields
}
func (s *Structural) XListType() string {
if s.Structural.XListType == nil {
return ""
}
return *s.Structural.XListType
}
func (s *Structural) XListMapKeys() []string {
return s.Structural.XListMapKeys
}
func (s *Structural) WithTypeAndObjectMeta() common.Schema {
return &Structural{Structural: WithTypeAndObjectMeta(s.Structural)}
}

View File

@ -17,19 +17,12 @@ limitations under the License.
package model package model
import ( import (
"time"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
apiservercel "k8s.io/apiserver/pkg/cel" apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
) )
// TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable
const maxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
// SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the // SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the
// structural schema should not be exposed in CEL expressions. // structural schema should not be exposed in CEL expressions.
// Set isResourceRoot to true for the root of a custom resource or embedded resource. // Set isResourceRoot to true for the root of a custom resource or embedded resource.
@ -40,152 +33,7 @@ const maxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
// //
// The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields. // The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *apiservercel.DeclType { func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *apiservercel.DeclType {
if s == nil { return common.SchemaDeclType(&Structural{Structural: s}, isResourceRoot)
return nil
}
if s.XIntOrString {
// schemas using XIntOrString are not required to have a type.
// intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
// In CEL, the type is represented as dynamic value, which can be thought of as a union type of all types.
// All type checking for XIntOrString is deferred to runtime, so all access to values of this type must
// be guarded with a type check, e.g.:
//
// To require that the string representation be a percentage:
// `type(intOrStringField) == string && intOrStringField.matches(r'(\d+(\.\d+)?%)')`
// To validate requirements on both the int and string representation:
// `type(intOrStringField) == int ? intOrStringField < 5 : double(intOrStringField.replace('%', '')) < 0.5
//
dyn := apiservercel.NewSimpleTypeWithMinSize("dyn", cel.DynType, nil, 1) // smallest value for a serialized x-kubernetes-int-or-string is 0
// handle x-kubernetes-int-or-string by returning the max length/min serialized size of the largest possible string
dyn.MaxElements = maxRequestSizeBytes - 2
return dyn
}
// We ignore XPreserveUnknownFields since we don't support validation rules on
// data that we don't have schema information for.
if isResourceRoot {
// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible to validator rules
// at the root of resources, even if not specified in the schema.
// This includes the root of a custom resource and the root of XEmbeddedResource objects.
s = WithTypeAndObjectMeta(s)
}
switch s.Type {
case "array":
if s.Items != nil {
itemsType := SchemaDeclType(s.Items, s.Items.XEmbeddedResource)
if itemsType == nil {
return nil
}
var maxItems int64
if s.ValueValidation != nil && s.ValueValidation.MaxItems != nil {
maxItems = zeroIfNegative(*s.ValueValidation.MaxItems)
} else {
maxItems = estimateMaxArrayItemsFromMinSize(itemsType.MinSerializedSize)
}
return apiservercel.NewListType(itemsType, maxItems)
}
return nil
case "object":
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
propsType := SchemaDeclType(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource)
if propsType != nil {
var maxProperties int64
if s.ValueValidation != nil && s.ValueValidation.MaxProperties != nil {
maxProperties = zeroIfNegative(*s.ValueValidation.MaxProperties)
} else {
maxProperties = estimateMaxAdditionalPropertiesFromMinSize(propsType.MinSerializedSize)
}
return apiservercel.NewMapType(apiservercel.StringType, propsType, maxProperties)
}
return nil
}
fields := make(map[string]*apiservercel.DeclField, len(s.Properties))
required := map[string]bool{}
if s.ValueValidation != nil {
for _, f := range s.ValueValidation.Required {
required[f] = true
}
}
// an object will always be serialized at least as {}, so account for that
minSerializedSize := int64(2)
for name, prop := range s.Properties {
var enumValues []interface{}
if prop.ValueValidation != nil {
for _, e := range prop.ValueValidation.Enum {
enumValues = append(enumValues, e.Object)
}
}
if fieldType := SchemaDeclType(&prop, prop.XEmbeddedResource); fieldType != nil {
if propName, ok := apiservercel.Escape(name); ok {
fields[propName] = apiservercel.NewDeclField(propName, fieldType, required[name], enumValues, prop.Default.Object)
}
// the min serialized size for an object is 2 (for {}) plus the min size of all its required
// properties
// only include required properties without a default value; default values are filled in
// server-side
if required[name] && prop.Default.Object == nil {
minSerializedSize += int64(len(name)) + fieldType.MinSerializedSize + 4
}
}
}
objType := apiservercel.NewObjectType("object", fields)
objType.MinSerializedSize = minSerializedSize
return objType
case "string":
if s.ValueValidation != nil {
switch s.ValueValidation.Format {
case "byte":
byteWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("bytes", cel.BytesType, types.Bytes([]byte{}), apiservercel.MinStringSize)
if s.ValueValidation.MaxLength != nil {
byteWithMaxLength.MaxElements = zeroIfNegative(*s.ValueValidation.MaxLength)
} else {
byteWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
}
return byteWithMaxLength
case "duration":
durationWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("duration", cel.DurationType, types.Duration{Duration: time.Duration(0)}, int64(apiservercel.MinDurationSizeJSON))
durationWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return durationWithMaxLength
case "date":
timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.JSONDateSize))
timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return timestampWithMaxLength
case "date-time":
timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.MinDatetimeSizeJSON))
timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return timestampWithMaxLength
}
}
strWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("string", cel.StringType, types.String(""), apiservercel.MinStringSize)
if s.ValueValidation != nil && s.ValueValidation.MaxLength != nil {
// multiply the user-provided max length by 4 in the case of an otherwise-untyped string
// we do this because the OpenAPIv3 spec indicates that maxLength is specified in runes/code points,
// but we need to reason about length for things like request size, so we use bytes in this code (and an individual
// unicode code point can be up to 4 bytes long)
strWithMaxLength.MaxElements = zeroIfNegative(*s.ValueValidation.MaxLength) * 4
} else {
strWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
}
return strWithMaxLength
case "boolean":
return apiservercel.BoolType
case "number":
return apiservercel.DoubleType
case "integer":
return apiservercel.IntType
}
return nil
}
func zeroIfNegative(v int64) int64 {
if v < 0 {
return 0
}
return v
} }
// WithTypeAndObjectMeta ensures the kind, apiVersion and // WithTypeAndObjectMeta ensures the kind, apiVersion and
@ -223,52 +71,3 @@ func WithTypeAndObjectMeta(s *schema.Structural) *schema.Structural {
return result return result
} }
// MaxCardinality returns the maximum number of times data conforming to the minimum size given could possibly exist in
// an object serialized to JSON. For cases where a schema is contained under map or array schemas of unbounded
// size, this can be used as an estimate as the worst case number of times data matching the schema could be repeated.
// Note that this only assumes a single comma between data elements, so if the schema is contained under only maps,
// this estimates a higher cardinality that would be possible. DeclType.MinSerializedSize is meant to be passed to
// this function.
func MaxCardinality(minSize int64) uint64 {
sz := minSize + 1 // assume at least one comma between elements
return uint64(maxRequestSizeBytes / sz)
}
// estimateMaxStringLengthPerRequest estimates the maximum string length (in characters)
// of a string compatible with the format requirements in the provided schema.
// must only be called on schemas of type "string" or x-kubernetes-int-or-string: true
func estimateMaxStringLengthPerRequest(s *schema.Structural) int64 {
if s.ValueValidation == nil || s.XIntOrString {
// subtract 2 to account for ""
return maxRequestSizeBytes - 2
}
switch s.ValueValidation.Format {
case "duration":
return apiservercel.MaxDurationSizeJSON
case "date":
return apiservercel.JSONDateSize
case "date-time":
return apiservercel.MaxDatetimeSizeJSON
default:
// subtract 2 to account for ""
return maxRequestSizeBytes - 2
}
}
// estimateMaxArrayItemsPerRequest estimates the maximum number of array items with
// the provided minimum serialized size that can fit into a single request.
func estimateMaxArrayItemsFromMinSize(minSize int64) int64 {
// subtract 2 to account for [ and ]
return (maxRequestSizeBytes - 2) / (minSize + 1)
}
// estimateMaxAdditionalPropertiesPerRequest estimates the maximum number of additional properties
// with the provided minimum serialized size that can fit into a single request.
func estimateMaxAdditionalPropertiesFromMinSize(minSize int64) int64 {
// 2 bytes for key + "" + colon + comma + smallest possible value, realistically the actual keys
// will all vary in length
keyValuePairSize := minSize + 6
// subtract 2 to account for { and }
return (maxRequestSizeBytes - 2) / keyValuePairSize
}

View File

@ -366,7 +366,7 @@ func (s *Validator) validateArray(ctx context.Context, fldPath *field.Path, sts
correlatableOldItems := makeMapList(sts, oldObj) correlatableOldItems := makeMapList(sts, oldObj)
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)
errs = append(errs, err...) errs = append(errs, err...)
if remainingBudget < 0 { if remainingBudget < 0 {
return errs, remainingBudget return errs, remainingBudget

View File

@ -17,688 +17,16 @@ limitations under the License.
package cel package cel
import ( import (
"fmt"
"reflect"
"sync"
"time"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
"k8s.io/apimachinery/pkg/api/equality" celopenapi "k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel"
"k8s.io/kube-openapi/pkg/validation/strfmt"
) )
// UnstructuredToVal converts a Kubernetes unstructured data element to a CEL Val. // UnstructuredToVal converts a Kubernetes unstructured data element to a CEL Val.
// The root schema of custom resource schema is expected contain type meta and object meta schemas. // The root schema of custom resource schema is expected contain type meta and object meta schemas.
// If Embedded resources do not contain type meta and object meta schemas, they will be added automatically. // If Embedded resources do not contain type meta and object meta schemas, they will be added automatically.
func UnstructuredToVal(unstructured interface{}, schema *structuralschema.Structural) ref.Val { func UnstructuredToVal(unstructured interface{}, schema *structuralschema.Structural) ref.Val {
if unstructured == nil { return celopenapi.UnstructuredToVal(unstructured, &model.Structural{Structural: schema})
if schema.Nullable {
return types.NullValue
}
return types.NewErr("invalid data, got null for schema with nullable=false")
}
if schema.XIntOrString {
switch v := unstructured.(type) {
case string:
return types.String(v)
case int:
return types.Int(v)
case int32:
return types.Int(v)
case int64:
return types.Int(v)
}
return types.NewErr("invalid data, expected XIntOrString value to be either a string or integer")
}
if schema.Type == "object" {
m, ok := unstructured.(map[string]interface{})
if !ok {
return types.NewErr("invalid data, expected a map for the provided schema with type=object")
}
if schema.XEmbeddedResource || schema.Properties != nil {
if schema.XEmbeddedResource {
schema = model.WithTypeAndObjectMeta(schema)
}
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (*structuralschema.Structural, bool) {
if schema, ok := schema.Properties[key]; ok {
return &schema, true
}
return nil, false
},
}
}
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Structural != nil {
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (*structuralschema.Structural, bool) {
return schema.AdditionalProperties.Structural, true
},
}
}
// A object with x-kubernetes-preserve-unknown-fields but no properties or additionalProperties is treated
// as an empty object.
if schema.XPreserveUnknownFields {
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (*structuralschema.Structural, bool) {
return nil, false
},
}
}
return types.NewErr("invalid object type, expected either Properties or AdditionalProperties with Allows=true and non-empty Schema")
}
if schema.Type == "array" {
l, ok := unstructured.([]interface{})
if !ok {
return types.NewErr("invalid data, expected an array for the provided schema with type=array")
}
if schema.Items == nil {
return types.NewErr("invalid array type, expected Items with a non-empty Schema")
}
typedList := unstructuredList{elements: l, itemsSchema: schema.Items}
listType := schema.XListType
if listType != nil {
switch *listType {
case "map":
mapKeys := schema.Extensions.XListMapKeys
return &unstructuredMapList{unstructuredList: typedList, escapedKeyProps: escapeKeyProps(mapKeys)}
case "set":
return &unstructuredSetList{unstructuredList: typedList}
case "atomic":
return &typedList
default:
return types.NewErr("invalid x-kubernetes-list-type, expected 'map', 'set' or 'atomic' but got %s", *listType)
}
}
return &typedList
}
if schema.Type == "string" {
str, ok := unstructured.(string)
if !ok {
return types.NewErr("invalid data, expected string, got %T", unstructured)
}
if schema.ValueValidation != nil {
switch schema.ValueValidation.Format {
case "duration":
d, err := strfmt.ParseDuration(str)
if err != nil {
return types.NewErr("Invalid duration %s: %v", str, err)
}
return types.Duration{Duration: d}
case "date":
d, err := time.Parse(strfmt.RFC3339FullDate, str) // strfmt uses this format for OpenAPIv3 value validation
if err != nil {
return types.NewErr("Invalid date formatted string %s: %v", str, err)
}
return types.Timestamp{Time: d}
case "date-time":
d, err := strfmt.ParseDateTime(str)
if err != nil {
return types.NewErr("Invalid date-time formatted string %s: %v", str, err)
}
return types.Timestamp{Time: time.Time(d)}
case "byte":
base64 := strfmt.Base64{}
err := base64.UnmarshalText([]byte(str))
if err != nil {
return types.NewErr("Invalid byte formatted string %s: %v", str, err)
}
return types.Bytes(base64)
}
}
return types.String(str)
}
if schema.Type == "number" {
switch v := unstructured.(type) {
// float representations of whole numbers (e.g. 1.0, 0.0) can convert to int representations (e.g. 1, 0) in yaml
// to json translation, and then get parsed as int64s
case int:
return types.Double(v)
case int32:
return types.Double(v)
case int64:
return types.Double(v)
case float32:
return types.Double(v)
case float64:
return types.Double(v)
default:
return types.NewErr("invalid data, expected float, got %T", unstructured)
}
}
if schema.Type == "integer" {
switch v := unstructured.(type) {
case int:
return types.Int(v)
case int32:
return types.Int(v)
case int64:
return types.Int(v)
default:
return types.NewErr("invalid data, expected int, got %T", unstructured)
}
}
if schema.Type == "boolean" {
b, ok := unstructured.(bool)
if !ok {
return types.NewErr("invalid data, expected bool, got %T", unstructured)
}
return types.Bool(b)
}
if schema.XPreserveUnknownFields {
return &unknownPreserved{u: unstructured}
}
return types.NewErr("invalid type, expected object, array, number, integer, boolean or string, or no type with x-kubernetes-int-or-string or x-kubernetes-preserve-unknown-fields is true, got %s", schema.Type)
}
// unknownPreserved represents unknown data preserved in custom resources via x-kubernetes-preserve-unknown-fields.
// It preserves the data at runtime without assuming it is of any particular type and supports only equality checking.
// unknownPreserved should be used only for values are not directly accessible in CEL expressions, i.e. for data
// where there is no corresponding CEL type declaration.
type unknownPreserved struct {
u interface{}
}
func (t *unknownPreserved) ConvertToNative(refType reflect.Type) (interface{}, error) {
return nil, fmt.Errorf("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", refType)
}
func (t *unknownPreserved) ConvertToType(typeValue ref.Type) ref.Val {
return types.NewErr("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", typeValue.TypeName())
}
func (t *unknownPreserved) Equal(other ref.Val) ref.Val {
return types.Bool(equality.Semantic.DeepEqual(t.u, other.Value()))
}
func (t *unknownPreserved) Type() ref.Type {
return types.UnknownType
}
func (t *unknownPreserved) Value() interface{} {
return t.u // used by Equal checks
}
// unstructuredMapList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=map.
type unstructuredMapList struct {
unstructuredList
escapedKeyProps []string
sync.Once // for for lazy load of mapOfList since it is only needed if Equals is called
mapOfList map[interface{}]interface{}
}
func (t *unstructuredMapList) getMap() map[interface{}]interface{} {
t.Do(func() {
t.mapOfList = make(map[interface{}]interface{}, len(t.elements))
for _, e := range t.elements {
t.mapOfList[t.toMapKey(e)] = e
}
})
return t.mapOfList
}
// toMapKey returns a valid golang map key for the given element of the map list.
// element must be a valid map list entry where all map key props are scalar types (which are comparable in go
// and valid for use in a golang map key).
func (t *unstructuredMapList) toMapKey(element interface{}) interface{} {
eObj, ok := element.(map[string]interface{})
if !ok {
return types.NewErr("unexpected data format for element of array with x-kubernetes-list-type=map: %T", element)
}
// Arrays are comparable in go and may be used as map keys, but maps and slices are not.
// So we can special case small numbers of key props as arrays and fall back to serialization
// for larger numbers of key props
if len(t.escapedKeyProps) == 1 {
return eObj[t.escapedKeyProps[0]]
}
if len(t.escapedKeyProps) == 2 {
return [2]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]]}
}
if len(t.escapedKeyProps) == 3 {
return [3]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]], eObj[t.escapedKeyProps[2]]}
}
key := make([]interface{}, len(t.escapedKeyProps))
for i, kf := range t.escapedKeyProps {
key[i] = eObj[kf]
}
return fmt.Sprintf("%v", key)
}
// Equal on a map list ignores list element order.
func (t *unstructuredMapList) Equal(other ref.Val) ref.Val {
oMapList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(t.elements))
if sz != oMapList.Size() {
return types.False
}
tMap := t.getMap()
for it := oMapList.Iterator(); it.HasNext() == types.True; {
v := it.Next()
k := t.toMapKey(v.Value())
tVal, ok := tMap[k]
if !ok {
return types.False
}
eq := UnstructuredToVal(tVal, t.itemsSchema).Equal(v)
if eq != types.True {
return eq // either false or error
}
}
return types.True
}
// Add for a map list `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
// non-intersecting keys are appended, retaining their partial order.
func (t *unstructuredMapList) Add(other ref.Val) ref.Val {
oMapList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
elements := make([]interface{}, len(t.elements))
keyToIdx := map[interface{}]int{}
for i, e := range t.elements {
k := t.toMapKey(e)
keyToIdx[k] = i
elements[i] = e
}
for it := oMapList.Iterator(); it.HasNext() == types.True; {
v := it.Next().Value()
k := t.toMapKey(v)
if overwritePosition, ok := keyToIdx[k]; ok {
elements[overwritePosition] = v
} else {
elements = append(elements, v)
}
}
return &unstructuredMapList{
unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
escapedKeyProps: t.escapedKeyProps,
}
}
// escapeKeyProps returns identifiers with Escape applied to each.
// Identifiers that cannot be escaped are left as-is. They are inaccessible to CEL programs but are
// are still needed internally to perform equality checks.
func escapeKeyProps(idents []string) []string {
result := make([]string, len(idents))
for i, prop := range idents {
if escaped, ok := cel.Escape(prop); ok {
result[i] = escaped
} else {
result[i] = prop
}
}
return result
}
// unstructuredSetList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=set.
type unstructuredSetList struct {
unstructuredList
escapedKeyProps []string
sync.Once // for for lazy load of setOfList since it is only needed if Equals is called
set map[interface{}]struct{}
}
func (t *unstructuredSetList) getSet() map[interface{}]struct{} {
// sets are only allowed to contain scalar elements, which are comparable in go, and can safely be used as
// golang map keys
t.Do(func() {
t.set = make(map[interface{}]struct{}, len(t.elements))
for _, e := range t.elements {
t.set[e] = struct{}{}
}
})
return t.set
}
// Equal on a map list ignores list element order.
func (t *unstructuredSetList) Equal(other ref.Val) ref.Val {
oSetList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(t.elements))
if sz != oSetList.Size() {
return types.False
}
tSet := t.getSet()
for it := oSetList.Iterator(); it.HasNext() == types.True; {
next := it.Next().Value()
_, ok := tSet[next]
if !ok {
return types.False
}
}
return types.True
}
// Add for a set list `X + Y` performs a union where the array positions of all elements in `X` are preserved and
// non-intersecting elements in `Y` are appended, retaining their partial order.
func (t *unstructuredSetList) Add(other ref.Val) ref.Val {
oSetList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
elements := t.elements
set := t.getSet()
for it := oSetList.Iterator(); it.HasNext() == types.True; {
next := it.Next().Value()
if _, ok := set[next]; !ok {
set[next] = struct{}{}
elements = append(elements, next)
}
}
return &unstructuredSetList{
unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
escapedKeyProps: t.escapedKeyProps,
}
}
// unstructuredList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=atomic (the default).
type unstructuredList struct {
elements []interface{}
itemsSchema *structuralschema.Structural
}
var _ = traits.Lister(&unstructuredList{})
func (t *unstructuredList) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
switch typeDesc.Kind() {
case reflect.Slice:
return t.elements, nil
}
return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
}
func (t *unstructuredList) ConvertToType(typeValue ref.Type) ref.Val {
switch typeValue {
case types.ListType:
return t
case types.TypeType:
return types.ListType
}
return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
}
func (t *unstructuredList) Equal(other ref.Val) ref.Val {
oList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(t.elements))
if sz != oList.Size() {
return types.False
}
for i := types.Int(0); i < sz; i++ {
eq := t.Get(i).Equal(oList.Get(i))
if eq != types.True {
return eq // either false or error
}
}
return types.True
}
func (t *unstructuredList) Type() ref.Type {
return types.ListType
}
func (t *unstructuredList) Value() interface{} {
return t.elements
}
func (t *unstructuredList) Add(other ref.Val) ref.Val {
oList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
elements := t.elements
for it := oList.Iterator(); it.HasNext() == types.True; {
next := it.Next().Value()
elements = append(elements, next)
}
return &unstructuredList{elements: elements, itemsSchema: t.itemsSchema}
}
func (t *unstructuredList) Contains(val ref.Val) ref.Val {
if types.IsUnknownOrError(val) {
return val
}
var err ref.Val
sz := len(t.elements)
for i := 0; i < sz; i++ {
elem := UnstructuredToVal(t.elements[i], t.itemsSchema)
cmp := elem.Equal(val)
b, ok := cmp.(types.Bool)
if !ok && err == nil {
err = types.MaybeNoSuchOverloadErr(cmp)
}
if b == types.True {
return types.True
}
}
if err != nil {
return err
}
return types.False
}
func (t *unstructuredList) Get(idx ref.Val) ref.Val {
iv, isInt := idx.(types.Int)
if !isInt {
return types.ValOrErr(idx, "unsupported index: %v", idx)
}
i := int(iv)
if i < 0 || i >= len(t.elements) {
return types.NewErr("index out of bounds: %v", idx)
}
return UnstructuredToVal(t.elements[i], t.itemsSchema)
}
func (t *unstructuredList) Iterator() traits.Iterator {
items := make([]ref.Val, len(t.elements))
for i, item := range t.elements {
itemCopy := item
items[i] = UnstructuredToVal(itemCopy, t.itemsSchema)
}
return &listIterator{unstructuredList: t, items: items}
}
type listIterator struct {
*unstructuredList
items []ref.Val
idx int
}
func (it *listIterator) HasNext() ref.Val {
return types.Bool(it.idx < len(it.items))
}
func (it *listIterator) Next() ref.Val {
item := it.items[it.idx]
it.idx++
return item
}
func (t *unstructuredList) Size() ref.Val {
return types.Int(len(t.elements))
}
// unstructuredMap represented an unstructured data instance of an OpenAPI object.
type unstructuredMap struct {
value map[string]interface{}
schema *structuralschema.Structural
// propSchema finds the schema to use for a particular map key.
propSchema func(key string) (*structuralschema.Structural, bool)
}
var _ = traits.Mapper(&unstructuredMap{})
func (t *unstructuredMap) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
switch typeDesc.Kind() {
case reflect.Map:
return t.value, nil
}
return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
}
func (t *unstructuredMap) ConvertToType(typeValue ref.Type) ref.Val {
switch typeValue {
case types.MapType:
return t
case types.TypeType:
return types.MapType
}
return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
}
func (t *unstructuredMap) Equal(other ref.Val) ref.Val {
oMap, isMap := other.(traits.Mapper)
if !isMap {
return types.MaybeNoSuchOverloadErr(other)
}
if t.Size() != oMap.Size() {
return types.False
}
for key, value := range t.value {
if propSchema, ok := t.propSchema(key); ok {
ov, found := oMap.Find(types.String(key))
if !found {
return types.False
}
v := UnstructuredToVal(value, propSchema)
vEq := v.Equal(ov)
if vEq != types.True {
return vEq // either false or error
}
} else {
// Must be an object with properties.
// Since we've encountered an unknown field, fallback to unstructured equality checking.
ouMap, ok := other.(*unstructuredMap)
if !ok {
// The compiler ensures equality is against the same type of object, so this should be unreachable
return types.MaybeNoSuchOverloadErr(other)
}
if oValue, ok := ouMap.value[key]; ok {
if !equality.Semantic.DeepEqual(value, oValue) {
return types.False
}
}
}
}
return types.True
}
func (t *unstructuredMap) Type() ref.Type {
return types.MapType
}
func (t *unstructuredMap) Value() interface{} {
return t.value
}
func (t *unstructuredMap) Contains(key ref.Val) ref.Val {
v, found := t.Find(key)
if v != nil && types.IsUnknownOrError(v) {
return v
}
return types.Bool(found)
}
func (t *unstructuredMap) Get(key ref.Val) ref.Val {
v, found := t.Find(key)
if found {
return v
}
return types.ValOrErr(key, "no such key: %v", key)
}
func (t *unstructuredMap) Iterator() traits.Iterator {
isObject := t.schema.Properties != nil
keys := make([]ref.Val, len(t.value))
i := 0
for k := range t.value {
if _, ok := t.propSchema(k); ok {
mapKey := k
if isObject {
if escaped, ok := cel.Escape(k); ok {
mapKey = escaped
}
}
keys[i] = types.String(mapKey)
i++
}
}
return &mapIterator{unstructuredMap: t, keys: keys}
}
type mapIterator struct {
*unstructuredMap
keys []ref.Val
idx int
}
func (it *mapIterator) HasNext() ref.Val {
return types.Bool(it.idx < len(it.keys))
}
func (it *mapIterator) Next() ref.Val {
key := it.keys[it.idx]
it.idx++
return key
}
func (t *unstructuredMap) Size() ref.Val {
return types.Int(len(t.value))
}
func (t *unstructuredMap) Find(key ref.Val) (ref.Val, bool) {
isObject := t.schema.Properties != nil
keyStr, ok := key.(types.String)
if !ok {
return types.MaybeNoSuchOverloadErr(key), true
}
k := keyStr.Value().(string)
if isObject {
k, ok = cel.Unescape(k)
if !ok {
return nil, false
}
}
if v, ok := t.value[k]; ok {
// If this is an object with properties, not an object with additionalProperties,
// then null valued nullable fields are treated the same as absent optional fields.
if isObject && v == nil {
return nil, false
}
if propSchema, ok := t.propSchema(k); ok {
return UnstructuredToVal(v, propSchema), true
}
}
return nil, false
} }

View File

@ -60,6 +60,7 @@ require (
cloud.google.com/go v0.97.0 // indirect cloud.google.com/go v0.97.0 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cenkalti/backoff/v4 v4.1.3 // indirect
@ -87,6 +88,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect

View File

@ -62,6 +62,8 @@ github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -355,6 +357,8 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View File

@ -0,0 +1,11 @@
# See the OWNERS docs at https://go.k8s.io/owners
# Kubernetes CEL library authors and maintainers
approvers:
- jpbetz
- cici37
- jiahuif
reviewers:
- jpbetz
- cici37
- jiahuif

View File

@ -0,0 +1,81 @@
/*
Copyright 2023 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 common
// Schema is the adapted type for an OpenAPI schema that CEL uses.
// This schema does not cover all OpenAPI fields but only these CEL requires
// are exposed as getters.
type Schema interface {
// Type returns the OpenAPI type.
// Multiple types are not supported. It should return
// empty string if no type is specified.
Type() string
// Format returns the OpenAPI format. May be empty
Format() string
// Items returns the OpenAPI items. or nil of this field does not exist or
// contains no schema.
Items() Schema
// Properties returns the OpenAPI properties, or nil if this field does not
// exist.
// The values of the returned map are of the adapted type.
Properties() map[string]Schema
// AdditionalProperties returns the OpenAPI additional properties field,
// or nil if this field does not exist.
AdditionalProperties() SchemaOrBool
// Default returns the OpenAPI default field, or nil if this field does not exist.
Default() any
Validations
KubeExtensions
// WithTypeAndObjectMeta returns a schema that has the type and object meta set.
// the type includes "kind", "apiVersion" field
// the "metadata" field requires "name" and "generateName" to be set
// The original schema must not be mutated. Make a copy if necessary.
WithTypeAndObjectMeta() Schema
}
// Validations contains OpenAPI validation that the CEL library uses.
type Validations interface {
MaxItems() *int64
MaxLength() *int64
MaxProperties() *int64
Required() []string
Enum() []any
Nullable() bool
}
// KubeExtensions contains Kubernetes-specific extensions to the OpenAPI schema.
type KubeExtensions interface {
IsXIntOrString() bool
IsXEmbeddedResource() bool
IsXPreserveUnknownFields() bool
XListType() string
XListMapKeys() []string
}
// SchemaOrBool contains either a schema or a boolean indicating if the object
// can contain any fields.
type SchemaOrBool interface {
Schema() Schema
Allows() bool
}

View File

@ -0,0 +1,177 @@
/*
Copyright 2022 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 common
import (
"fmt"
"strings"
)
// MapList provides a "lookup by key" operation for lists (arrays) with x-kubernetes-list-type=map.
type MapList interface {
// Get returns the first element having given key, for all
// x-kubernetes-list-map-keys, to the provided object. If the provided object isn't itself a valid MapList element,
// get returns nil.
Get(interface{}) interface{}
}
type keyStrategy interface {
// CompositeKeyFor returns a composite key for the provided object, if possible, and a
// boolean that indicates whether or not a key could be generated for the provided object.
CompositeKeyFor(map[string]interface{}) (interface{}, bool)
}
// singleKeyStrategy is a cheaper strategy for associative lists that have exactly one key.
type singleKeyStrategy struct {
key string
}
// CompositeKeyFor directly returns the value of the single key to
// use as a composite key.
func (ks *singleKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
v, ok := obj[ks.key]
if !ok {
return nil, false
}
switch v.(type) {
case bool, float64, int64, string:
return v, true
default:
return nil, false // non-scalar
}
}
// multiKeyStrategy computes a composite key of all key values.
type multiKeyStrategy struct {
sts Schema
}
// CompositeKeyFor returns a composite key computed from the values of all
// keys.
func (ks *multiKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
const keyDelimiter = "\x00" // 0 byte should never appear in the composite key except as delimiter
var delimited strings.Builder
for _, key := range ks.sts.XListMapKeys() {
v, ok := obj[key]
if !ok {
return nil, false
}
switch v.(type) {
case bool:
fmt.Fprintf(&delimited, keyDelimiter+"%t", v)
case float64:
fmt.Fprintf(&delimited, keyDelimiter+"%f", v)
case int64:
fmt.Fprintf(&delimited, keyDelimiter+"%d", v)
case string:
fmt.Fprintf(&delimited, keyDelimiter+"%q", v)
default:
return nil, false // values must be scalars
}
}
return delimited.String(), true
}
// emptyMapList is a MapList containing no elements.
type emptyMapList struct{}
func (emptyMapList) Get(interface{}) interface{} {
return nil
}
type mapListImpl struct {
sts Schema
ks keyStrategy
// 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{} {
mobj, ok := obj.(map[string]interface{})
if !ok {
return nil
}
key, ok := a.ks.CompositeKeyFor(mobj)
if !ok {
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:]
// key the item
mitem, ok := item.(map[string]interface{})
if !ok {
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
}
}
return nil
}
func makeKeyStrategy(sts Schema) keyStrategy {
listMapKeys := sts.XListMapKeys()
if len(listMapKeys) == 1 {
key := listMapKeys[0]
return &singleKeyStrategy{
key: key,
}
}
return &multiKeyStrategy{
sts: sts,
}
}
// MakeMapList returns a queryable interface over the provided x-kubernetes-list-type=map
// keyedItems. If the provided schema is _not_ an array with x-kubernetes-list-type=map, returns an
// empty mapList.
func MakeMapList(sts Schema, items []interface{}) (rv MapList) {
if sts.Type() != "array" || sts.XListType() != "map" || len(sts.XListMapKeys()) == 0 || len(items) == 0 {
return emptyMapList{}
}
ks := makeKeyStrategy(sts)
return &mapListImpl{
sts: sts,
ks: ks,
keyedItems: map[interface{}]interface{}{},
unkeyedItems: items,
}
}

View File

@ -0,0 +1,257 @@
/*
Copyright 2022 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 common
import (
"time"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/kube-openapi/pkg/validation/spec"
)
const maxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
// SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the
// structural schema should not be exposed in CEL expressions.
// Set isResourceRoot to true for the root of a custom resource or embedded resource.
//
// Schemas with XPreserveUnknownFields not exposed unless they are objects. Array and "maps" schemas
// are not exposed if their items or additionalProperties schemas are not exposed. Object Properties are not exposed
// if their schema is not exposed.
//
// The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
func SchemaDeclType(s Schema, isResourceRoot bool) *apiservercel.DeclType {
if s == nil {
return nil
}
if s.IsXIntOrString() {
// schemas using XIntOrString are not required to have a type.
// intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
// In CEL, the type is represented as dynamic value, which can be thought of as a union type of all types.
// All type checking for XIntOrString is deferred to runtime, so all access to values of this type must
// be guarded with a type check, e.g.:
//
// To require that the string representation be a percentage:
// `type(intOrStringField) == string && intOrStringField.matches(r'(\d+(\.\d+)?%)')`
// To validate requirements on both the int and string representation:
// `type(intOrStringField) == int ? intOrStringField < 5 : double(intOrStringField.replace('%', '')) < 0.5
//
dyn := apiservercel.NewSimpleTypeWithMinSize("dyn", cel.DynType, nil, 1) // smallest value for a serialized x-kubernetes-int-or-string is 0
// handle x-kubernetes-int-or-string by returning the max length/min serialized size of the largest possible string
dyn.MaxElements = maxRequestSizeBytes - 2
return dyn
}
// We ignore XPreserveUnknownFields since we don't support validation rules on
// data that we don't have schema information for.
if isResourceRoot {
// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible to validator rules
// at the root of resources, even if not specified in the schema.
// This includes the root of a custom resource and the root of XEmbeddedResource objects.
s = s.WithTypeAndObjectMeta()
}
switch s.Type() {
case "array":
if s.Items() != nil {
itemsType := SchemaDeclType(s.Items(), s.Items().IsXEmbeddedResource())
if itemsType == nil {
return nil
}
var maxItems int64
if s.MaxItems() != nil {
maxItems = zeroIfNegative(*s.MaxItems())
} else {
maxItems = estimateMaxArrayItemsFromMinSize(itemsType.MinSerializedSize)
}
return apiservercel.NewListType(itemsType, maxItems)
}
return nil
case "object":
if s.AdditionalProperties() != nil && s.AdditionalProperties().Schema() != nil {
propsType := SchemaDeclType(s.AdditionalProperties().Schema(), s.AdditionalProperties().Schema().IsXEmbeddedResource())
if propsType != nil {
var maxProperties int64
if s.MaxProperties() != nil {
maxProperties = zeroIfNegative(*s.MaxProperties())
} else {
maxProperties = estimateMaxAdditionalPropertiesFromMinSize(propsType.MinSerializedSize)
}
return apiservercel.NewMapType(apiservercel.StringType, propsType, maxProperties)
}
return nil
}
fields := make(map[string]*apiservercel.DeclField, len(s.Properties()))
required := map[string]bool{}
if s.Required() != nil {
for _, f := range s.Required() {
required[f] = true
}
}
// an object will always be serialized at least as {}, so account for that
minSerializedSize := int64(2)
for name, prop := range s.Properties() {
var enumValues []interface{}
if prop.Enum() != nil {
for _, e := range prop.Enum() {
enumValues = append(enumValues, e)
}
}
if fieldType := SchemaDeclType(prop, prop.IsXEmbeddedResource()); fieldType != nil {
if propName, ok := apiservercel.Escape(name); ok {
fields[propName] = apiservercel.NewDeclField(propName, fieldType, required[name], enumValues, prop.Default())
}
// the min serialized size for an object is 2 (for {}) plus the min size of all its required
// properties
// only include required properties without a default value; default values are filled in
// server-side
if required[name] && prop.Default() == nil {
minSerializedSize += int64(len(name)) + fieldType.MinSerializedSize + 4
}
}
}
objType := apiservercel.NewObjectType("object", fields)
objType.MinSerializedSize = minSerializedSize
return objType
case "string":
switch s.Format() {
case "byte":
byteWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("bytes", cel.BytesType, types.Bytes([]byte{}), apiservercel.MinStringSize)
if s.MaxLength() != nil {
byteWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength())
} else {
byteWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
}
return byteWithMaxLength
case "duration":
durationWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("duration", cel.DurationType, types.Duration{Duration: time.Duration(0)}, int64(apiservercel.MinDurationSizeJSON))
durationWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return durationWithMaxLength
case "date":
timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.JSONDateSize))
timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return timestampWithMaxLength
case "date-time":
timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.MinDatetimeSizeJSON))
timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return timestampWithMaxLength
}
strWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("string", cel.StringType, types.String(""), apiservercel.MinStringSize)
if s.MaxLength() != nil {
// multiply the user-provided max length by 4 in the case of an otherwise-untyped string
// we do this because the OpenAPIv3 spec indicates that maxLength is specified in runes/code points,
// but we need to reason about length for things like request size, so we use bytes in this code (and an individual
// unicode code point can be up to 4 bytes long)
strWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength()) * 4
} else {
strWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
}
return strWithMaxLength
case "boolean":
return apiservercel.BoolType
case "number":
return apiservercel.DoubleType
case "integer":
return apiservercel.IntType
}
return nil
}
func zeroIfNegative(v int64) int64 {
if v < 0 {
return 0
}
return v
}
// WithTypeAndObjectMeta ensures the kind, apiVersion and
// metadata.name and metadata.generateName properties are specified, making a shallow copy of the provided schema if needed.
func WithTypeAndObjectMeta(s *spec.Schema) *spec.Schema {
if s.Properties != nil &&
s.Properties["kind"].Type.Contains("string") &&
s.Properties["apiVersion"].Type.Contains("string") &&
s.Properties["metadata"].Type.Contains("object") &&
s.Properties["metadata"].Properties != nil &&
s.Properties["metadata"].Properties["name"].Type.Contains("string") &&
s.Properties["metadata"].Properties["generateName"].Type.Contains("string") {
return s
}
result := *s
props := make(map[string]spec.Schema, len(s.Properties))
for k, prop := range s.Properties {
props[k] = prop
}
stringType := spec.StringProperty()
props["kind"] = *stringType
props["apiVersion"] = *stringType
props["metadata"] = spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": *stringType,
"generateName": *stringType,
},
},
}
result.Properties = props
return &result
}
// estimateMaxStringLengthPerRequest estimates the maximum string length (in characters)
// of a string compatible with the format requirements in the provided schema.
// must only be called on schemas of type "string" or x-kubernetes-int-or-string: true
func estimateMaxStringLengthPerRequest(s Schema) int64 {
if s.IsXIntOrString() {
return maxRequestSizeBytes - 2
}
switch s.Format() {
case "duration":
return apiservercel.MaxDurationSizeJSON
case "date":
return apiservercel.JSONDateSize
case "date-time":
return apiservercel.MaxDatetimeSizeJSON
default:
// subtract 2 to account for ""
return maxRequestSizeBytes - 2
}
}
// estimateMaxArrayItemsPerRequest estimates the maximum number of array items with
// the provided minimum serialized size that can fit into a single request.
func estimateMaxArrayItemsFromMinSize(minSize int64) int64 {
// subtract 2 to account for [ and ]
return (maxRequestSizeBytes - 2) / (minSize + 1)
}
// estimateMaxAdditionalPropertiesPerRequest estimates the maximum number of additional properties
// with the provided minimum serialized size that can fit into a single request.
func estimateMaxAdditionalPropertiesFromMinSize(minSize int64) int64 {
// 2 bytes for key + "" + colon + comma + smallest possible value, realistically the actual keys
// will all vary in length
keyValuePairSize := minSize + 6
// subtract 2 to account for { and }
return (maxRequestSizeBytes - 2) / keyValuePairSize
}

View File

@ -0,0 +1,701 @@
/*
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 common
import (
"fmt"
"reflect"
"sync"
"time"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apiserver/pkg/cel"
"k8s.io/kube-openapi/pkg/validation/strfmt"
)
// UnstructuredToVal converts a Kubernetes unstructured data element to a CEL Val.
// The root schema of custom resource schema is expected contain type meta and object meta schemas.
// If Embedded resources do not contain type meta and object meta schemas, they will be added automatically.
func UnstructuredToVal(unstructured interface{}, schema Schema) ref.Val {
if unstructured == nil {
if schema.Nullable() {
return types.NullValue
}
return types.NewErr("invalid data, got null for schema with nullable=false")
}
if schema.IsXIntOrString() {
switch v := unstructured.(type) {
case string:
return types.String(v)
case int:
return types.Int(v)
case int32:
return types.Int(v)
case int64:
return types.Int(v)
}
return types.NewErr("invalid data, expected XIntOrString value to be either a string or integer")
}
if schema.Type() == "object" {
m, ok := unstructured.(map[string]interface{})
if !ok {
return types.NewErr("invalid data, expected a map for the provided schema with type=object")
}
if schema.IsXEmbeddedResource() || schema.Properties() != nil {
if schema.IsXEmbeddedResource() {
schema = schema.WithTypeAndObjectMeta()
}
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (Schema, bool) {
if schema, ok := schema.Properties()[key]; ok {
return schema, true
}
return nil, false
},
}
}
if schema.AdditionalProperties() != nil && schema.AdditionalProperties().Schema() != nil {
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (Schema, bool) {
return schema.AdditionalProperties().Schema(), true
},
}
}
// A object with x-kubernetes-preserve-unknown-fields but no properties or additionalProperties is treated
// as an empty object.
if schema.IsXPreserveUnknownFields() {
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (Schema, bool) {
return nil, false
},
}
}
return types.NewErr("invalid object type, expected either Properties or AdditionalProperties with Allows=true and non-empty Schema")
}
if schema.Type() == "array" {
l, ok := unstructured.([]interface{})
if !ok {
return types.NewErr("invalid data, expected an array for the provided schema with type=array")
}
if schema.Items() == nil {
return types.NewErr("invalid array type, expected Items with a non-empty Schema")
}
typedList := unstructuredList{elements: l, itemsSchema: schema.Items()}
listType := schema.XListType()
if listType != "" {
switch listType {
case "map":
mapKeys := schema.XListMapKeys()
return &unstructuredMapList{unstructuredList: typedList, escapedKeyProps: escapeKeyProps(mapKeys)}
case "set":
return &unstructuredSetList{unstructuredList: typedList}
case "atomic":
return &typedList
default:
return types.NewErr("invalid x-kubernetes-list-type, expected 'map', 'set' or 'atomic' but got %s", listType)
}
}
return &typedList
}
if schema.Type() == "string" {
str, ok := unstructured.(string)
if !ok {
return types.NewErr("invalid data, expected string, got %T", unstructured)
}
switch schema.Format() {
case "duration":
d, err := strfmt.ParseDuration(str)
if err != nil {
return types.NewErr("Invalid duration %s: %v", str, err)
}
return types.Duration{Duration: d}
case "date":
d, err := time.Parse(strfmt.RFC3339FullDate, str) // strfmt uses this format for OpenAPIv3 value validation
if err != nil {
return types.NewErr("Invalid date formatted string %s: %v", str, err)
}
return types.Timestamp{Time: d}
case "date-time":
d, err := strfmt.ParseDateTime(str)
if err != nil {
return types.NewErr("Invalid date-time formatted string %s: %v", str, err)
}
return types.Timestamp{Time: time.Time(d)}
case "byte":
base64 := strfmt.Base64{}
err := base64.UnmarshalText([]byte(str))
if err != nil {
return types.NewErr("Invalid byte formatted string %s: %v", str, err)
}
return types.Bytes(base64)
}
return types.String(str)
}
if schema.Type() == "number" {
switch v := unstructured.(type) {
// float representations of whole numbers (e.g. 1.0, 0.0) can convert to int representations (e.g. 1, 0) in yaml
// to json translation, and then get parsed as int64s
case int:
return types.Double(v)
case int32:
return types.Double(v)
case int64:
return types.Double(v)
case float32:
return types.Double(v)
case float64:
return types.Double(v)
default:
return types.NewErr("invalid data, expected float, got %T", unstructured)
}
}
if schema.Type() == "integer" {
switch v := unstructured.(type) {
case int:
return types.Int(v)
case int32:
return types.Int(v)
case int64:
return types.Int(v)
default:
return types.NewErr("invalid data, expected int, got %T", unstructured)
}
}
if schema.Type() == "boolean" {
b, ok := unstructured.(bool)
if !ok {
return types.NewErr("invalid data, expected bool, got %T", unstructured)
}
return types.Bool(b)
}
if schema.IsXPreserveUnknownFields() {
return &unknownPreserved{u: unstructured}
}
return types.NewErr("invalid type, expected object, array, number, integer, boolean or string, or no type with x-kubernetes-int-or-string or x-kubernetes-preserve-unknown-fields is true, got %s", schema.Type())
}
// unknownPreserved represents unknown data preserved in custom resources via x-kubernetes-preserve-unknown-fields.
// It preserves the data at runtime without assuming it is of any particular type and supports only equality checking.
// unknownPreserved should be used only for values are not directly accessible in CEL expressions, i.e. for data
// where there is no corresponding CEL type declaration.
type unknownPreserved struct {
u interface{}
}
func (t *unknownPreserved) ConvertToNative(refType reflect.Type) (interface{}, error) {
return nil, fmt.Errorf("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", refType)
}
func (t *unknownPreserved) ConvertToType(typeValue ref.Type) ref.Val {
return types.NewErr("type conversion to '%s' not supported for values preserved by x-kubernetes-preserve-unknown-fields", typeValue.TypeName())
}
func (t *unknownPreserved) Equal(other ref.Val) ref.Val {
return types.Bool(equality.Semantic.DeepEqual(t.u, other.Value()))
}
func (t *unknownPreserved) Type() ref.Type {
return types.UnknownType
}
func (t *unknownPreserved) Value() interface{} {
return t.u // used by Equal checks
}
// unstructuredMapList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=map.
type unstructuredMapList struct {
unstructuredList
escapedKeyProps []string
sync.Once // for for lazy load of mapOfList since it is only needed if Equals is called
mapOfList map[interface{}]interface{}
}
func (t *unstructuredMapList) getMap() map[interface{}]interface{} {
t.Do(func() {
t.mapOfList = make(map[interface{}]interface{}, len(t.elements))
for _, e := range t.elements {
t.mapOfList[t.toMapKey(e)] = e
}
})
return t.mapOfList
}
// toMapKey returns a valid golang map key for the given element of the map list.
// element must be a valid map list entry where all map key props are scalar types (which are comparable in go
// and valid for use in a golang map key).
func (t *unstructuredMapList) toMapKey(element interface{}) interface{} {
eObj, ok := element.(map[string]interface{})
if !ok {
return types.NewErr("unexpected data format for element of array with x-kubernetes-list-type=map: %T", element)
}
// Arrays are comparable in go and may be used as map keys, but maps and slices are not.
// So we can special case small numbers of key props as arrays and fall back to serialization
// for larger numbers of key props
if len(t.escapedKeyProps) == 1 {
return eObj[t.escapedKeyProps[0]]
}
if len(t.escapedKeyProps) == 2 {
return [2]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]]}
}
if len(t.escapedKeyProps) == 3 {
return [3]interface{}{eObj[t.escapedKeyProps[0]], eObj[t.escapedKeyProps[1]], eObj[t.escapedKeyProps[2]]}
}
key := make([]interface{}, len(t.escapedKeyProps))
for i, kf := range t.escapedKeyProps {
key[i] = eObj[kf]
}
return fmt.Sprintf("%v", key)
}
// Equal on a map list ignores list element order.
func (t *unstructuredMapList) Equal(other ref.Val) ref.Val {
oMapList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(t.elements))
if sz != oMapList.Size() {
return types.False
}
tMap := t.getMap()
for it := oMapList.Iterator(); it.HasNext() == types.True; {
v := it.Next()
k := t.toMapKey(v.Value())
tVal, ok := tMap[k]
if !ok {
return types.False
}
eq := UnstructuredToVal(tVal, t.itemsSchema).Equal(v)
if eq != types.True {
return eq // either false or error
}
}
return types.True
}
// Add for a map list `X + Y` performs a merge where the array positions of all keys in `X` are preserved but the values
// are overwritten by values in `Y` when the key sets of `X` and `Y` intersect. Elements in `Y` with
// non-intersecting keys are appended, retaining their partial order.
func (t *unstructuredMapList) Add(other ref.Val) ref.Val {
oMapList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
elements := make([]interface{}, len(t.elements))
keyToIdx := map[interface{}]int{}
for i, e := range t.elements {
k := t.toMapKey(e)
keyToIdx[k] = i
elements[i] = e
}
for it := oMapList.Iterator(); it.HasNext() == types.True; {
v := it.Next().Value()
k := t.toMapKey(v)
if overwritePosition, ok := keyToIdx[k]; ok {
elements[overwritePosition] = v
} else {
elements = append(elements, v)
}
}
return &unstructuredMapList{
unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
escapedKeyProps: t.escapedKeyProps,
}
}
// escapeKeyProps returns identifiers with Escape applied to each.
// Identifiers that cannot be escaped are left as-is. They are inaccessible to CEL programs but are
// are still needed internally to perform equality checks.
func escapeKeyProps(idents []string) []string {
result := make([]string, len(idents))
for i, prop := range idents {
if escaped, ok := cel.Escape(prop); ok {
result[i] = escaped
} else {
result[i] = prop
}
}
return result
}
// unstructuredSetList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=set.
type unstructuredSetList struct {
unstructuredList
escapedKeyProps []string
sync.Once // for for lazy load of setOfList since it is only needed if Equals is called
set map[interface{}]struct{}
}
func (t *unstructuredSetList) getSet() map[interface{}]struct{} {
// sets are only allowed to contain scalar elements, which are comparable in go, and can safely be used as
// golang map keys
t.Do(func() {
t.set = make(map[interface{}]struct{}, len(t.elements))
for _, e := range t.elements {
t.set[e] = struct{}{}
}
})
return t.set
}
// Equal on a map list ignores list element order.
func (t *unstructuredSetList) Equal(other ref.Val) ref.Val {
oSetList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(t.elements))
if sz != oSetList.Size() {
return types.False
}
tSet := t.getSet()
for it := oSetList.Iterator(); it.HasNext() == types.True; {
next := it.Next().Value()
_, ok := tSet[next]
if !ok {
return types.False
}
}
return types.True
}
// Add for a set list `X + Y` performs a union where the array positions of all elements in `X` are preserved and
// non-intersecting elements in `Y` are appended, retaining their partial order.
func (t *unstructuredSetList) Add(other ref.Val) ref.Val {
oSetList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
elements := t.elements
set := t.getSet()
for it := oSetList.Iterator(); it.HasNext() == types.True; {
next := it.Next().Value()
if _, ok := set[next]; !ok {
set[next] = struct{}{}
elements = append(elements, next)
}
}
return &unstructuredSetList{
unstructuredList: unstructuredList{elements: elements, itemsSchema: t.itemsSchema},
escapedKeyProps: t.escapedKeyProps,
}
}
// unstructuredList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=atomic (the default).
type unstructuredList struct {
elements []interface{}
itemsSchema Schema
}
var _ = traits.Lister(&unstructuredList{})
func (t *unstructuredList) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
switch typeDesc.Kind() {
case reflect.Slice:
return t.elements, nil
}
return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
}
func (t *unstructuredList) ConvertToType(typeValue ref.Type) ref.Val {
switch typeValue {
case types.ListType:
return t
case types.TypeType:
return types.ListType
}
return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
}
func (t *unstructuredList) Equal(other ref.Val) ref.Val {
oList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(t.elements))
if sz != oList.Size() {
return types.False
}
for i := types.Int(0); i < sz; i++ {
eq := t.Get(i).Equal(oList.Get(i))
if eq != types.True {
return eq // either false or error
}
}
return types.True
}
func (t *unstructuredList) Type() ref.Type {
return types.ListType
}
func (t *unstructuredList) Value() interface{} {
return t.elements
}
func (t *unstructuredList) Add(other ref.Val) ref.Val {
oList, ok := other.(traits.Lister)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
elements := t.elements
for it := oList.Iterator(); it.HasNext() == types.True; {
next := it.Next().Value()
elements = append(elements, next)
}
return &unstructuredList{elements: elements, itemsSchema: t.itemsSchema}
}
func (t *unstructuredList) Contains(val ref.Val) ref.Val {
if types.IsUnknownOrError(val) {
return val
}
var err ref.Val
sz := len(t.elements)
for i := 0; i < sz; i++ {
elem := UnstructuredToVal(t.elements[i], t.itemsSchema)
cmp := elem.Equal(val)
b, ok := cmp.(types.Bool)
if !ok && err == nil {
err = types.MaybeNoSuchOverloadErr(cmp)
}
if b == types.True {
return types.True
}
}
if err != nil {
return err
}
return types.False
}
func (t *unstructuredList) Get(idx ref.Val) ref.Val {
iv, isInt := idx.(types.Int)
if !isInt {
return types.ValOrErr(idx, "unsupported index: %v", idx)
}
i := int(iv)
if i < 0 || i >= len(t.elements) {
return types.NewErr("index out of bounds: %v", idx)
}
return UnstructuredToVal(t.elements[i], t.itemsSchema)
}
func (t *unstructuredList) Iterator() traits.Iterator {
items := make([]ref.Val, len(t.elements))
for i, item := range t.elements {
itemCopy := item
items[i] = UnstructuredToVal(itemCopy, t.itemsSchema)
}
return &listIterator{unstructuredList: t, items: items}
}
type listIterator struct {
*unstructuredList
items []ref.Val
idx int
}
func (it *listIterator) HasNext() ref.Val {
return types.Bool(it.idx < len(it.items))
}
func (it *listIterator) Next() ref.Val {
item := it.items[it.idx]
it.idx++
return item
}
func (t *unstructuredList) Size() ref.Val {
return types.Int(len(t.elements))
}
// unstructuredMap represented an unstructured data instance of an OpenAPI object.
type unstructuredMap struct {
value map[string]interface{}
schema Schema
// propSchema finds the schema to use for a particular map key.
propSchema func(key string) (Schema, bool)
}
var _ = traits.Mapper(&unstructuredMap{})
func (t *unstructuredMap) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
switch typeDesc.Kind() {
case reflect.Map:
return t.value, nil
}
return nil, fmt.Errorf("type conversion error from '%s' to '%s'", t.Type(), typeDesc)
}
func (t *unstructuredMap) ConvertToType(typeValue ref.Type) ref.Val {
switch typeValue {
case types.MapType:
return t
case types.TypeType:
return types.MapType
}
return types.NewErr("type conversion error from '%s' to '%s'", t.Type(), typeValue.TypeName())
}
func (t *unstructuredMap) Equal(other ref.Val) ref.Val {
oMap, isMap := other.(traits.Mapper)
if !isMap {
return types.MaybeNoSuchOverloadErr(other)
}
if t.Size() != oMap.Size() {
return types.False
}
for key, value := range t.value {
if propSchema, ok := t.propSchema(key); ok {
ov, found := oMap.Find(types.String(key))
if !found {
return types.False
}
v := UnstructuredToVal(value, propSchema)
vEq := v.Equal(ov)
if vEq != types.True {
return vEq // either false or error
}
} else {
// Must be an object with properties.
// Since we've encountered an unknown field, fallback to unstructured equality checking.
ouMap, ok := other.(*unstructuredMap)
if !ok {
// The compiler ensures equality is against the same type of object, so this should be unreachable
return types.MaybeNoSuchOverloadErr(other)
}
if oValue, ok := ouMap.value[key]; ok {
if !equality.Semantic.DeepEqual(value, oValue) {
return types.False
}
}
}
}
return types.True
}
func (t *unstructuredMap) Type() ref.Type {
return types.MapType
}
func (t *unstructuredMap) Value() interface{} {
return t.value
}
func (t *unstructuredMap) Contains(key ref.Val) ref.Val {
v, found := t.Find(key)
if v != nil && types.IsUnknownOrError(v) {
return v
}
return types.Bool(found)
}
func (t *unstructuredMap) Get(key ref.Val) ref.Val {
v, found := t.Find(key)
if found {
return v
}
return types.ValOrErr(key, "no such key: %v", key)
}
func (t *unstructuredMap) Iterator() traits.Iterator {
isObject := t.schema.Properties() != nil
keys := make([]ref.Val, len(t.value))
i := 0
for k := range t.value {
if _, ok := t.propSchema(k); ok {
mapKey := k
if isObject {
if escaped, ok := cel.Escape(k); ok {
mapKey = escaped
}
}
keys[i] = types.String(mapKey)
i++
}
}
return &mapIterator{unstructuredMap: t, keys: keys}
}
type mapIterator struct {
*unstructuredMap
keys []ref.Val
idx int
}
func (it *mapIterator) HasNext() ref.Val {
return types.Bool(it.idx < len(it.keys))
}
func (it *mapIterator) Next() ref.Val {
key := it.keys[it.idx]
it.idx++
return key
}
func (t *unstructuredMap) Size() ref.Val {
return types.Int(len(t.value))
}
func (t *unstructuredMap) Find(key ref.Val) (ref.Val, bool) {
isObject := t.schema.Properties() != nil
keyStr, ok := key.(types.String)
if !ok {
return types.MaybeNoSuchOverloadErr(key), true
}
k := keyStr.Value().(string)
if isObject {
k, ok = cel.Unescape(k)
if !ok {
return nil, false
}
}
if v, ok := t.value[k]; ok {
// If this is an object with properties, not an object with additionalProperties,
// then null valued nullable fields are treated the same as absent optional fields.
if isObject && v == nil {
return nil, false
}
if propSchema, ok := t.propSchema(k); ok {
return UnstructuredToVal(v, propSchema), true
}
}
return nil, false
}

View File

@ -0,0 +1,147 @@
/*
Copyright 2023 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 openapi
import (
"github.com/google/cel-go/common/types/ref"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var _ common.Schema = (*Schema)(nil)
var _ common.SchemaOrBool = (*SchemaOrBool)(nil)
type Schema struct {
Schema *spec.Schema
}
type SchemaOrBool struct {
SchemaOrBool *spec.SchemaOrBool
}
func (sb *SchemaOrBool) Schema() common.Schema {
return &Schema{Schema: sb.SchemaOrBool.Schema}
}
func (sb *SchemaOrBool) Allows() bool {
return sb.SchemaOrBool.Allows
}
func (s *Schema) Type() string {
if len(s.Schema.Type) == 0 {
return ""
}
return s.Schema.Type[0]
}
func (s *Schema) Format() string {
return s.Schema.Format
}
func (s *Schema) Items() common.Schema {
if s.Schema.Items == nil || s.Schema.Items.Schema == nil {
return nil
}
return &Schema{Schema: s.Schema.Items.Schema}
}
func (s *Schema) Properties() map[string]common.Schema {
if s.Schema.Properties == nil {
return nil
}
res := make(map[string]common.Schema, len(s.Schema.Properties))
for n, prop := range s.Schema.Properties {
// map value is unaddressable, create a shallow copy
// this is a shallow non-recursive copy
s := prop
res[n] = &Schema{Schema: &s}
}
return res
}
func (s *Schema) AdditionalProperties() common.SchemaOrBool {
if s.Schema.AdditionalProperties == nil {
return nil
}
return &SchemaOrBool{SchemaOrBool: s.Schema.AdditionalProperties}
}
func (s *Schema) Default() any {
return s.Schema.Default
}
func (s *Schema) MaxItems() *int64 {
return s.Schema.MaxItems
}
func (s *Schema) MaxLength() *int64 {
return s.Schema.MaxLength
}
func (s *Schema) MaxProperties() *int64 {
return s.Schema.MaxProperties
}
func (s *Schema) Required() []string {
return s.Schema.Required
}
func (s *Schema) Enum() []any {
return s.Schema.Enum
}
func (s *Schema) Nullable() bool {
return s.Schema.Nullable
}
func (s *Schema) IsXIntOrString() bool {
return isXIntOrString(s.Schema)
}
func (s *Schema) IsXEmbeddedResource() bool {
return isXEmbeddedResource(s.Schema)
}
func (s *Schema) IsXPreserveUnknownFields() bool {
return isXPreserveUnknownFields(s.Schema)
}
func (s *Schema) XListType() string {
return getXListType(s.Schema)
}
func (s *Schema) XListMapKeys() []string {
return getXListMapKeys(s.Schema)
}
func (s *Schema) WithTypeAndObjectMeta() common.Schema {
return &Schema{common.WithTypeAndObjectMeta(s.Schema)}
}
func UnstructuredToVal(unstructured any, schema *spec.Schema) ref.Val {
return common.UnstructuredToVal(unstructured, &Schema{schema})
}
func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType {
return common.SchemaDeclType(&Schema{Schema: s}, isResourceRoot)
}
func MakeMapList(sts *spec.Schema, items []interface{}) (rv common.MapList) {
return common.MakeMapList(&Schema{Schema: sts}, items)
}

View File

@ -0,0 +1,62 @@
/*
Copyright 2022 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 openapi
import (
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var intOrStringFormat = intstr.IntOrString{}.OpenAPISchemaFormat()
func isExtension(schema *spec.Schema, key string) bool {
v, ok := schema.Extensions.GetBool(key)
return v && ok
}
func isXIntOrString(schema *spec.Schema) bool {
// built-in types have the Format while CRDs use extension
// both are valid, checking both
return schema.Format == intOrStringFormat || isExtension(schema, extIntOrString)
}
func isXEmbeddedResource(schema *spec.Schema) bool {
return isExtension(schema, extEmbeddedResource)
}
func isXPreserveUnknownFields(schema *spec.Schema) bool {
return isExtension(schema, extPreserveUnknownFields)
}
func getXListType(schema *spec.Schema) string {
s, _ := schema.Extensions.GetString(extListType)
return s
}
func getXListMapKeys(schema *spec.Schema) []string {
mapKeys, ok := schema.Extensions.GetStringSlice(extListMapKeys)
if !ok {
return nil
}
return mapKeys
}
const extIntOrString = "x-kubernetes-int-or-string"
const extEmbeddedResource = "x-kubernetes-embedded-resource"
const extPreserveUnknownFields = "x-kubernetes-preserve-unknown-fields"
const extListType = "x-kubernetes-list-type"
const extListMapKeys = "x-kubernetes-list-map-keys"

View File

@ -0,0 +1,309 @@
/*
Copyright 2022 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 openapi
import (
"reflect"
"testing"
"k8s.io/kube-openapi/pkg/validation/spec"
)
func TestMapList(t *testing.T) {
for _, tc := range []struct {
name string
sts *spec.Schema
items []interface{}
warmUpQueries []interface{}
query interface{}
expected interface{}
}{
{
name: "default list type",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
}},
query: map[string]interface{}{},
expected: nil,
},
{
name: "non list type",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"map"},
}},
query: map[string]interface{}{},
expected: nil,
},
{
name: "non-map list type",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
},
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
extListType: listTypeSet,
}}},
query: map[string]interface{}{},
expected: nil,
},
{
name: "no keys",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
}},
query: map[string]interface{}{},
expected: nil,
},
{
name: "single key",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
},
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
extListType: listTypeMap,
extListMapKeys: []any{"k"},
}}},
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 ignoring non-map query",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
}},
items: []interface{}{
map[string]interface{}{
"k": "a",
"v1": "a",
},
},
query: 42,
expected: nil,
},
{
name: "single key ignoring unkeyable query",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
},
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
extListType: listTypeMap,
extListMapKeys: []any{"k"},
}}},
items: []interface{}{
map[string]interface{}{
"k": "a",
"v1": "a",
},
},
query: map[string]interface{}{
"k": map[string]interface{}{
"keys": "must",
"be": "scalars",
},
"v1": "A",
},
expected: nil,
},
{
name: "ignores item of invalid type",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
},
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
extListType: listTypeMap,
extListMapKeys: []any{"k"},
}}},
items: []interface{}{
map[string]interface{}{
"k": "a",
"v1": "a",
},
5,
},
query: map[string]interface{}{
"k": "a",
"v1": "A",
},
expected: map[string]interface{}{
"k": "a",
"v1": "a",
},
},
{
name: "keep first entry when duplicated keys are encountered",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
},
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
extListType: listTypeMap,
extListMapKeys: []any{"k"},
}}},
items: []interface{}{
map[string]interface{}{
"k": "a",
"v1": "a",
},
map[string]interface{}{
"k": "a",
"v1": "b",
},
},
query: map[string]interface{}{
"k": "a",
"v1": "A",
},
expected: map[string]interface{}{
"k": "a",
"v1": "a",
},
},
{
name: "keep first entry when duplicated multi-keys are encountered",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
},
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
extListType: listTypeMap,
extListMapKeys: []any{"k1", "k2"},
}}},
items: []interface{}{
map[string]interface{}{
"k1": "a",
"k2": "b",
"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{}{
"k1": "a",
"k2": "b",
},
expected: map[string]interface{}{
"k1": "a",
"k2": "b",
"v1": "a",
},
},
{
name: "multiple keys with defaults ignores item with nil value for key",
sts: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Properties: map[string]spec.Schema{
"kb": {SchemaProps: spec.SchemaProps{
Default: true,
}},
"kf": {SchemaProps: spec.SchemaProps{
Default: 2.0,
}},
"ki": {SchemaProps: spec.SchemaProps{
Default: int64(64),
}},
"ks": {
SchemaProps: spec.SchemaProps{
Default: "hello",
}},
},
},
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
extListType: listTypeMap,
extListMapKeys: []any{"kb", "kf", "ki", "ks"},
}}},
items: []interface{}{
map[string]interface{}{
"kb": nil,
"kf": float64(2.0),
"ki": int64(42),
"ks": "hello",
"v1": "a",
},
map[string]interface{}{
"kb": false,
"kf": float64(2.0),
"ki": int64(42),
"ks": "hello",
"v1": "b",
},
},
query: map[string]interface{}{
"kb": false,
"kf": float64(2.0),
"ki": int64(42),
"ks": "hello",
"v1": "B",
},
expected: map[string]interface{}{
"kb": false,
"kf": float64(2.0),
"ki": int64(42),
"ks": "hello",
"v1": "b",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
mapList := MakeMapList(tc.sts, tc.items)
for _, warmUp := range tc.warmUpQueries {
mapList.Get(warmUp)
}
actual := mapList.Get(tc.query)
if !reflect.DeepEqual(tc.expected, actual) {
t.Errorf("got: %v, expected %v", actual, tc.expected)
}
})
}
}

View File

@ -0,0 +1,115 @@
/*
Copyright 2023 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 resolver
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/openapi"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// DefinitionsSchemaResolver resolves the schema of a built-in type
// by looking up the OpenAPI definitions.
type DefinitionsSchemaResolver struct {
defs map[string]common.OpenAPIDefinition
gvkToSchema map[schema.GroupVersionKind]*spec.Schema
}
// NewDefinitionsSchemaResolver creates a new DefinitionsSchemaResolver.
// An example working setup:
// scheme = "k8s.io/client-go/kubernetes/scheme".Scheme
// getDefinitions = "k8s.io/kubernetes/pkg/generated/openapi".GetOpenAPIDefinitions
func NewDefinitionsSchemaResolver(scheme *runtime.Scheme, getDefinitions common.GetOpenAPIDefinitions) *DefinitionsSchemaResolver {
gvkToSchema := make(map[schema.GroupVersionKind]*spec.Schema)
namer := openapi.NewDefinitionNamer(scheme)
defs := getDefinitions(func(path string) spec.Ref {
return spec.MustCreateRef(path)
})
for name, def := range defs {
_, e := namer.GetDefinitionName(name)
gvks := extensionsToGVKs(e)
s := def.Schema // map value not addressable, make copy
for _, gvk := range gvks {
gvkToSchema[gvk] = &s
}
}
return &DefinitionsSchemaResolver{
gvkToSchema: gvkToSchema,
defs: defs,
}
}
func (d *DefinitionsSchemaResolver) ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error) {
s, ok := d.gvkToSchema[gvk]
if !ok {
return nil, fmt.Errorf("cannot resolve %v: %w", gvk, ErrSchemaNotFound)
}
s, err := populateRefs(func(ref string) (*spec.Schema, bool) {
// find the schema by the ref string, and return a deep copy
def, ok := d.defs[ref]
if !ok {
return nil, false
}
s := def.Schema
return &s, true
}, s)
if err != nil {
return nil, err
}
return s, nil
}
func extensionsToGVKs(extensions spec.Extensions) []schema.GroupVersionKind {
gvksAny, ok := extensions[extGVK]
if !ok {
return nil
}
gvks, ok := gvksAny.([]any)
if !ok {
return nil
}
result := make([]schema.GroupVersionKind, 0, len(gvks))
for _, gvkAny := range gvks {
// type check the map and all fields
gvkMap, ok := gvkAny.(map[string]any)
if !ok {
return nil
}
g, ok := gvkMap["group"].(string)
if !ok {
return nil
}
v, ok := gvkMap["version"].(string)
if !ok {
return nil
}
k, ok := gvkMap["kind"].(string)
if !ok {
return nil
}
result = append(result, schema.GroupVersionKind{
Group: g,
Version: v,
Kind: k,
})
}
return result
}

View File

@ -0,0 +1,104 @@
/*
Copyright 2023 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 resolver
import (
"encoding/json"
"fmt"
"strings"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// ClientDiscoveryResolver uses client-go discovery to resolve schemas at run time.
type ClientDiscoveryResolver struct {
Discovery discovery.DiscoveryInterface
}
var _ SchemaResolver = (*ClientDiscoveryResolver)(nil)
func (r *ClientDiscoveryResolver) ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error) {
p, err := r.Discovery.OpenAPIV3().Paths()
if err != nil {
return nil, err
}
resourcePath := resourcePathFromGV(gvk.GroupVersion())
c, ok := p[resourcePath]
if !ok {
return nil, fmt.Errorf("cannot resolve group version %q: %w", gvk.GroupVersion(), ErrSchemaNotFound)
}
b, err := c.Schema(runtime.ContentTypeJSON)
if err != nil {
return nil, err
}
resp := new(schemaResponse)
err = json.Unmarshal(b, resp)
if err != nil {
return nil, err
}
s, err := resolveType(resp, gvk)
if err != nil {
return nil, err
}
s, err = populateRefs(func(ref string) (*spec.Schema, bool) {
s, ok := resp.Components.Schemas[strings.TrimPrefix(ref, refPrefix)]
return s, ok
}, s)
if err != nil {
return nil, err
}
return s, nil
}
func resolveType(resp *schemaResponse, gvk schema.GroupVersionKind) (*spec.Schema, error) {
for _, s := range resp.Components.Schemas {
var gvks []schema.GroupVersionKind
err := s.Extensions.GetObject(extGVK, &gvks)
if err != nil {
return nil, err
}
for _, g := range gvks {
if g == gvk {
return s, nil
}
}
}
return nil, fmt.Errorf("cannot resolve group version kind %q: %w", gvk, ErrSchemaNotFound)
}
func resourcePathFromGV(gv schema.GroupVersion) string {
var resourcePath string
if len(gv.Group) == 0 {
resourcePath = fmt.Sprintf("api/%s", gv.Version)
} else {
resourcePath = fmt.Sprintf("apis/%s/%s", gv.Group, gv.Version)
}
return resourcePath
}
type schemaResponse struct {
Components struct {
Schemas map[string]*spec.Schema `json:"schemas"`
} `json:"components"`
}
const refPrefix = "#/components/schemas/"
const extGVK = "x-kubernetes-group-version-kind"

View File

@ -0,0 +1,100 @@
/*
Copyright 2023 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 resolver
import (
"fmt"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// populateRefs recursively replaces Refs in the schema with the referred one.
// schemaOf is the callback to find the corresponding schema by the ref.
// This function will not mutate the original schema. If the schema needs to be
// mutated, a copy will be returned, otherwise it returns the original schema.
func populateRefs(schemaOf func(ref string) (*spec.Schema, bool), schema *spec.Schema) (*spec.Schema, error) {
result := *schema
changed := false
ref, isRef := refOf(schema)
if isRef {
// replace the whole schema with the referred one.
resolved, ok := schemaOf(ref)
if !ok {
return nil, fmt.Errorf("internal error: cannot resolve Ref %q: %w", ref, ErrSchemaNotFound)
}
result = *resolved
changed = true
}
// schema is an object, populate its properties and additionalProperties
props := make(map[string]spec.Schema, len(schema.Properties))
propsChanged := false
for name, prop := range result.Properties {
populated, err := populateRefs(schemaOf, &prop)
if err != nil {
return nil, err
}
if populated != &prop {
propsChanged = true
}
props[name] = *populated
}
if propsChanged {
changed = true
result.Properties = props
}
if result.AdditionalProperties != nil && result.AdditionalProperties.Schema != nil {
populated, err := populateRefs(schemaOf, result.AdditionalProperties.Schema)
if err != nil {
return nil, err
}
if populated != result.AdditionalProperties.Schema {
changed = true
result.AdditionalProperties.Schema = populated
}
}
// schema is a list, populate its items
if result.Items != nil && result.Items.Schema != nil {
populated, err := populateRefs(schemaOf, result.Items.Schema)
if err != nil {
return nil, err
}
if populated != result.Items.Schema {
changed = true
result.Items.Schema = populated
}
}
if changed {
return &result, nil
}
return schema, nil
}
func refOf(schema *spec.Schema) (string, bool) {
if schema.Ref.GetURL() != nil {
return schema.Ref.String(), true
}
// A Ref may be wrapped in allOf to preserve its description
// see https://github.com/kubernetes/kubernetes/issues/106387
// For kube-openapi, allOf is only used for wrapping a Ref.
for _, allOf := range schema.AllOf {
if ref, isRef := refOf(&allOf); isRef {
return ref, isRef
}
}
return "", false
}

View File

@ -0,0 +1,39 @@
/*
Copyright 2023 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 resolver
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// SchemaResolver finds the OpenAPI schema for the given GroupVersionKind.
// This interface uses the type defined by k8s.io/kube-openapi
type SchemaResolver interface {
// ResolveSchema takes a GroupVersionKind (GVK) and returns the OpenAPI schema
// identified by the GVK.
// The function returns a non-nil error if the schema cannot be found or fail
// to resolve. The returned error wraps ErrSchemaNotFound if the resolution is
// attempted but the corresponding schema cannot be found.
ResolveSchema(gvk schema.GroupVersionKind) (*spec.Schema, error)
}
// ErrSchemaNotFound is wrapped and returned if the schema cannot be located
// by the resolver.
var ErrSchemaNotFound = fmt.Errorf("schema not found")

View File

@ -0,0 +1,463 @@
/*
Copyright 2022 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 openapi
import (
"reflect"
"testing"
"github.com/google/cel-go/common/types"
"google.golang.org/protobuf/proto"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/kube-openapi/pkg/validation/spec"
)
func TestSchemaDeclType(t *testing.T) {
ts := testSchema()
cust := SchemaDeclType(ts, false)
if cust.TypeName() != "object" {
t.Errorf("incorrect type name, got %v, wanted object", cust.TypeName())
}
if len(cust.Fields) != 4 {
t.Errorf("incorrect number of fields, got %d, wanted 4", len(cust.Fields))
}
for _, f := range cust.Fields {
prop, found := ts.Properties[f.Name]
if !found {
t.Errorf("type field not found in schema, field: %s", f.Name)
}
fdv := f.DefaultValue()
if prop.Default != nil {
pdv := types.DefaultTypeAdapter.NativeToValue(prop.Default)
if !reflect.DeepEqual(fdv, pdv) {
t.Errorf("field and schema do not agree on default value for field: %s, field value: %v, schema default: %v", f.Name, fdv, pdv)
}
}
if (len(prop.Enum) == 0) && len(f.EnumValues()) != 0 {
t.Errorf("field had more enum values than the property. field: %s", f.Name)
}
fevs := f.EnumValues()
for _, fev := range fevs {
found := false
for _, pev := range prop.Enum {
celpev := types.DefaultTypeAdapter.NativeToValue(pev)
if reflect.DeepEqual(fev, celpev) {
found = true
break
}
}
if !found {
t.Errorf(
"could not find field enum value in property definition. field: %s, enum: %v",
f.Name, fev)
}
}
}
for _, name := range ts.Required {
df, found := cust.FindField(name)
if !found {
t.Errorf("custom type missing required field. field=%s", name)
}
if !df.Required {
t.Errorf("field marked as required in schema, but optional in type. field=%s", df.Name)
}
}
}
func TestSchemaDeclTypes(t *testing.T) {
ts := testSchema()
cust := SchemaDeclType(ts, true).MaybeAssignTypeName("CustomObject")
typeMap := apiservercel.FieldTypeMap("CustomObject", cust)
nested, _ := cust.FindField("nested")
metadata, _ := cust.FindField("metadata")
expectedObjTypeMap := map[string]*apiservercel.DeclType{
"CustomObject": cust,
"CustomObject.nested": nested.Type,
"CustomObject.metadata": metadata.Type,
}
objTypeMap := map[string]*apiservercel.DeclType{}
for name, t := range typeMap {
if t.IsObject() {
objTypeMap[name] = t
}
}
if len(objTypeMap) != len(expectedObjTypeMap) {
t.Errorf("got different type set. got=%v, wanted=%v", objTypeMap, expectedObjTypeMap)
}
for exp, expType := range expectedObjTypeMap {
actType, found := objTypeMap[exp]
if !found {
t.Errorf("missing type in rule types: %s", exp)
continue
}
expT, err := expType.ExprType()
if err != nil {
t.Errorf("fail to get cel type: %s", err)
}
actT, err := actType.ExprType()
if err != nil {
t.Errorf("fail to get cel type: %s", err)
}
if !proto.Equal(expT, actT) {
t.Errorf("incompatible CEL types. got=%v, wanted=%v", expT, actT)
}
}
}
func testSchema() *spec.Schema {
// Manual construction of a schema with the following definition:
//
// schema:
// type: object
// metadata:
// custom_type: "CustomObject"
// required:
// - name
// - value
// properties:
// name:
// type: string
// nested:
// type: object
// properties:
// subname:
// type: string
// flags:
// type: object
// additionalProperties:
// type: boolean
// dates:
// type: array
// items:
// type: string
// format: date-time
// metadata:
// type: object
// additionalProperties:
// type: object
// properties:
// key:
// type: string
// values:
// type: array
// items: string
// value:
// type: integer
// format: int64
// default: 1
// enum: [1,2,3]
ts := &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": *spec.StringProperty(),
"value": {SchemaProps: spec.SchemaProps{
Type: []string{"integer"},
Default: int64(1),
Format: "int64",
Enum: []any{1, 2, 3},
}},
"nested": {SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"subname": *spec.StringProperty(),
"flags": {SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: spec.BooleanProperty(),
},
}},
"dates": {SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "date-time",
}}}}},
},
},
},
"metadata": {SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"name": *spec.StringProperty(),
"value": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
}}},
},
},
},
}},
}}}
return ts
}
func arraySchema(arrayType, format string, maxItems *int64) *spec.Schema {
return &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{arrayType},
Format: format,
}}},
MaxItems: maxItems,
},
}
}
func maxPtr(max int64) *int64 {
return &max
}
func TestEstimateMaxLengthJSON(t *testing.T) {
type maxLengthTest struct {
Name string
InputSchema *spec.Schema
ExpectedMaxElements int64
}
tests := []maxLengthTest{
{
Name: "booleanArray",
InputSchema: arraySchema("boolean", "", nil),
// expected JSON is [true,true,...], so our length should be (maxRequestSizeBytes - 2) / 5
ExpectedMaxElements: 629145,
},
{
Name: "durationArray",
InputSchema: arraySchema("string", "duration", nil),
// expected JSON is ["0","0",...] so our length should be (maxRequestSizeBytes - 2) / 4
ExpectedMaxElements: 786431,
},
{
Name: "datetimeArray",
InputSchema: arraySchema("string", "date-time", nil),
// expected JSON is ["2000-01-01T01:01:01","2000-01-01T01:01:01",...] so our length should be (maxRequestSizeBytes - 2) / 22
ExpectedMaxElements: 142987,
},
{
Name: "dateArray",
InputSchema: arraySchema("string", "date", nil),
// expected JSON is ["2000-01-01","2000-01-02",...] so our length should be (maxRequestSizeBytes - 2) / 13
ExpectedMaxElements: 241978,
},
{
Name: "numberArray",
InputSchema: arraySchema("integer", "", nil),
// expected JSON is [0,0,...] so our length should be (maxRequestSizeBytes - 2) / 2
ExpectedMaxElements: 1572863,
},
{
Name: "stringArray",
InputSchema: arraySchema("string", "", nil),
// expected JSON is ["","",...] so our length should be (maxRequestSizeBytes - 2) / 3
ExpectedMaxElements: 1048575,
},
{
Name: "stringMap",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
}},
},
}},
// expected JSON is {"":"","":"",...} so our length should be (3000000 - 2) / 6
ExpectedMaxElements: 393215,
},
{
Name: "objectOptionalPropertyArray",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"required": *spec.StringProperty(),
"optional": *spec.StringProperty(),
},
Required: []string{"required"},
}}},
}},
// expected JSON is [{"required":"",},{"required":"",},...] so our length should be (maxRequestSizeBytes - 2) / 17
ExpectedMaxElements: 185042,
},
{
Name: "arrayWithLength",
InputSchema: arraySchema("integer", "int64", maxPtr(10)),
// manually set by MaxItems
ExpectedMaxElements: 10,
},
{
Name: "stringWithLength",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
MaxLength: maxPtr(20),
}},
// manually set by MaxLength, but we expect a 4x multiplier compared to the original input
// since OpenAPIv3 maxLength uses code points, but DeclType works with bytes
ExpectedMaxElements: 80,
},
{
Name: "mapWithLength",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Schema: spec.StringProperty(),
},
Format: "string",
MaxProperties: maxPtr(15),
}},
// manually set by MaxProperties
ExpectedMaxElements: 15,
},
{
Name: "durationMaxSize",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "duration",
}},
// should be exactly equal to maxDurationSizeJSON
ExpectedMaxElements: apiservercel.MaxDurationSizeJSON,
},
{
Name: "dateSize",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "date",
}},
// should be exactly equal to dateSizeJSON
ExpectedMaxElements: apiservercel.JSONDateSize,
},
{
Name: "maxdatetimeSize",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "date-time",
}},
// should be exactly equal to maxDatetimeSizeJSON
ExpectedMaxElements: apiservercel.MaxDatetimeSizeJSON,
},
{
Name: "maxintOrStringSize",
InputSchema: &spec.Schema{
VendorExtensible: spec.VendorExtensible{Extensions: map[string]interface{}{
extIntOrString: true,
}}},
// should be exactly equal to maxRequestSizeBytes - 2 (to allow for quotes in the case of a string)
ExpectedMaxElements: apiservercel.DefaultMaxRequestSizeBytes - 2,
},
{
Name: "objectDefaultFieldArray",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"field": {SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Default: "default",
},
}},
Required: []string{"field"},
}}},
},
},
// expected JSON is [{},{},...] so our length should be (maxRequestSizeBytes - 2) / 3
ExpectedMaxElements: 1048575,
},
{
Name: "byteStringSize",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "byte",
}},
// expected JSON is "" so our length should be (maxRequestSizeBytes - 2)
ExpectedMaxElements: 3145726,
},
{
Name: "byteStringSetMaxLength",
InputSchema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "byte",
MaxLength: maxPtr(20),
}},
// note that unlike regular strings we don't have to take unicode into account,
// so we expect the max length to be exactly equal to the user-supplied one
ExpectedMaxElements: 20,
},
}
for _, testCase := range tests {
t.Run(testCase.Name, func(t *testing.T) {
decl := SchemaDeclType(testCase.InputSchema, false)
if decl.MaxElements != testCase.ExpectedMaxElements {
t.Errorf("wrong maxElements (got %d, expected %d)", decl.MaxElements, testCase.ExpectedMaxElements)
}
})
}
}
func genNestedSchema(depth int) *spec.Schema {
var generator func(d int) spec.Schema
generator = func(d int) spec.Schema {
nodeTemplate := &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{},
}}
if d == 1 {
return *nodeTemplate
} else {
mapType := generator(d - 1)
nodeTemplate.AdditionalProperties.Schema = &mapType
return *nodeTemplate
}
}
schema := generator(depth)
return &schema
}
func BenchmarkDeeplyNestedSchemaDeclType(b *testing.B) {
benchmarkSchema := genNestedSchema(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
SchemaDeclType(benchmarkSchema, false)
}
}

View File

@ -0,0 +1,660 @@
/*
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 openapi
import (
"reflect"
"testing"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var (
listTypeSet = "set"
listTypeMap = "map"
stringSchema = spec.StringProperty()
intSchema = spec.Int64Property()
mapListElementSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"key": *stringSchema,
"val": *intSchema,
},
}}
mapListSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: mapListElementSchema},
},
VendorExtensible: spec.VendorExtensible{
Extensions: map[string]interface{}{
extListType: listTypeMap,
extListMapKeys: []any{"key"},
}},
}
multiKeyMapListSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"key1": *stringSchema,
"key2": *stringSchema,
"val": *intSchema,
},
}}}},
VendorExtensible: spec.VendorExtensible{
Extensions: map[string]interface{}{
extListType: listTypeMap,
extListMapKeys: []any{"key1", "key2"},
}},
}
setListSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: stringSchema}},
VendorExtensible: spec.VendorExtensible{
Extensions: map[string]interface{}{
extListType: listTypeSet,
}},
}
atomicListSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{Schema: stringSchema},
}}
objectSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"field1": *stringSchema,
"field2": *stringSchema,
},
}}
mapSchema = &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{Schema: stringSchema},
}}
)
func TestEquality(t *testing.T) {
cases := []struct {
name string
lhs ref.Val
rhs ref.Val
equal bool
}{
{
name: "map lists are equal regardless of order",
lhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key": "a",
"val": 1,
},
map[string]interface{}{
"key": "b",
"val": 2,
},
}, mapListSchema),
rhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key": "b",
"val": 2,
},
map[string]interface{}{
"key": "a",
"val": 1,
},
}, mapListSchema),
equal: true,
},
{
name: "map lists are not equal if contents differs",
lhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key": "a",
"val": 1,
},
map[string]interface{}{
"key": "b",
"val": 2,
},
}, mapListSchema),
rhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key": "a",
"val": 1,
},
map[string]interface{}{
"key": "b",
"val": 3,
},
}, mapListSchema),
equal: false,
},
{
name: "map lists are not equal if length differs",
lhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key": "a",
"val": 1,
},
map[string]interface{}{
"key": "b",
"val": 2,
},
}, mapListSchema),
rhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key": "a",
"val": 1,
},
map[string]interface{}{
"key": "b",
"val": 2,
},
map[string]interface{}{
"key": "c",
"val": 3,
},
}, mapListSchema),
equal: false,
},
{
name: "multi-key map lists are equal regardless of order",
lhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key1": "a1",
"key2": "a2",
"val": 1,
},
map[string]interface{}{
"key1": "b1",
"key2": "b2",
"val": 2,
},
}, multiKeyMapListSchema),
rhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key1": "b1",
"key2": "b2",
"val": 2,
},
map[string]interface{}{
"key1": "a1",
"key2": "a2",
"val": 1,
},
}, multiKeyMapListSchema),
equal: true,
},
{
name: "multi-key map lists with different contents are not equal",
lhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key1": "a1",
"key2": "a2",
"val": 1,
},
map[string]interface{}{
"key1": "b1",
"key2": "b2",
"val": 2,
},
}, multiKeyMapListSchema),
rhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key1": "a1",
"key2": "a2",
"val": 1,
},
map[string]interface{}{
"key1": "b1",
"key2": "b2",
"val": 3,
},
}, multiKeyMapListSchema),
equal: false,
},
{
name: "multi-key map lists with different keys are not equal",
lhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key1": "a1",
"key2": "a2",
"val": 1,
},
map[string]interface{}{
"key1": "b1",
"key2": "b2",
"val": 2,
},
}, multiKeyMapListSchema),
rhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key1": "a1",
"key2": "a2",
"val": 1,
},
map[string]interface{}{
"key1": "c1",
"key2": "c2",
"val": 3,
},
}, multiKeyMapListSchema),
equal: false,
},
{
name: "multi-key map lists with different lengths are not equal",
lhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key1": "a1",
"key2": "a2",
"val": 1,
},
}, multiKeyMapListSchema),
rhs: UnstructuredToVal([]interface{}{
map[string]interface{}{
"key1": "a1",
"key2": "a2",
"val": 1,
},
map[string]interface{}{
"key1": "b1",
"key2": "b2",
"val": 3,
},
}, multiKeyMapListSchema),
equal: false,
},
{
name: "set lists are equal regardless of order",
lhs: UnstructuredToVal([]interface{}{"a", "b"}, setListSchema),
rhs: UnstructuredToVal([]interface{}{"b", "a"}, setListSchema),
equal: true,
},
{
name: "set lists are not equal if contents differ",
lhs: UnstructuredToVal([]interface{}{"a", "b"}, setListSchema),
rhs: UnstructuredToVal([]interface{}{"a", "c"}, setListSchema),
equal: false,
},
{
name: "set lists are not equal if lengths differ",
lhs: UnstructuredToVal([]interface{}{"a", "b"}, setListSchema),
rhs: UnstructuredToVal([]interface{}{"a", "b", "c"}, setListSchema),
equal: false,
},
{
name: "identical atomic lists are equal",
lhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema),
rhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema),
equal: true,
},
{
name: "atomic lists are not equal if order differs",
lhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema),
rhs: UnstructuredToVal([]interface{}{"b", "a"}, atomicListSchema),
equal: false,
},
{
name: "atomic lists are not equal if contents differ",
lhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema),
rhs: UnstructuredToVal([]interface{}{"a", "c"}, atomicListSchema),
equal: false,
},
{
name: "atomic lists are not equal if lengths differ",
lhs: UnstructuredToVal([]interface{}{"a", "b"}, atomicListSchema),
rhs: UnstructuredToVal([]interface{}{"a", "b", "c"}, atomicListSchema),
equal: false,
},
{
name: "identical objects are equal",
lhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema),
rhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema),
equal: true,
},
{
name: "objects are equal regardless of field order",
lhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema),
rhs: UnstructuredToVal(map[string]interface{}{"field2": "b", "field1": "a"}, objectSchema),
equal: true,
},
{
name: "objects are not equal if contents differs",
lhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema),
rhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "c"}, objectSchema),
equal: false,
},
{
name: "objects are not equal if length differs",
lhs: UnstructuredToVal(map[string]interface{}{"field1": "a", "field2": "b"}, objectSchema),
rhs: UnstructuredToVal(map[string]interface{}{"field1": "a"}, objectSchema),
equal: false,
},
{
name: "identical maps are equal",
lhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema),
rhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema),
equal: true,
},
{
name: "maps are equal regardless of field order",
lhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema),
rhs: UnstructuredToVal(map[string]interface{}{"key2": "b", "key1": "a"}, mapSchema),
equal: true,
},
{
name: "maps are not equal if contents differs",
lhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema),
rhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "c"}, mapSchema),
equal: false,
},
{
name: "maps are not equal if length differs",
lhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b"}, mapSchema),
rhs: UnstructuredToVal(map[string]interface{}{"key1": "a", "key2": "b", "key3": "c"}, mapSchema),
equal: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
// Compare types with schema against themselves
if tc.lhs.Equal(tc.rhs) != types.Bool(tc.equal) {
t.Errorf("expected Equals to return %v", tc.equal)
}
if tc.rhs.Equal(tc.lhs) != types.Bool(tc.equal) {
t.Errorf("expected Equals to return %v", tc.equal)
}
// Compare types with schema against native types. This is slightly different than how
// CEL performs equality against data literals, but is a good sanity check.
if tc.lhs.Equal(types.DefaultTypeAdapter.NativeToValue(tc.rhs.Value())) != types.Bool(tc.equal) {
t.Errorf("expected unstructuredVal.Equals(<native type>) to return %v", tc.equal)
}
if tc.rhs.Equal(types.DefaultTypeAdapter.NativeToValue(tc.lhs.Value())) != types.Bool(tc.equal) {
t.Errorf("expected unstructuredVal.Equals(<native type>) to return %v", tc.equal)
}
})
}
}
func TestLister(t *testing.T) {
cases := []struct {
name string
unstructured []interface{}
schema *spec.Schema
itemSchema *spec.Schema
size int64
notContains []ref.Val
addition []interface{}
expectAdded []interface{}
}{
{
name: "map list",
unstructured: []interface{}{
map[string]interface{}{
"key": "a",
"val": 1,
},
map[string]interface{}{
"key": "b",
"val": 2,
},
},
schema: mapListSchema,
itemSchema: mapListElementSchema,
size: 2,
notContains: []ref.Val{
UnstructuredToVal(map[string]interface{}{
"key": "a",
"val": 2,
}, mapListElementSchema),
UnstructuredToVal(map[string]interface{}{
"key": "c",
"val": 1,
}, mapListElementSchema),
},
addition: []interface{}{
map[string]interface{}{
"key": "b",
"val": 3,
},
map[string]interface{}{
"key": "c",
"val": 4,
},
},
expectAdded: []interface{}{
map[string]interface{}{
"key": "a",
"val": 1,
},
map[string]interface{}{
"key": "b",
"val": 3,
},
map[string]interface{}{
"key": "c",
"val": 4,
},
},
},
{
name: "set list",
unstructured: []interface{}{"a", "b"},
schema: setListSchema,
itemSchema: stringSchema,
size: 2,
notContains: []ref.Val{UnstructuredToVal("c", stringSchema)},
addition: []interface{}{"b", "c"},
expectAdded: []interface{}{"a", "b", "c"},
},
{
name: "atomic list",
unstructured: []interface{}{"a", "b"},
schema: atomicListSchema,
itemSchema: stringSchema,
size: 2,
notContains: []ref.Val{UnstructuredToVal("c", stringSchema)},
addition: []interface{}{"b", "c"},
expectAdded: []interface{}{"a", "b", "b", "c"},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
lister := UnstructuredToVal(tc.unstructured, tc.schema).(traits.Lister)
if lister.Size().Value() != tc.size {
t.Errorf("Expected Size to return %d but got %d", tc.size, lister.Size().Value())
}
iter := lister.Iterator()
for i := 0; i < int(tc.size); i++ {
get := lister.Get(types.Int(i)).Value()
if !reflect.DeepEqual(get, tc.unstructured[i]) {
t.Errorf("Expected Get to return %v for index %d but got %v", tc.unstructured[i], i, get)
}
if iter.HasNext() != types.True {
t.Error("Expected HasNext to return true")
}
next := iter.Next().Value()
if !reflect.DeepEqual(next, tc.unstructured[i]) {
t.Errorf("Expected Next to return %v for index %d but got %v", tc.unstructured[i], i, next)
}
}
if iter.HasNext() != types.False {
t.Error("Expected HasNext to return false")
}
for _, contains := range tc.unstructured {
if lister.Contains(UnstructuredToVal(contains, tc.itemSchema)) != types.True {
t.Errorf("Expected Contains to return true for %v", contains)
}
}
for _, notContains := range tc.notContains {
if lister.Contains(notContains) != types.False {
t.Errorf("Expected Contains to return false for %v", notContains)
}
}
addition := UnstructuredToVal(tc.addition, tc.schema).(traits.Lister)
added := lister.Add(addition).Value()
if !reflect.DeepEqual(added, tc.expectAdded) {
t.Errorf("Expected Add to return %v but got %v", tc.expectAdded, added)
}
})
}
}
func TestMapper(t *testing.T) {
cases := []struct {
name string
unstructured map[string]interface{}
schema *spec.Schema
propertySchema func(key string) (*spec.Schema, bool)
size int64
notContains []ref.Val
}{
{
name: "object",
unstructured: map[string]interface{}{
"field1": "a",
"field2": "b",
},
schema: objectSchema,
propertySchema: func(key string) (*spec.Schema, bool) {
if s, ok := objectSchema.Properties[key]; ok {
return &s, true
}
return nil, false
},
size: 2,
notContains: []ref.Val{
UnstructuredToVal("field3", stringSchema),
},
},
{
name: "map",
unstructured: map[string]interface{}{
"key1": "a",
"key2": "b",
},
schema: mapSchema,
propertySchema: func(key string) (*spec.Schema, bool) { return mapSchema.AdditionalProperties.Schema, true },
size: 2,
notContains: []ref.Val{
UnstructuredToVal("key3", stringSchema),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
mapper := UnstructuredToVal(tc.unstructured, tc.schema).(traits.Mapper)
if mapper.Size().Value() != tc.size {
t.Errorf("Expected Size to return %d but got %d", tc.size, mapper.Size().Value())
}
iter := mapper.Iterator()
iterResults := map[interface{}]struct{}{}
keys := map[interface{}]struct{}{}
for k := range tc.unstructured {
keys[k] = struct{}{}
get := mapper.Get(types.String(k)).Value()
if !reflect.DeepEqual(get, tc.unstructured[k]) {
t.Errorf("Expected Get to return %v for key %s but got %v", tc.unstructured[k], k, get)
}
if iter.HasNext() != types.True {
t.Error("Expected HasNext to return true")
}
iterResults[iter.Next().Value()] = struct{}{}
}
if !reflect.DeepEqual(iterResults, keys) {
t.Errorf("Expected accumulation of iterator.Next calls to be %v but got %v", keys, iterResults)
}
if iter.HasNext() != types.False {
t.Error("Expected HasNext to return false")
}
for contains := range tc.unstructured {
if mapper.Contains(UnstructuredToVal(contains, stringSchema)) != types.True {
t.Errorf("Expected Contains to return true for %v", contains)
}
}
for _, notContains := range tc.notContains {
if mapper.Contains(notContains) != types.False {
t.Errorf("Expected Contains to return false for %v", notContains)
}
}
})
}
}
func BenchmarkUnstructuredToVal(b *testing.B) {
u := []interface{}{
map[string]interface{}{
"key": "a",
"val": 1,
},
map[string]interface{}{
"key": "b",
"val": 2,
},
map[string]interface{}{
"key": "@b",
"val": 2,
},
}
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
if val := UnstructuredToVal(u, mapListSchema); val == nil {
b.Fatal(val)
}
}
}
func BenchmarkUnstructuredToValWithEscape(b *testing.B) {
u := []interface{}{
map[string]interface{}{
"key": "a.1",
"val": "__i.1",
},
map[string]interface{}{
"key": "b.1",
"val": 2,
},
}
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
if val := UnstructuredToVal(u, mapListSchema); val == nil {
b.Fatal(val)
}
}
}

View File

@ -0,0 +1,516 @@
/*
Copyright 2022 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 cel
import (
"context"
"reflect"
"strings"
"testing"
"time"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/interpreter"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
nodev1 "k8s.io/api/node/v1"
storagev1 "k8s.io/api/storage/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/wait"
commoncel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library"
celopenapi "k8s.io/apiserver/pkg/cel/openapi"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
k8sscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/kube-openapi/pkg/validation/spec"
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
"k8s.io/kubernetes/pkg/generated/openapi"
"k8s.io/kubernetes/test/integration/framework"
"k8s.io/utils/pointer"
)
func TestTypeResolver(t *testing.T) {
server, err := apiservertesting.StartTestServer(t, nil, nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := extclientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
crd, err := installCRD(client)
if err != nil {
t.Fatal(err)
}
defer func(crd *apiextensionsv1.CustomResourceDefinition) {
err := client.ApiextensionsV1().CustomResourceDefinitions().Delete(context.Background(), crd.Name, metav1.DeleteOptions{})
if err != nil {
t.Fatal(err)
}
}(crd)
discoveryResolver := &resolver.ClientDiscoveryResolver{Discovery: client.Discovery()}
definitionsResolver := resolver.NewDefinitionsSchemaResolver(k8sscheme.Scheme, openapi.GetOpenAPIDefinitions)
// wait until the CRD schema is published at the OpenAPI v3 endpoint
err = wait.PollImmediate(time.Second, time.Minute, func() (done bool, err error) {
p, err := client.OpenAPIV3().Paths()
if err != nil {
return
}
if _, ok := p["apis/apis.example.com/v1beta1"]; ok {
return true, nil
}
return false, nil
})
if err != nil {
t.Fatalf("timeout wait for CRD schema publication: %v", err)
}
for _, tc := range []struct {
name string
obj runtime.Object
expression string
expectResolutionErr bool
expectCompileErr bool
expectEvalErr bool
expectedResult any
resolvers []resolver.SchemaResolver
}{
{
name: "unknown type",
obj: &unstructured.Unstructured{Object: map[string]any{
"kind": "Bad",
"apiVersion": "bad.example.com/v1",
}},
expectResolutionErr: true,
resolvers: []resolver.SchemaResolver{definitionsResolver, discoveryResolver},
},
{
name: "deployment",
obj: sampleReplicatedDeployment(),
expression: "self.spec.replicas > 1",
expectResolutionErr: false,
expectCompileErr: false,
expectEvalErr: false,
resolvers: []resolver.SchemaResolver{definitionsResolver, discoveryResolver},
// expect a boolean, which is `true`.
expectedResult: true,
},
{
name: "missing field",
obj: sampleReplicatedDeployment(),
expression: "self.spec.missing > 1",
expectResolutionErr: false,
expectCompileErr: true,
resolvers: []resolver.SchemaResolver{definitionsResolver, discoveryResolver},
},
{
name: "mistyped expression",
obj: sampleReplicatedDeployment(),
expression: "self.spec.replicas == '1'",
expectResolutionErr: false,
expectCompileErr: true,
resolvers: []resolver.SchemaResolver{definitionsResolver, discoveryResolver},
},
{
name: "crd valid",
obj: &unstructured.Unstructured{Object: map[string]any{
"kind": "CronTab",
"apiVersion": "apis.example.com/v1beta1",
"spec": map[string]any{
"cronSpec": "* * * * *",
"image": "foo-image",
"replicas": 2,
},
}},
expression: "self.spec.replicas > 1",
expectResolutionErr: false,
expectCompileErr: false,
expectEvalErr: false,
resolvers: []resolver.SchemaResolver{discoveryResolver},
// expect a boolean, which is `true`.
expectedResult: true,
},
{
name: "crd missing field",
obj: &unstructured.Unstructured{Object: map[string]any{
"kind": "CronTab",
"apiVersion": "apis.example.com/v1beta1",
"spec": map[string]any{
"cronSpec": "* * * * *",
"image": "foo-image",
"replicas": 2,
},
}},
expression: "self.spec.missing > 1",
expectResolutionErr: false,
expectCompileErr: true,
resolvers: []resolver.SchemaResolver{discoveryResolver},
},
{
name: "crd mistyped",
obj: &unstructured.Unstructured{Object: map[string]any{
"kind": "CronTab",
"apiVersion": "apis.example.com/v1beta1",
"spec": map[string]any{
"cronSpec": "* * * * *",
"image": "foo-image",
"replicas": 2,
},
}},
expression: "self.spec.replica == '1'",
expectResolutionErr: false,
expectCompileErr: true,
resolvers: []resolver.SchemaResolver{discoveryResolver},
},
{
name: "items population",
obj: sampleReplicatedDeployment(),
// `containers` is an array whose items are of `Container` type
// `ports` is an array of `ContainerPort`
expression: "size(self.spec.template.spec.containers) > 0 &&" +
"self.spec.template.spec.containers.all(c, c.ports.all(p, p.containerPort < 1024))",
expectResolutionErr: false,
expectCompileErr: false,
expectEvalErr: false,
expectedResult: true,
resolvers: []resolver.SchemaResolver{definitionsResolver, discoveryResolver},
},
{
name: "int-or-string int",
obj: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
Spec: appsv1.DeploymentSpec{
Strategy: appsv1.DeploymentStrategy{
Type: appsv1.RollingUpdateDeploymentStrategyType,
RollingUpdate: &appsv1.RollingUpdateDeployment{
MaxSurge: &intstr.IntOrString{Type: intstr.Int, IntVal: 5},
},
},
},
},
expression: "has(self.spec.strategy.rollingUpdate) &&" +
"type(self.spec.strategy.rollingUpdate.maxSurge) == int &&" +
"self.spec.strategy.rollingUpdate.maxSurge > 1",
expectResolutionErr: false,
expectCompileErr: false,
expectEvalErr: false,
expectedResult: true,
resolvers: []resolver.SchemaResolver{definitionsResolver, discoveryResolver},
},
{
name: "int-or-string string",
obj: &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
Spec: appsv1.DeploymentSpec{
Strategy: appsv1.DeploymentStrategy{
Type: appsv1.RollingUpdateDeploymentStrategyType,
RollingUpdate: &appsv1.RollingUpdateDeployment{
MaxSurge: &intstr.IntOrString{Type: intstr.String, StrVal: "10%"},
},
},
},
},
expression: "has(self.spec.strategy.rollingUpdate) &&" +
"type(self.spec.strategy.rollingUpdate.maxSurge) == string &&" +
"self.spec.strategy.rollingUpdate.maxSurge == '10%'",
expectResolutionErr: false,
expectCompileErr: false,
expectEvalErr: false,
expectedResult: true,
resolvers: []resolver.SchemaResolver{definitionsResolver, discoveryResolver},
},
} {
t.Run(tc.name, func(t *testing.T) {
gvk := tc.obj.GetObjectKind().GroupVersionKind()
var s *spec.Schema
for _, r := range tc.resolvers {
var err error
s, err = r.ResolveSchema(gvk)
if err != nil {
if tc.expectResolutionErr {
return
}
t.Fatalf("cannot resolve type: %v", err)
}
if tc.expectResolutionErr {
t.Fatalf("expected resulution error but got none")
}
}
program, err := simpleCompileCEL(s, tc.expression)
if err != nil {
if tc.expectCompileErr {
return
}
t.Fatalf("cannot eval: %v", err)
}
if tc.expectCompileErr {
t.Fatalf("expected compilation error but got none")
}
unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.obj)
if err != nil {
t.Fatal(err)
}
ret, _, err := program.Eval(&simpleActivation{self: celopenapi.UnstructuredToVal(unstructured, s)})
if err != nil {
if tc.expectEvalErr {
return
}
t.Fatalf("cannot eval: %v", err)
}
if tc.expectEvalErr {
t.Fatalf("expected eval error but got none")
}
if !reflect.DeepEqual(ret.Value(), tc.expectedResult) {
t.Errorf("wrong result, expected %q but got %q", tc.expectedResult, ret)
}
})
}
}
// TestBuiltinResolution asserts that all resolver implementations should
// resolve Kubernetes built-in types without error.
func TestBuiltinResolution(t *testing.T) {
// before all, setup server and client
server, err := apiservertesting.StartTestServer(t, nil, nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
client, err := extclientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
resolver resolver.SchemaResolver
scheme *runtime.Scheme
}{
{
name: "definitions",
resolver: resolver.NewDefinitionsSchemaResolver(k8sscheme.Scheme, openapi.GetOpenAPIDefinitions),
scheme: buildTestScheme(),
},
{
name: "discovery",
resolver: &resolver.ClientDiscoveryResolver{Discovery: client.Discovery()},
scheme: buildTestScheme(),
},
} {
t.Run(tc.name, func(t *testing.T) {
for gvk := range tc.scheme.AllKnownTypes() {
// skip aliases to metav1
if gvk.Kind == "APIGroup" || gvk.Kind == "APIGroupList" || gvk.Kind == "APIVersions" ||
strings.HasSuffix(gvk.Kind, "Options") || strings.HasSuffix(gvk.Kind, "Event") {
continue
}
// skip private, reference, and alias types that cannot appear in the wild
if gvk.Kind == "SerializedReference" || gvk.Kind == "List" || gvk.Kind == "RangeAllocation" || gvk.Kind == "PodStatusResult" {
continue
}
// skip internal types
if gvk.Version == "__internal" {
continue
}
_, err = tc.resolver.ResolveSchema(gvk)
if err != nil {
t.Errorf("resolver %q cannot resolve %v", tc.name, gvk)
}
}
})
}
}
// simpleCompileCEL compiles the CEL expression against the schema
// with the practical defaults.
// `self` is defined as the object being evaluated against.
func simpleCompileCEL(schema *spec.Schema, expression string) (cel.Program, error) {
var opts []cel.EnvOption
opts = append(opts, cel.HomogeneousAggregateLiterals())
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
opts = append(opts, library.ExtensionLibs...)
env, err := cel.NewEnv(opts...)
if err != nil {
return nil, err
}
reg := commoncel.NewRegistry(env)
declType := celopenapi.SchemaDeclType(schema, true)
rt, err := commoncel.NewRuleTypes("selfType", declType, reg)
if err != nil {
return nil, err
}
opts, err = rt.EnvOptions(env.TypeProvider())
if err != nil {
return nil, err
}
rootType, _ := rt.FindDeclType("selfType")
opts = append(opts, cel.Variable("self", rootType.CelType()))
env, err = env.Extend(opts...)
if err != nil {
return nil, err
}
ast, issues := env.Compile(expression)
if issues != nil {
return nil, issues.Err()
}
return env.Program(ast)
}
// sampleReplicatedDeployment returns a sample Deployment with 2 replicas.
// The object is not inlined because the schema of Deployment is well-known
// and thus requires no reference when reading the test cases.
func sampleReplicatedDeployment() *appsv1.Deployment {
return &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "demo-deployment",
},
Spec: appsv1.DeploymentSpec{
Replicas: pointer.Int32(2),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "demo",
},
},
Template: apiv1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "demo",
},
},
Spec: apiv1.PodSpec{
Containers: []apiv1.Container{
{
Name: "web",
Image: "nginx",
Ports: []apiv1.ContainerPort{
{
Name: "http",
Protocol: apiv1.ProtocolTCP,
ContainerPort: 80,
},
},
},
},
},
},
},
}
}
func installCRD(apiExtensionClient extclientset.Interface) (*apiextensionsv1.CustomResourceDefinition, error) {
// CRD borrowed from https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/
crd := &apiextensionsv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "crontabs.apis.example.com",
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "apis.example.com",
Scope: apiextensionsv1.NamespaceScoped,
Names: apiextensionsv1.CustomResourceDefinitionNames{
Plural: "crontabs",
Singular: "crontab",
Kind: "CronTab",
ListKind: "CronTabList",
},
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1beta1",
Served: true,
Storage: true,
Schema: &apiextensionsv1.CustomResourceValidation{
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
XPreserveUnknownFields: pointer.Bool(true),
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"spec": {
Type: "object",
Properties: map[string]apiextensionsv1.JSONSchemaProps{
"cronSpec": {Type: "string"},
"image": {Type: "string"},
"replicas": {Type: "integer"},
},
},
},
},
},
},
},
},
}
return apiExtensionClient.ApiextensionsV1().
CustomResourceDefinitions().Create(context.Background(), crd, metav1.CreateOptions{})
}
type simpleActivation struct {
self any
}
func (a *simpleActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case "self":
return a.self, true
default:
return nil, false
}
}
func (a *simpleActivation) Parent() interpreter.Activation {
return nil
}
func buildTestScheme() *runtime.Scheme {
// hand-picked schemes that the test API server serves
scheme := runtime.NewScheme()
_ = corev1.AddToScheme(scheme)
_ = appsv1.AddToScheme(scheme)
_ = admissionregistrationv1.AddToScheme(scheme)
_ = networkingv1.AddToScheme(scheme)
_ = nodev1.AddToScheme(scheme)
_ = storagev1.AddToScheme(scheme)
return scheme
}

3
vendor/modules.txt vendored
View File

@ -1511,8 +1511,11 @@ k8s.io/apiserver/pkg/authorization/authorizerfactory
k8s.io/apiserver/pkg/authorization/path k8s.io/apiserver/pkg/authorization/path
k8s.io/apiserver/pkg/authorization/union k8s.io/apiserver/pkg/authorization/union
k8s.io/apiserver/pkg/cel k8s.io/apiserver/pkg/cel
k8s.io/apiserver/pkg/cel/common
k8s.io/apiserver/pkg/cel/library k8s.io/apiserver/pkg/cel/library
k8s.io/apiserver/pkg/cel/metrics k8s.io/apiserver/pkg/cel/metrics
k8s.io/apiserver/pkg/cel/openapi
k8s.io/apiserver/pkg/cel/openapi/resolver
k8s.io/apiserver/pkg/endpoints k8s.io/apiserver/pkg/endpoints
k8s.io/apiserver/pkg/endpoints/deprecation k8s.io/apiserver/pkg/endpoints/deprecation
k8s.io/apiserver/pkg/endpoints/discovery k8s.io/apiserver/pkg/endpoints/discovery