Skip to content

Commit acf0ef4

Browse files
authored
Merge pull request kubernetes#113312 from jiahuif-forks/feature/cel/builtins
OpenAPI-based CEL type library
2 parents 3585ea7 + 8036764 commit acf0ef4

26 files changed

Lines changed: 3931 additions & 1035 deletions

File tree

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ require (
3939
github.com/golang/mock v1.6.0
4040
github.com/golang/protobuf v1.5.2
4141
github.com/google/cadvisor v0.47.1
42+
github.com/google/cel-go v0.12.6
4243
github.com/google/gnostic v0.5.7-v3refs
4344
github.com/google/go-cmp v0.5.9
4445
github.com/google/gofuzz v1.1.0
@@ -172,7 +173,6 @@ require (
172173
github.com/gofrs/uuid v4.0.0+incompatible // indirect
173174
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
174175
github.com/google/btree v1.0.1 // indirect
175-
github.com/google/cel-go v0.12.6 // indirect
176176
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
177177
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
178178
github.com/googleapis/gax-go/v2 v2.1.1 // indirect

‎staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import (
2727

2828
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2929
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
30-
celmodel "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
3130
apiservercel "k8s.io/apiserver/pkg/cel"
3231
"k8s.io/apiserver/pkg/cel/library"
3332
"k8s.io/apiserver/pkg/cel/metrics"
@@ -53,6 +52,10 @@ const (
5352
// checkFrequency configures the number of iterations within a comprehension to evaluate
5453
// before checking whether the function evaluation has been interrupted
5554
checkFrequency = 100
55+
56+
// maxRequestSizeBytes is the maximum size of a request to the API server
57+
// TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable
58+
maxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
5659
)
5760

5861
// CompilationResult represents the cel compilation result for one rule
@@ -149,7 +152,7 @@ func Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit
149152
estimator := newCostEstimator(root)
150153
// compResults is the return value which saves a list of compilation results in the same order as x-kubernetes-validations rules.
151154
compResults := make([]CompilationResult, len(celRules))
152-
maxCardinality := celmodel.MaxCardinality(root.MinSerializedSize)
155+
maxCardinality := maxCardinality(root.MinSerializedSize)
153156
for i, rule := range celRules {
154157
compResults[i] = compileRule(rule, env, perCallLimit, estimator, maxCardinality)
155158
}
@@ -262,3 +265,14 @@ func (c *sizeEstimator) EstimateSize(element checker.AstNode) *checker.SizeEstim
262265
func (c *sizeEstimator) EstimateCallCost(function, overloadID string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate {
263266
return nil
264267
}
268+
269+
// maxCardinality returns the maximum number of times data conforming to the minimum size given could possibly exist in
270+
// an object serialized to JSON. For cases where a schema is contained under map or array schemas of unbounded
271+
// size, this can be used as an estimate as the worst case number of times data matching the schema could be repeated.
272+
// Note that this only assumes a single comma between data elements, so if the schema is contained under only maps,
273+
// this estimates a higher cardinality that would be possible. DeclType.MinSerializedSize is meant to be passed to
274+
// this function.
275+
func maxCardinality(minSize int64) uint64 {
276+
sz := minSize + 1 // assume at least one comma between elements
277+
return uint64(maxRequestSizeBytes / sz)
278+
}

‎staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/maplist.go‎

Lines changed: 4 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -17,162 +17,14 @@ limitations under the License.
1717
package cel
1818

1919
import (
20-
"fmt"
21-
"strings"
22-
2320
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
21+
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
22+
"k8s.io/apiserver/pkg/cel/common"
2423
)
2524

26-
// mapList provides a "lookup by key" operation for lists (arrays) with x-kubernetes-list-type=map.
27-
type mapList interface {
28-
// get returns the first element having given key, for all
29-
// x-kubernetes-list-map-keys, to the provided object. If the provided object isn't itself a valid mapList element,
30-
// get returns nil.
31-
get(interface{}) interface{}
32-
}
33-
34-
type keyStrategy interface {
35-
// CompositeKeyFor returns a composite key for the provided object, if possible, and a
36-
// boolean that indicates whether or not a key could be generated for the provided object.
37-
CompositeKeyFor(map[string]interface{}) (interface{}, bool)
38-
}
39-
40-
// singleKeyStrategy is a cheaper strategy for associative lists that have exactly one key.
41-
type singleKeyStrategy struct {
42-
key string
43-
}
44-
45-
// CompositeKeyFor directly returns the value of the single key to
46-
// use as a composite key.
47-
func (ks *singleKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
48-
v, ok := obj[ks.key]
49-
if !ok {
50-
return nil, false
51-
}
52-
53-
switch v.(type) {
54-
case bool, float64, int64, string:
55-
return v, true
56-
default:
57-
return nil, false // non-scalar
58-
}
59-
}
60-
61-
// multiKeyStrategy computes a composite key of all key values.
62-
type multiKeyStrategy struct {
63-
sts *schema.Structural
64-
}
65-
66-
// CompositeKeyFor returns a composite key computed from the values of all
67-
// keys.
68-
func (ks *multiKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interface{}, bool) {
69-
const keyDelimiter = "\x00" // 0 byte should never appear in the composite key except as delimiter
70-
71-
var delimited strings.Builder
72-
for _, key := range ks.sts.XListMapKeys {
73-
v, ok := obj[key]
74-
if !ok {
75-
return nil, false
76-
}
77-
78-
switch v.(type) {
79-
case bool:
80-
fmt.Fprintf(&delimited, keyDelimiter+"%t", v)
81-
case float64:
82-
fmt.Fprintf(&delimited, keyDelimiter+"%f", v)
83-
case int64:
84-
fmt.Fprintf(&delimited, keyDelimiter+"%d", v)
85-
case string:
86-
fmt.Fprintf(&delimited, keyDelimiter+"%q", v)
87-
default:
88-
return nil, false // values must be scalars
89-
}
90-
}
91-
return delimited.String(), true
92-
}
93-
94-
// emptyMapList is a mapList containing no elements.
95-
type emptyMapList struct{}
96-
97-
func (emptyMapList) get(interface{}) interface{} {
98-
return nil
99-
}
100-
101-
type mapListImpl struct {
102-
sts *schema.Structural
103-
ks keyStrategy
104-
// keyedItems contains all lazily keyed map items
105-
keyedItems map[interface{}]interface{}
106-
// unkeyedItems contains all map items that have not yet been keyed
107-
unkeyedItems []interface{}
108-
}
109-
110-
func (a *mapListImpl) get(obj interface{}) interface{} {
111-
mobj, ok := obj.(map[string]interface{})
112-
if !ok {
113-
return nil
114-
}
115-
116-
key, ok := a.ks.CompositeKeyFor(mobj)
117-
if !ok {
118-
return nil
119-
}
120-
if match, ok := a.keyedItems[key]; ok {
121-
return match
122-
}
123-
// keep keying items until we either find a match or run out of unkeyed items
124-
for len(a.unkeyedItems) > 0 {
125-
// dequeue an unkeyed item
126-
item := a.unkeyedItems[0]
127-
a.unkeyedItems = a.unkeyedItems[1:]
128-
129-
// key the item
130-
mitem, ok := item.(map[string]interface{})
131-
if !ok {
132-
continue
133-
}
134-
itemKey, ok := a.ks.CompositeKeyFor(mitem)
135-
if !ok {
136-
continue
137-
}
138-
if _, exists := a.keyedItems[itemKey]; !exists {
139-
a.keyedItems[itemKey] = mitem
140-
}
141-
142-
// if it matches, short-circuit
143-
if itemKey == key {
144-
return mitem
145-
}
146-
}
147-
148-
return nil
149-
}
150-
151-
func makeKeyStrategy(sts *schema.Structural) keyStrategy {
152-
if len(sts.XListMapKeys) == 1 {
153-
key := sts.XListMapKeys[0]
154-
return &singleKeyStrategy{
155-
key: key,
156-
}
157-
}
158-
159-
return &multiKeyStrategy{
160-
sts: sts,
161-
}
162-
}
163-
16425
// makeMapList returns a queryable interface over the provided x-kubernetes-list-type=map
16526
// keyedItems. If the provided schema is _not_ an array with x-kubernetes-list-type=map, returns an
16627
// empty mapList.
167-
func makeMapList(sts *schema.Structural, items []interface{}) (rv mapList) {
168-
if sts.Type != "array" || sts.XListType == nil || *sts.XListType != "map" || len(sts.XListMapKeys) == 0 || len(items) == 0 {
169-
return emptyMapList{}
170-
}
171-
ks := makeKeyStrategy(sts)
172-
return &mapListImpl{
173-
sts: sts,
174-
ks: ks,
175-
keyedItems: map[interface{}]interface{}{},
176-
unkeyedItems: items,
177-
}
28+
func makeMapList(sts *schema.Structural, items []interface{}) (rv common.MapList) {
29+
return common.MakeMapList(&model.Structural{Structural: sts}, items)
17830
}

‎staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/maplist_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,9 @@ func TestMapList(t *testing.T) {
323323
t.Run(tc.name, func(t *testing.T) {
324324
mapList := makeMapList(&tc.sts, tc.items)
325325
for _, warmUp := range tc.warmUpQueries {
326-
mapList.get(warmUp)
326+
mapList.Get(warmUp)
327327
}
328-
actual := mapList.get(tc.query)
328+
actual := mapList.Get(tc.query)
329329
if !reflect.DeepEqual(tc.expected, actual) {
330330
t.Errorf("got: %v, expected %v", actual, tc.expected)
331331
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package model
18+
19+
import (
20+
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
21+
"k8s.io/apiserver/pkg/cel/common"
22+
)
23+
24+
var _ common.Schema = (*Structural)(nil)
25+
var _ common.SchemaOrBool = (*StructuralOrBool)(nil)
26+
27+
type Structural struct {
28+
Structural *schema.Structural
29+
}
30+
31+
type StructuralOrBool struct {
32+
StructuralOrBool *schema.StructuralOrBool
33+
}
34+
35+
func (sb *StructuralOrBool) Schema() common.Schema {
36+
if sb.StructuralOrBool.Structural == nil {
37+
return nil
38+
}
39+
return &Structural{Structural: sb.StructuralOrBool.Structural}
40+
}
41+
42+
func (sb *StructuralOrBool) Allows() bool {
43+
return sb.StructuralOrBool.Bool
44+
}
45+
46+
func (s *Structural) Type() string {
47+
return s.Structural.Type
48+
}
49+
50+
func (s *Structural) Format() string {
51+
if s.Structural.ValueValidation == nil {
52+
return ""
53+
}
54+
return s.Structural.ValueValidation.Format
55+
}
56+
57+
func (s *Structural) Items() common.Schema {
58+
return &Structural{Structural: s.Structural.Items}
59+
}
60+
61+
func (s *Structural) Properties() map[string]common.Schema {
62+
if s.Structural.Properties == nil {
63+
return nil
64+
}
65+
res := make(map[string]common.Schema, len(s.Structural.Properties))
66+
for n, prop := range s.Structural.Properties {
67+
s := prop
68+
res[n] = &Structural{Structural: &s}
69+
}
70+
return res
71+
}
72+
73+
func (s *Structural) AdditionalProperties() common.SchemaOrBool {
74+
if s.Structural.AdditionalProperties == nil {
75+
return nil
76+
}
77+
return &StructuralOrBool{StructuralOrBool: s.Structural.AdditionalProperties}
78+
}
79+
80+
func (s *Structural) Default() any {
81+
return s.Structural.Default.Object
82+
}
83+
84+
func (s *Structural) MaxItems() *int64 {
85+
if s.Structural.ValueValidation == nil {
86+
return nil
87+
}
88+
return s.Structural.ValueValidation.MaxItems
89+
}
90+
91+
func (s *Structural) MaxLength() *int64 {
92+
if s.Structural.ValueValidation == nil {
93+
return nil
94+
}
95+
return s.Structural.ValueValidation.MaxLength
96+
}
97+
98+
func (s *Structural) MaxProperties() *int64 {
99+
if s.Structural.ValueValidation == nil {
100+
return nil
101+
}
102+
return s.Structural.ValueValidation.MaxProperties
103+
}
104+
105+
func (s *Structural) Required() []string {
106+
if s.Structural.ValueValidation == nil {
107+
return nil
108+
}
109+
return s.Structural.ValueValidation.Required
110+
}
111+
112+
func (s *Structural) Enum() []any {
113+
if s.Structural.ValueValidation == nil {
114+
return nil
115+
}
116+
ret := make([]any, 0, len(s.Structural.ValueValidation.Enum))
117+
for _, e := range s.Structural.ValueValidation.Enum {
118+
ret = append(ret, e.Object)
119+
}
120+
return ret
121+
}
122+
123+
func (s *Structural) Nullable() bool {
124+
return s.Structural.Nullable
125+
}
126+
127+
func (s *Structural) IsXIntOrString() bool {
128+
return s.Structural.XIntOrString
129+
}
130+
131+
func (s *Structural) IsXEmbeddedResource() bool {
132+
return s.Structural.XEmbeddedResource
133+
}
134+
135+
func (s *Structural) IsXPreserveUnknownFields() bool {
136+
return s.Structural.XPreserveUnknownFields
137+
}
138+
139+
func (s *Structural) XListType() string {
140+
if s.Structural.XListType == nil {
141+
return ""
142+
}
143+
return *s.Structural.XListType
144+
}
145+
146+
func (s *Structural) XListMapKeys() []string {
147+
return s.Structural.XListMapKeys
148+
}
149+
150+
func (s *Structural) WithTypeAndObjectMeta() common.Schema {
151+
return &Structural{Structural: WithTypeAndObjectMeta(s.Structural)}
152+
}

0 commit comments

Comments
 (0)