Merge pull request #113312 from jiahuif-forks/feature/cel/builtins
OpenAPI-based CEL type library
This commit is contained in:
commit
70b2e4aa3e
2
go.mod
2
go.mod
@ -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
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
4
staging/src/k8s.io/apiserver/go.sum
generated
4
staging/src/k8s.io/apiserver/go.sum
generated
@ -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=
|
||||||
|
11
staging/src/k8s.io/apiserver/pkg/cel/OWNERS
Normal file
11
staging/src/k8s.io/apiserver/pkg/cel/OWNERS
Normal 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
|
81
staging/src/k8s.io/apiserver/pkg/cel/common/adaptor.go
Normal file
81
staging/src/k8s.io/apiserver/pkg/cel/common/adaptor.go
Normal 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
|
||||||
|
}
|
177
staging/src/k8s.io/apiserver/pkg/cel/common/maplist.go
Normal file
177
staging/src/k8s.io/apiserver/pkg/cel/common/maplist.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
257
staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go
Normal file
257
staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go
Normal 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
|
||||||
|
}
|
701
staging/src/k8s.io/apiserver/pkg/cel/common/values.go
Normal file
701
staging/src/k8s.io/apiserver/pkg/cel/common/values.go
Normal 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
|
||||||
|
}
|
147
staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go
Normal file
147
staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go
Normal 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)
|
||||||
|
}
|
62
staging/src/k8s.io/apiserver/pkg/cel/openapi/extensions.go
Normal file
62
staging/src/k8s.io/apiserver/pkg/cel/openapi/extensions.go
Normal 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"
|
309
staging/src/k8s.io/apiserver/pkg/cel/openapi/maplist_test.go
Normal file
309
staging/src/k8s.io/apiserver/pkg/cel/openapi/maplist_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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"
|
100
staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/refs.go
Normal file
100
staging/src/k8s.io/apiserver/pkg/cel/openapi/resolver/refs.go
Normal 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
|
||||||
|
}
|
@ -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")
|
463
staging/src/k8s.io/apiserver/pkg/cel/openapi/schemas_test.go
Normal file
463
staging/src/k8s.io/apiserver/pkg/cel/openapi/schemas_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
660
staging/src/k8s.io/apiserver/pkg/cel/openapi/values_test.go
Normal file
660
staging/src/k8s.io/apiserver/pkg/cel/openapi/values_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
516
test/integration/apiserver/cel/typeresolution_test.go
Normal file
516
test/integration/apiserver/cel/typeresolution_test.go
Normal 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
3
vendor/modules.txt
vendored
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user