move CEL package to apiserver package.

only anything that does not require Structural
This commit is contained in:
Jiahui Feng 2022-10-07 15:02:47 -07:00
parent 575031b68f
commit 0dd316a5c1
30 changed files with 258 additions and 193 deletions

View File

@ -25,6 +25,7 @@ import (
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/cel"
)
// unbounded uses nil to represent an unbounded cardinality value.
@ -109,7 +110,7 @@ type CELTypeInfo struct {
// Schema is a structural schema for this CELSchemaContext node. It must be non-nil.
Schema *structuralschema.Structural
// DeclType is a CEL declaration representation of Schema of this CELSchemaContext node. It must be non-nil.
DeclType *model.DeclType
DeclType *cel.DeclType
}
// converter converts from JSON schema to a structural schema and a CEL declType, or returns an error if the conversion
@ -224,7 +225,7 @@ type propertyTypeInfoAccessor struct {
func (c propertyTypeInfoAccessor) accessTypeInfo(parentTypeInfo *CELTypeInfo) *CELTypeInfo {
if parentTypeInfo.Schema.Properties != nil {
propSchema := parentTypeInfo.Schema.Properties[c.propertyName]
if escapedPropName, ok := model.Escape(c.propertyName); ok {
if escapedPropName, ok := cel.Escape(c.propertyName); ok {
if fieldDeclType, ok := parentTypeInfo.DeclType.Fields[escapedPropName]; ok {
return &CELTypeInfo{Schema: &propSchema, DeclType: fieldDeclType.Type}
} // else fields with unknown types are omitted from CEL validation entirely

View File

@ -34,6 +34,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
@ -1038,7 +1039,7 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
celContext.TotalCost.ObserveExpressionCost(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), expressionCost)
}
if cr.Error != nil {
if cr.Error.Type == cel.ErrorTypeRequired {
if cr.Error.Type == apiservercel.ErrorTypeRequired {
allErrs.CELErrors = append(allErrs.CELErrors, field.Required(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), cr.Error.Detail))
} else {
allErrs.CELErrors = append(allErrs.CELErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("rule"), schema.XValidations[i], cr.Error.Detail))

View File

@ -24,11 +24,13 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/library"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics"
celmodel "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/metrics"
)
const (
@ -56,7 +58,7 @@ const (
// CompilationResult represents the cel compilation result for one rule
type CompilationResult struct {
Program cel.Program
Error *Error
Error *apiservercel.Error
// If true, the compiled expression contains a reference to the identifier "oldSelf", and its corresponding rule
// is implicitly a transition rule.
TransitionRule bool
@ -97,7 +99,7 @@ func getBaseEnv() (*cel.Env, error) {
// - nil Program, nil Error: The provided rule was empty so compilation was not attempted
//
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit as input.
func Compile(s *schema.Structural, declType *celmodel.DeclType, perCallLimit uint64) ([]CompilationResult, error) {
func Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit uint64) ([]CompilationResult, error) {
t := time.Now()
defer metrics.Metrics.ObserveCompilation(time.Since(t))
if len(s.Extensions.XValidations) == 0 {
@ -106,15 +108,15 @@ func Compile(s *schema.Structural, declType *celmodel.DeclType, perCallLimit uin
celRules := s.Extensions.XValidations
var propDecls []cel.EnvOption
var root *celmodel.DeclType
var root *apiservercel.DeclType
var ok bool
baseEnv, err := getBaseEnv()
if err != nil {
return nil, err
}
reg := celmodel.NewRegistry(baseEnv)
reg := apiservercel.NewRegistry(baseEnv)
scopedTypeName := generateUniqueSelfTypeName()
rt, err := celmodel.NewRuleTypes(scopedTypeName, declType, reg)
rt, err := apiservercel.NewRuleTypes(scopedTypeName, declType, reg)
if err != nil {
return nil, err
}
@ -158,18 +160,18 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u
}
ast, issues := env.Compile(rule.Rule)
if issues != nil {
compilationResult.Error = &Error{ErrorTypeInvalid, "compilation failed: " + issues.String()}
compilationResult.Error = &apiservercel.Error{apiservercel.ErrorTypeInvalid, "compilation failed: " + issues.String()}
return
}
if ast.OutputType() != cel.BoolType {
compilationResult.Error = &Error{ErrorTypeInvalid, "cel expression must evaluate to a bool"}
compilationResult.Error = &apiservercel.Error{apiservercel.ErrorTypeInvalid, "cel expression must evaluate to a bool"}
return
}
checkedExpr, err := cel.AstToCheckedExpr(ast)
if err != nil {
// should be impossible since env.Compile returned no issues
compilationResult.Error = &Error{ErrorTypeInternal, "unexpected compilation error: " + err.Error()}
compilationResult.Error = &apiservercel.Error{apiservercel.ErrorTypeInternal, "unexpected compilation error: " + err.Error()}
return
}
for _, ref := range checkedExpr.ReferenceMap {
@ -188,12 +190,12 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u
cel.InterruptCheckFrequency(checkFrequency),
)
if err != nil {
compilationResult.Error = &Error{ErrorTypeInvalid, "program instantiation failed: " + err.Error()}
compilationResult.Error = &apiservercel.Error{apiservercel.ErrorTypeInvalid, "program instantiation failed: " + err.Error()}
return
}
costEst, err := env.EstimateCost(ast, estimator)
if err != nil {
compilationResult.Error = &Error{ErrorTypeInternal, "cost estimation failed: " + err.Error()}
compilationResult.Error = &apiservercel.Error{apiservercel.ErrorTypeInternal, "cost estimation failed: " + err.Error()}
return
}
compilationResult.MaxCost = costEst.Max
@ -210,12 +212,12 @@ func generateUniqueSelfTypeName() string {
return fmt.Sprintf("selfType%d", time.Now().Nanosecond())
}
func newCostEstimator(root *celmodel.DeclType) *library.CostEstimator {
func newCostEstimator(root *apiservercel.DeclType) *library.CostEstimator {
return &library.CostEstimator{SizeEstimator: &sizeEstimator{root: root}}
}
type sizeEstimator struct {
root *celmodel.DeclType
root *apiservercel.DeclType
}
func (c *sizeEstimator) EstimateSize(element checker.AstNode) *checker.SizeEstimate {

View File

@ -22,6 +22,8 @@ import (
"strings"
"testing"
"k8s.io/apiserver/pkg/cel"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
@ -79,12 +81,12 @@ func (m fnMatcher) String() string {
}
type errorMatcher struct {
errorType ErrorType
errorType cel.ErrorType
contains string
}
func invalidError(contains string) validationMatcher {
return errorMatcher{errorType: ErrorTypeInvalid, contains: contains}
return errorMatcher{errorType: cel.ErrorTypeInvalid, contains: contains}
}
func (v errorMatcher) matches(cr CompilationResult) bool {

View File

@ -1,4 +0,0 @@
This directory contains a port of https://github.com/google/cel-policy-templates-go/tree/master/policy/model modified in a few ways:
- Uses the Structural schema types
- All template related code has been removed

View File

@ -22,37 +22,13 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
const (
// the largest request that will be accepted is 3MB
// TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable
maxRequestSizeBytes = int64(3 * 1024 * 1024)
// OpenAPI duration strings follow RFC 3339, section 5.6 - see the comment on maxDatetimeSizeJSON
maxDurationSizeJSON = 32
// OpenAPI datetime strings follow RFC 3339, section 5.6, and the longest possible
// such string is 9999-12-31T23:59:59.999999999Z, which has length 30 - we add 2
// to allow for quotation marks
maxDatetimeSizeJSON = 32
// Golang allows a string of 0 to be parsed as a duration, so that plus 2 to account for
// quotation marks makes 3
minDurationSizeJSON = 3
// RFC 3339 dates require YYYY-MM-DD, and then we add 2 to allow for quotation marks
dateSizeJSON = 12
// RFC 3339 datetimes require a full date (YYYY-MM-DD) and full time (HH:MM:SS), and we add 3 for
// quotation marks like always in addition to the capital T that separates the date and time
minDatetimeSizeJSON = 21
// ""
minStringSize = 2
// true
minBoolSize = 4
// 0
minNumberSize = 1
)
// SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the
// 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.
//
// Schemas with XPreserveUnknownFields not exposed unless they are objects. Array and "maps" schemas
@ -60,7 +36,7 @@ const (
// if their schema is not exposed.
//
// The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *apiservercel.DeclType {
if s == nil {
return nil
}
@ -77,9 +53,9 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
// To validate requirements on both the int and string representation:
// `type(intOrStringField) == int ? intOrStringField < 5 : double(intOrStringField.replace('%', '')) < 0.5
//
dyn := newSimpleTypeWithMinSize("dyn", cel.DynType, nil, 1) // smallest value for a serialied x-kubernetes-int-or-string is 0
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
dyn.MaxElements = apiservercel.MaxRequestSizeBytes - 2
return dyn
}
@ -106,7 +82,7 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
} else {
maxItems = estimateMaxArrayItemsFromMinSize(itemsType.MinSerializedSize)
}
return NewListType(itemsType, maxItems)
return apiservercel.NewListType(itemsType, maxItems)
}
return nil
case "object":
@ -119,11 +95,11 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
} else {
maxProperties = estimateMaxAdditionalPropertiesFromMinSize(propsType.MinSerializedSize)
}
return NewMapType(StringType, propsType, maxProperties)
return apiservercel.NewMapType(apiservercel.StringType, propsType, maxProperties)
}
return nil
}
fields := make(map[string]*DeclField, len(s.Properties))
fields := make(map[string]*apiservercel.DeclField, len(s.Properties))
required := map[string]bool{}
if s.ValueValidation != nil {
@ -141,14 +117,8 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
}
}
if fieldType := SchemaDeclType(&prop, prop.XEmbeddedResource); fieldType != nil {
if propName, ok := Escape(name); ok {
fields[propName] = &DeclField{
Name: propName,
Required: required[name],
Type: fieldType,
defaultValue: prop.Default.Object,
enumValues: enumValues, // Enum values are represented as strings in CEL
}
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
@ -159,14 +129,14 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
}
}
}
objType := NewObjectType("object", fields)
objType := apiservercel.NewObjectType("object", fields)
objType.MinSerializedSize = minSerializedSize
return objType
case "string":
if s.ValueValidation != nil {
switch s.ValueValidation.Format {
case "byte":
byteWithMaxLength := newSimpleTypeWithMinSize("bytes", cel.BytesType, types.Bytes([]byte{}), minStringSize)
byteWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("bytes", cel.BytesType, types.Bytes([]byte{}), apiservercel.MinStringSize)
if s.ValueValidation.MaxLength != nil {
byteWithMaxLength.MaxElements = zeroIfNegative(*s.ValueValidation.MaxLength)
} else {
@ -174,20 +144,20 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
}
return byteWithMaxLength
case "duration":
durationWithMaxLength := newSimpleTypeWithMinSize("duration", cel.DurationType, types.Duration{Duration: time.Duration(0)}, int64(minDurationSizeJSON))
durationWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("duration", cel.DurationType, types.Duration{Duration: time.Duration(0)}, int64(apiservercel.MinDurationSizeJSON))
durationWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return durationWithMaxLength
case "date":
timestampWithMaxLength := newSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(dateSizeJSON))
timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.JSONDateSize))
timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return timestampWithMaxLength
case "date-time":
timestampWithMaxLength := newSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(minDatetimeSizeJSON))
timestampWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, int64(apiservercel.MinDatetimeSizeJSON))
timestampWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
return timestampWithMaxLength
}
}
strWithMaxLength := newSimpleTypeWithMinSize("string", cel.StringType, types.String(""), minStringSize)
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,
@ -199,11 +169,11 @@ func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
}
return strWithMaxLength
case "boolean":
return BoolType
return apiservercel.BoolType
case "number":
return DoubleType
return apiservercel.DoubleType
case "integer":
return IntType
return apiservercel.IntType
}
return nil
}
@ -259,7 +229,7 @@ func WithTypeAndObjectMeta(s *schema.Structural) *schema.Structural {
// this function.
func MaxCardinality(minSize int64) uint64 {
sz := minSize + 1 // assume at least one comma between elements
return uint64(maxRequestSizeBytes / sz)
return uint64(apiservercel.MaxRequestSizeBytes / sz)
}
// estimateMaxStringLengthPerRequest estimates the maximum string length (in characters)
@ -268,18 +238,18 @@ func MaxCardinality(minSize int64) uint64 {
func estimateMaxStringLengthPerRequest(s *schema.Structural) int64 {
if s.ValueValidation == nil || s.XIntOrString {
// subtract 2 to account for ""
return (maxRequestSizeBytes - 2)
return apiservercel.MaxRequestSizeBytes - 2
}
switch s.ValueValidation.Format {
case "duration":
return maxDurationSizeJSON
return apiservercel.MaxDurationSizeJSON
case "date":
return dateSizeJSON
return apiservercel.JSONDateSize
case "date-time":
return maxDatetimeSizeJSON
return apiservercel.MaxDatetimeSizeJSON
default:
// subtract 2 to account for ""
return (maxRequestSizeBytes - 2)
return apiservercel.MaxRequestSizeBytes - 2
}
}
@ -287,7 +257,7 @@ func estimateMaxStringLengthPerRequest(s *schema.Structural) int64 {
// 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)
return (apiservercel.MaxRequestSizeBytes - 2) / (minSize + 1)
}
// estimateMaxAdditionalPropertiesPerRequest estimates the maximum number of additional properties
@ -297,5 +267,5 @@ func estimateMaxAdditionalPropertiesFromMinSize(minSize int64) int64 {
// will all vary in length
keyValuePairSize := minSize + 6
// subtract 2 to account for { and }
return (maxRequestSizeBytes - 2) / keyValuePairSize
return (apiservercel.MaxRequestSizeBytes - 2) / keyValuePairSize
}

View File

@ -25,6 +25,7 @@ import (
"google.golang.org/protobuf/proto"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
apiservercel "k8s.io/apiserver/pkg/cel"
)
func TestSchemaDeclType(t *testing.T) {
@ -86,15 +87,15 @@ func TestSchemaDeclType(t *testing.T) {
func TestSchemaDeclTypes(t *testing.T) {
ts := testSchema()
cust := SchemaDeclType(ts, true).MaybeAssignTypeName("CustomObject")
typeMap := FieldTypeMap("CustomObject", cust)
typeMap := apiservercel.FieldTypeMap("CustomObject", cust)
nested, _ := cust.FindField("nested")
metadata, _ := cust.FindField("metadata")
expectedObjTypeMap := map[string]*DeclType{
expectedObjTypeMap := map[string]*apiservercel.DeclType{
"CustomObject": cust,
"CustomObject.nested": nested.Type,
"CustomObject.metadata": metadata.Type,
}
objTypeMap := map[string]*DeclType{}
objTypeMap := map[string]*apiservercel.DeclType{}
for name, t := range typeMap {
if t.IsObject() {
objTypeMap[name] = t
@ -406,7 +407,7 @@ func TestEstimateMaxLengthJSON(t *testing.T) {
},
},
// should be exactly equal to maxDurationSizeJSON
ExpectedMaxElements: maxDurationSizeJSON,
ExpectedMaxElements: apiservercel.MaxDurationSizeJSON,
},
{
Name: "dateSize",
@ -419,7 +420,7 @@ func TestEstimateMaxLengthJSON(t *testing.T) {
},
},
// should be exactly equal to dateSizeJSON
ExpectedMaxElements: dateSizeJSON,
ExpectedMaxElements: apiservercel.JSONDateSize,
},
{
Name: "maxdatetimeSize",
@ -432,7 +433,7 @@ func TestEstimateMaxLengthJSON(t *testing.T) {
},
},
// should be exactly equal to maxDatetimeSizeJSON
ExpectedMaxElements: maxDatetimeSizeJSON,
ExpectedMaxElements: apiservercel.MaxDatetimeSizeJSON,
},
{
Name: "maxintOrStringSize",
@ -442,7 +443,7 @@ func TestEstimateMaxLengthJSON(t *testing.T) {
},
},
// should be exactly equal to maxRequestSizeBytes - 2 (to allow for quotes in the case of a string)
ExpectedMaxElements: maxRequestSizeBytes - 2,
ExpectedMaxElements: apiservercel.MaxRequestSizeBytes - 2,
},
{
Name: "objectDefaultFieldArray",
@ -495,7 +496,7 @@ func TestEstimateMaxLengthJSON(t *testing.T) {
},
},
// note that unlike regular strings we don't have to take unicode into account,
// so we we expect the max length to be exactly equal to the user-supplied one
// so we expect the max length to be exactly equal to the user-supplied one
ExpectedMaxElements: 20,
},
}

View File

@ -23,61 +23,14 @@ import (
"github.com/google/cel-go/common/types"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
apiservercel "k8s.io/apiserver/pkg/cel"
)
func TestTypes_ListType(t *testing.T) {
list := NewListType(StringType, -1)
if !list.IsList() {
t.Error("list type not identifiable as list")
}
if list.TypeName() != "list" {
t.Errorf("got %s, wanted list", list.TypeName())
}
if list.DefaultValue() == nil {
t.Error("got nil zero value for list type")
}
if list.ElemType.TypeName() != "string" {
t.Errorf("got %s, wanted elem type of string", list.ElemType.TypeName())
}
expT, err := list.ExprType()
if err != nil {
t.Errorf("fail to get cel type: %s", err)
}
if expT.GetListType() == nil {
t.Errorf("got %v, wanted CEL list type", expT)
}
}
func TestTypes_MapType(t *testing.T) {
mp := NewMapType(StringType, IntType, -1)
if !mp.IsMap() {
t.Error("map type not identifiable as map")
}
if mp.TypeName() != "map" {
t.Errorf("got %s, wanted map", mp.TypeName())
}
if mp.DefaultValue() == nil {
t.Error("got nil zero value for map type")
}
if mp.KeyType.TypeName() != "string" {
t.Errorf("got %s, wanted key type of string", mp.KeyType.TypeName())
}
if mp.ElemType.TypeName() != "int" {
t.Errorf("got %s, wanted elem type of int", mp.ElemType.TypeName())
}
expT, err := mp.ExprType()
if err != nil {
t.Errorf("fail to get cel type: %s", err)
}
if expT.GetMapType() == nil {
t.Errorf("got %v, wanted CEL map type", expT)
}
}
func TestTypes_RuleTypesFieldMapping(t *testing.T) {
stdEnv, _ := cel.NewEnv()
reg := NewRegistry(stdEnv)
rt, err := NewRuleTypes("CustomObject", SchemaDeclType(testSchema(), true), reg)
reg := apiservercel.NewRegistry(stdEnv)
rt, err := apiservercel.NewRuleTypes("CustomObject", SchemaDeclType(testSchema(), true), reg)
if err != nil {
t.Fatal(err)
}
@ -112,22 +65,22 @@ func TestTypes_RuleTypesFieldMapping(t *testing.T) {
}
// Manually constructed instance of the schema.
name := NewField(1, "name")
name := apiservercel.NewField(1, "name")
name.Ref = testValue(t, 2, "test-instance")
nestedVal := NewMapValue()
flags := NewField(5, "flags")
flagsVal := NewMapValue()
myFlag := NewField(6, "my_flag")
nestedVal := apiservercel.NewMapValue()
flags := apiservercel.NewField(5, "flags")
flagsVal := apiservercel.NewMapValue()
myFlag := apiservercel.NewField(6, "my_flag")
myFlag.Ref = testValue(t, 7, true)
flagsVal.AddField(myFlag)
flags.Ref = testValue(t, 8, flagsVal)
dates := NewField(9, "dates")
dates.Ref = testValue(t, 10, NewListValue())
dates := apiservercel.NewField(9, "dates")
dates.Ref = testValue(t, 10, apiservercel.NewListValue())
nestedVal.AddField(flags)
nestedVal.AddField(dates)
nested := NewField(3, "nested")
nested := apiservercel.NewField(3, "nested")
nested.Ref = testValue(t, 4, nestedVal)
mapVal := NewMapValue()
mapVal := apiservercel.NewMapValue()
mapVal.AddField(name)
mapVal.AddField(nested)
//rule := rt.ConvertToRule(testValue(t, 11, mapVal))
@ -156,11 +109,11 @@ func TestTypes_RuleTypesFieldMapping(t *testing.T) {
}
}
func testValue(t *testing.T, id int64, val interface{}) *DynValue {
func testValue(t *testing.T, id int64, val interface{}) *apiservercel.DynValue {
t.Helper()
dv, err := NewDynValue(id, val)
dv, err := apiservercel.NewDynValue(id, val)
if err != nil {
t.Fatalf("model.NewDynValue(%d, %v) failed: %v", id, val, err)
t.Fatalf("NewDynValue(%d, %v) failed: %v", id, val, err)
}
return dv
}

View File

@ -30,9 +30,10 @@ import (
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/metrics"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/metrics"
)
// Validator parallels the structure of schema.Structural and includes the compiled CEL programs
@ -74,7 +75,7 @@ func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64
// validator creates a Validator for all x-kubernetes-validations at the level of the provided schema and lower and
// returns the Validator if any x-kubernetes-validations exist in the schema, or nil if no x-kubernetes-validations
// exist. declType is expected to be a CEL DeclType corresponding to the structural schema.
func validator(s *schema.Structural, isResourceRoot bool, declType *model.DeclType, perCallLimit uint64) *Validator {
func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType, perCallLimit uint64) *Validator {
compiledRules, err := Compile(s, declType, perCallLimit)
var itemsValidator, additionalPropertiesValidator *Validator
var propertiesValidators map[string]Validator
@ -85,8 +86,8 @@ func validator(s *schema.Structural, isResourceRoot bool, declType *model.DeclTy
propertiesValidators = make(map[string]Validator, len(s.Properties))
for k, p := range s.Properties {
prop := p
var fieldType *model.DeclType
if escapedPropName, ok := model.Escape(k); ok {
var fieldType *cel.DeclType
if escapedPropName, ok := cel.Escape(k); ok {
if f, ok := declType.Fields[escapedPropName]; ok {
fieldType = f.Type
} else {

View File

@ -25,10 +25,11 @@ import (
"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"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apiserver/pkg/cel"
"k8s.io/kube-openapi/pkg/validation/strfmt"
)
@ -343,7 +344,7 @@ func (t *unstructuredMapList) Add(other ref.Val) ref.Val {
func escapeKeyProps(idents []string) []string {
result := make([]string, len(idents))
for i, prop := range idents {
if escaped, ok := model.Escape(prop); ok {
if escaped, ok := cel.Escape(prop); ok {
result[i] = escaped
} else {
result[i] = prop
@ -644,7 +645,7 @@ func (t *unstructuredMap) Iterator() traits.Iterator {
if _, ok := t.propSchema(k); ok {
mapKey := k
if isObject {
if escaped, ok := model.Escape(k); ok {
if escaped, ok := cel.Escape(k); ok {
mapKey = escaped
}
}
@ -683,7 +684,7 @@ func (t *unstructuredMap) Find(key ref.Val) (ref.Val, bool) {
}
k := keyStr.Value().(string)
if isObject {
k, ok = model.Unescape(k)
k, ok = cel.Unescape(k)
if !ok {
return nil, false
}

View File

@ -15,6 +15,7 @@ require (
github.com/google/gnostic v0.5.7-v3refs
github.com/google/go-cmp v0.5.9
github.com/google/gofuzz v1.1.0
github.com/google/cel-go v0.12.5
github.com/google/uuid v1.1.2
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package model
package cel
import (
"regexp"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package model
package cel
import (
"fmt"

View File

@ -22,7 +22,8 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
apiservercel "k8s.io/apiserver/pkg/cel"
)
// URLs provides a CEL function library extension of URL parsing functions.
@ -113,25 +114,25 @@ type urls struct{}
var urlLibraryDecls = map[string][]cel.FunctionOpt{
"url": {
cel.Overload("string_to_url", []*cel.Type{cel.StringType}, model.URLType,
cel.Overload("string_to_url", []*cel.Type{cel.StringType}, apiservercel.URLType,
cel.UnaryBinding(stringToUrl))},
"getScheme": {
cel.MemberOverload("url_get_scheme", []*cel.Type{model.URLType}, cel.StringType,
cel.MemberOverload("url_get_scheme", []*cel.Type{apiservercel.URLType}, cel.StringType,
cel.UnaryBinding(getScheme))},
"getHost": {
cel.MemberOverload("url_get_host", []*cel.Type{model.URLType}, cel.StringType,
cel.MemberOverload("url_get_host", []*cel.Type{apiservercel.URLType}, cel.StringType,
cel.UnaryBinding(getHost))},
"getHostname": {
cel.MemberOverload("url_get_hostname", []*cel.Type{model.URLType}, cel.StringType,
cel.MemberOverload("url_get_hostname", []*cel.Type{apiservercel.URLType}, cel.StringType,
cel.UnaryBinding(getHostname))},
"getPort": {
cel.MemberOverload("url_get_port", []*cel.Type{model.URLType}, cel.StringType,
cel.MemberOverload("url_get_port", []*cel.Type{apiservercel.URLType}, cel.StringType,
cel.UnaryBinding(getPort))},
"getEscapedPath": {
cel.MemberOverload("url_get_escaped_path", []*cel.Type{model.URLType}, cel.StringType,
cel.MemberOverload("url_get_escaped_path", []*cel.Type{apiservercel.URLType}, cel.StringType,
cel.UnaryBinding(getEscapedPath))},
"getQuery": {
cel.MemberOverload("url_get_query", []*cel.Type{model.URLType},
cel.MemberOverload("url_get_query", []*cel.Type{apiservercel.URLType},
cel.MapType(cel.StringType, cel.ListType(cel.StringType)),
cel.UnaryBinding(getQuery))},
"isURL": {
@ -169,7 +170,7 @@ func stringToUrl(arg ref.Val) ref.Val {
// Errors are not expected here since Parse is a more lenient parser than ParseRequestURI.
return types.NewErr("URL parse error during conversion from string: %v", err)
}
return model.URL{URL: u}
return apiservercel.URL{URL: u}
}
func getScheme(arg ref.Val) ref.Val {

View File

@ -0,0 +1,49 @@
/*
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
const (
// MaxRequestSizeBytes is the largest request that will be accepted is 3MB
// TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable
MaxRequestSizeBytes = int64(3 * 1024 * 1024)
// MaxDurationSizeJSON
// OpenAPI duration strings follow RFC 3339, section 5.6 - see the comment on maxDatetimeSizeJSON
MaxDurationSizeJSON = 32
// MaxDatetimeSizeJSON
// OpenAPI datetime strings follow RFC 3339, section 5.6, and the longest possible
// such string is 9999-12-31T23:59:59.999999999Z, which has length 30 - we add 2
// to allow for quotation marks
MaxDatetimeSizeJSON = 32
// MinDurationSizeJSON
// Golang allows a string of 0 to be parsed as a duration, so that plus 2 to account for
// quotation marks makes 3
MinDurationSizeJSON = 3
// JSONDateSize is the size of a date serialized as part of a JSON object
// RFC 3339 dates require YYYY-MM-DD, and then we add 2 to allow for quotation marks
JSONDateSize = 12
// MinDatetimeSizeJSON is the minimal length of a datetime formatted as RFC 3339
// RFC 3339 datetimes require a full date (YYYY-MM-DD) and full time (HH:MM:SS), and we add 3 for
// quotation marks like always in addition to the capital T that separates the date and time
MinDatetimeSizeJSON = 21
// MinStringSize is the size of literal ""
MinStringSize = 2
// MinBoolSize is the length of literal true
MinBoolSize = 4
// MinNumberSize is the length of literal 0
MinNumberSize = 1
)

View File

@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package model
package cel
import (
"sync"
"github.com/google/cel-go/cel"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
// Resolver declares methods to find policy templates and related configuration objects.
@ -30,12 +29,11 @@ type Resolver interface {
FindType(name string) (*DeclType, bool)
}
// NewRegistry create a registry for keeping track of environments, schemas, templates, and more
// NewRegistry create a registry for keeping track of environments and types
// from a base cel.Env expression environment.
func NewRegistry(stdExprEnv *cel.Env) *Registry {
return &Registry{
exprEnvs: map[string]*cel.Env{"": stdExprEnv},
schemas: map[string]*schema.Structural{},
types: map[string]*DeclType{
BoolType.TypeName(): BoolType,
BytesType.TypeName(): BytesType,
@ -58,7 +56,6 @@ func NewRegistry(stdExprEnv *cel.Env) *Registry {
type Registry struct {
rwMux sync.RWMutex
exprEnvs map[string]*cel.Env
schemas map[string]*schema.Structural
types map[string]*DeclType
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package model
package cel
import (
"fmt"
@ -79,7 +79,7 @@ func NewObjectType(name string, fields map[string]*DeclField) *DeclType {
return t
}
func newSimpleTypeWithMinSize(name string, celType *cel.Type, zeroVal ref.Val, minSize int64) *DeclType {
func NewSimpleTypeWithMinSize(name string, celType *cel.Type, zeroVal ref.Val, minSize int64) *DeclType {
return &DeclType{
name: name,
celType: celType,
@ -284,6 +284,16 @@ type DeclField struct {
defaultValue interface{}
}
func NewDeclField(name string, declType *DeclType, required bool, enumValues []interface{}, defaultValue interface{}) *DeclField {
return &DeclField{
Name: name,
Type: declType,
Required: required,
enumValues: enumValues,
defaultValue: defaultValue,
}
}
// TypeName returns the string type name of the field.
func (f *DeclField) TypeName() string {
return f.Type.TypeName()
@ -495,44 +505,44 @@ type schemaTypeProvider struct {
var (
// AnyType is equivalent to the CEL 'protobuf.Any' type in that the value may have any of the
// types supported.
AnyType = newSimpleTypeWithMinSize("any", cel.AnyType, nil, 1)
AnyType = NewSimpleTypeWithMinSize("any", cel.AnyType, nil, 1)
// BoolType is equivalent to the CEL 'bool' type.
BoolType = newSimpleTypeWithMinSize("bool", cel.BoolType, types.False, minBoolSize)
BoolType = NewSimpleTypeWithMinSize("bool", cel.BoolType, types.False, MinBoolSize)
// BytesType is equivalent to the CEL 'bytes' type.
BytesType = newSimpleTypeWithMinSize("bytes", cel.BytesType, types.Bytes([]byte{}), minStringSize)
BytesType = NewSimpleTypeWithMinSize("bytes", cel.BytesType, types.Bytes([]byte{}), MinStringSize)
// DoubleType is equivalent to the CEL 'double' type which is a 64-bit floating point value.
DoubleType = newSimpleTypeWithMinSize("double", cel.DoubleType, types.Double(0), minNumberSize)
DoubleType = NewSimpleTypeWithMinSize("double", cel.DoubleType, types.Double(0), MinNumberSize)
// DurationType is equivalent to the CEL 'duration' type.
DurationType = newSimpleTypeWithMinSize("duration", cel.DurationType, types.Duration{Duration: time.Duration(0)}, minDurationSizeJSON)
DurationType = NewSimpleTypeWithMinSize("duration", cel.DurationType, types.Duration{Duration: time.Duration(0)}, MinDurationSizeJSON)
// DateType is equivalent to the CEL 'date' type.
DateType = newSimpleTypeWithMinSize("date", cel.TimestampType, types.Timestamp{Time: time.Time{}}, dateSizeJSON)
DateType = NewSimpleTypeWithMinSize("date", cel.TimestampType, types.Timestamp{Time: time.Time{}}, JSONDateSize)
// DynType is the equivalent of the CEL 'dyn' concept which indicates that the type will be
// determined at runtime rather than compile time.
DynType = newSimpleTypeWithMinSize("dyn", cel.DynType, nil, 1)
DynType = NewSimpleTypeWithMinSize("dyn", cel.DynType, nil, 1)
// IntType is equivalent to the CEL 'int' type which is a 64-bit signed int.
IntType = newSimpleTypeWithMinSize("int", cel.IntType, types.IntZero, minNumberSize)
IntType = NewSimpleTypeWithMinSize("int", cel.IntType, types.IntZero, MinNumberSize)
// NullType is equivalent to the CEL 'null_type'.
NullType = newSimpleTypeWithMinSize("null_type", cel.NullType, types.NullValue, 4)
NullType = NewSimpleTypeWithMinSize("null_type", cel.NullType, types.NullValue, 4)
// StringType is equivalent to the CEL 'string' type which is expected to be a UTF-8 string.
// StringType values may either be string literals or expression strings.
StringType = newSimpleTypeWithMinSize("string", cel.StringType, types.String(""), minStringSize)
StringType = NewSimpleTypeWithMinSize("string", cel.StringType, types.String(""), MinStringSize)
// TimestampType corresponds to the well-known protobuf.Timestamp type supported within CEL.
// Note that both the OpenAPI date and date-time types map onto TimestampType, so not all types
// labeled as Timestamp will necessarily have the same MinSerializedSize.
TimestampType = newSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, dateSizeJSON)
TimestampType = NewSimpleTypeWithMinSize("timestamp", cel.TimestampType, types.Timestamp{Time: time.Time{}}, JSONDateSize)
// UintType is equivalent to the CEL 'uint' type.
UintType = newSimpleTypeWithMinSize("uint", cel.UintType, types.Uint(0), 1)
UintType = NewSimpleTypeWithMinSize("uint", cel.UintType, types.Uint(0), 1)
// ListType is equivalent to the CEL 'list' type.
ListType = NewListType(AnyType, noMaxLength)

View File

@ -0,0 +1,79 @@
/*
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 (
"testing"
)
func TestTypes_ListType(t *testing.T) {
list := NewListType(StringType, -1)
if !list.IsList() {
t.Error("list type not identifiable as list")
}
if list.TypeName() != "list" {
t.Errorf("got %s, wanted list", list.TypeName())
}
if list.DefaultValue() == nil {
t.Error("got nil zero value for list type")
}
if list.ElemType.TypeName() != "string" {
t.Errorf("got %s, wanted elem type of string", list.ElemType.TypeName())
}
expT, err := list.ExprType()
if err != nil {
t.Errorf("fail to get cel type: %s", err)
}
if expT.GetListType() == nil {
t.Errorf("got %v, wanted CEL list type", expT)
}
}
func TestTypes_MapType(t *testing.T) {
mp := NewMapType(StringType, IntType, -1)
if !mp.IsMap() {
t.Error("map type not identifiable as map")
}
if mp.TypeName() != "map" {
t.Errorf("got %s, wanted map", mp.TypeName())
}
if mp.DefaultValue() == nil {
t.Error("got nil zero value for map type")
}
if mp.KeyType.TypeName() != "string" {
t.Errorf("got %s, wanted key type of string", mp.KeyType.TypeName())
}
if mp.ElemType.TypeName() != "int" {
t.Errorf("got %s, wanted elem type of int", mp.ElemType.TypeName())
}
expT, err := mp.ExprType()
if err != nil {
t.Errorf("fail to get cel type: %s", err)
}
if expT.GetMapType() == nil {
t.Errorf("got %v, wanted CEL map type", expT)
}
}
func testValue(t *testing.T, id int64, val interface{}) *DynValue {
t.Helper()
dv, err := NewDynValue(id, val)
if err != nil {
t.Fatalf("NewDynValue(%d, %v) failed: %v", id, val, err)
}
return dv
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package model
package cel
import (
"fmt"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package model
package cel
import (
"fmt"

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package model
package cel
import (
"fmt"