Update third_party/forked/celopenapi to support kubernetes schema extensions and CEL property name escaping

This commit is contained in:
Joe Betz 2021-11-15 21:41:53 -05:00
parent 66af4ecfd5
commit f0a80eda46
7 changed files with 281 additions and 195 deletions

View File

@ -17,14 +17,11 @@ limitations under the License.
package model package model
import ( import (
"fmt" "regexp"
"strings"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
) )
// TODO: replace escaping with new rules described in kEP update
// celReservedSymbols is a list of RESERVED symbols defined in the CEL lexer. // celReservedSymbols is a list of RESERVED symbols defined in the CEL lexer.
// No identifiers are allowed to collide with these symbols. // No identifiers are allowed to collide with these symbols.
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#syntax // https://github.com/google/cel-spec/blob/master/doc/langdef.md#syntax
@ -36,47 +33,78 @@ var celReservedSymbols = sets.NewString(
"var", "void", "while", "var", "void", "while",
) )
// celLanguageIdentifiers is a list of identifiers that are part of the CEL language. // expandMatcher matches the escape sequence, characters that are escaped, and characters that are unsupported
// This does NOT include builtin macro or function identifiers. var expandMatcher = regexp.MustCompile(`(__|[-./]|[^a-zA-Z0-9-./_])`)
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#values
var celLanguageIdentifiers = sets.NewString(
"int", "uint", "double", "bool", "string", "bytes", "list", "map", "null_type", "type",
)
// IsRootReserved returns true if an identifier is reserved by CEL. Declaring root variables in CEL with // Escape escapes ident and returns a CEL identifier (of the form '[a-zA-Z_][a-zA-Z0-9_]*'), or returns
// these identifiers is not allowed and would result in an "overlapping identifier for name '<identifier>'" // false if the ident does not match the supported input format of `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*`.
// CEL compilation error. // Escaping Rules:
func IsRootReserved(prop string) bool { // - '__' escapes to '__underscores__'
return celLanguageIdentifiers.Has(prop) // - '.' escapes to '__dot__'
} // - '-' escapes to '__dash__'
// - '/' escapes to '__slash__'
// Escape escapes identifiers in the AlwaysReservedIdentifiers set by prefixing ident with "_" and by prefixing // - Identifiers that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are: "true", "false",
// any ident already prefixed with N '_' with N+1 '_'. // "null", "in", "as", "break", "const", "continue", "else", "for", "function", "if", "import", "let", loop", "package",
// For an identifier that does not require escaping, the identifier is returned as-is. // "namespace", "return".
func Escape(ident string) string { func Escape(ident string) (string, bool) {
if strings.HasPrefix(ident, "_") || celReservedSymbols.Has(ident) { if len(ident) == 0 || ('0' <= ident[0] && ident[0] <= '9') {
return "_" + ident return "", false
} }
return ident if celReservedSymbols.Has(ident) {
} return "__" + ident + "__", true
// EscapeSlice returns identifiers with Escape applied to each.
func EscapeSlice(idents []string) []string {
result := make([]string, len(idents))
for i, prop := range idents {
result[i] = Escape(prop)
} }
return result ok := true
} ident = expandMatcher.ReplaceAllStringFunc(ident, func(s string) string {
switch s {
// Unescape unescapes an identifier escaped by Escape. case "__":
func Unescape(escaped string) string { return "__underscores__"
if strings.HasPrefix(escaped, "_") { case ".":
trimmed := strings.TrimPrefix(escaped, "_") return "__dot__"
if strings.HasPrefix(trimmed, "_") || celReservedSymbols.Has(trimmed) { case "-":
return trimmed return "__dash__"
case "/":
return "__slash__"
default: // matched a unsupported supported
ok = false
return ""
} }
panic(fmt.Sprintf("failed to unescape improperly escaped string: %v", escaped)) })
if !ok {
return "", false
} }
return escaped return ident, true
}
var unexpandMatcher = regexp.MustCompile(`(_{2}[^_]+_{2})`)
// Unescape unescapes an CEL identifier containing the escape sequences described in Escape, or return false if the
// string contains invalid escape sequences. The escaped input is expected to be a valid CEL identifier, but is
// not checked.
func Unescape(escaped string) (string, bool) {
ok := true
escaped = unexpandMatcher.ReplaceAllStringFunc(escaped, func(s string) string {
contents := s[2 : len(s)-2]
switch contents {
case "underscores":
return "__"
case "dot":
return "."
case "dash":
return "-"
case "slash":
return "/"
}
if celReservedSymbols.Has(contents) {
if len(s) != len(escaped) {
ok = false
}
return contents
}
ok = false
return ""
})
if !ok {
return "", false
}
return escaped, true
} }

View File

@ -17,99 +17,156 @@ limitations under the License.
package model package model
import ( import (
"fmt"
"regexp"
"testing" "testing"
fuzz "github.com/google/gofuzz"
) )
// TestEscaping tests that property names are escaped as expected. // TestEscaping tests that property names are escaped as expected.
func TestEscaping(t *testing.T) { func TestEscaping(t *testing.T) {
cases := []struct{ cases := []struct {
unescaped string unescaped string
escaped string escaped string
reservedAtRoot bool unescapable bool
} { }{
// '.', '-', '/' and '__' are escaped since
// CEL only allows identifiers of the form: [a-zA-Z_][a-zA-Z0-9_]*
{unescaped: "a.a", escaped: "a__dot__a"},
{unescaped: "a-a", escaped: "a__dash__a"},
{unescaped: "a__a", escaped: "a__underscores__a"},
{unescaped: "a.-/__a", escaped: "a__dot____dash____slash____underscores__a"},
{unescaped: "a._a", escaped: "a__dot___a"},
{unescaped: "a__.__a", escaped: "a__underscores____dot____underscores__a"},
{unescaped: "a___a", escaped: "a__underscores___a"},
{unescaped: "a____a", escaped: "a__underscores____underscores__a"},
{unescaped: "a__dot__a", escaped: "a__underscores__dot__underscores__a"},
{unescaped: "a__underscores__a", escaped: "a__underscores__underscores__underscores__a"},
// CEL lexer RESERVED keywords must be escaped // CEL lexer RESERVED keywords must be escaped
{ unescaped: "true", escaped: "_true" }, {unescaped: "true", escaped: "__true__"},
{ unescaped: "false", escaped: "_false" }, {unescaped: "false", escaped: "__false__"},
{ unescaped: "null", escaped: "_null" }, {unescaped: "null", escaped: "__null__"},
{ unescaped: "in", escaped: "_in" }, {unescaped: "in", escaped: "__in__"},
{ unescaped: "as", escaped: "_as" }, {unescaped: "as", escaped: "__as__"},
{ unescaped: "break", escaped: "_break" }, {unescaped: "break", escaped: "__break__"},
{ unescaped: "const", escaped: "_const" }, {unescaped: "const", escaped: "__const__"},
{ unescaped: "continue", escaped: "_continue" }, {unescaped: "continue", escaped: "__continue__"},
{ unescaped: "else", escaped: "_else" }, {unescaped: "else", escaped: "__else__"},
{ unescaped: "for", escaped: "_for" }, {unescaped: "for", escaped: "__for__"},
{ unescaped: "function", escaped: "_function" }, {unescaped: "function", escaped: "__function__"},
{ unescaped: "if", escaped: "_if" }, {unescaped: "if", escaped: "__if__"},
{ unescaped: "import", escaped: "_import" }, {unescaped: "import", escaped: "__import__"},
{ unescaped: "let", escaped: "_let" }, {unescaped: "let", escaped: "__let__"},
{ unescaped: "loop", escaped: "_loop" }, {unescaped: "loop", escaped: "__loop__"},
{ unescaped: "package", escaped: "_package" }, {unescaped: "package", escaped: "__package__"},
{ unescaped: "namespace", escaped: "_namespace" }, {unescaped: "namespace", escaped: "__namespace__"},
{ unescaped: "return", escaped: "_return" }, {unescaped: "return", escaped: "__return__"},
{ unescaped: "var", escaped: "_var" }, {unescaped: "var", escaped: "__var__"},
{ unescaped: "void", escaped: "_void" }, {unescaped: "void", escaped: "__void__"},
{ unescaped: "while", escaped: "_while" }, {unescaped: "while", escaped: "__while__"},
// CEL language identifiers do not need to be escaped, but collide with builtin language identifier if bound as // Not all property names are escapable
// root variable names. {unescaped: "@", unescapable: true},
// i.e. "self.int == 1" is legal, but "int == 1" is not. {unescaped: "1up", unescapable: true},
{ unescaped: "int", escaped: "int", reservedAtRoot: true }, {unescaped: "👑", unescapable: true},
{ unescaped: "uint", escaped: "uint", reservedAtRoot: true }, // CEL macro and function names do not need to be escaped because the parser keeps identifiers in a
{ unescaped: "double", escaped: "double", reservedAtRoot: true }, // different namespace than function and macro names.
{ unescaped: "bool", escaped: "bool", reservedAtRoot: true }, {unescaped: "has", escaped: "has"},
{ unescaped: "string", escaped: "string", reservedAtRoot: true }, {unescaped: "all", escaped: "all"},
{ unescaped: "bytes", escaped: "bytes", reservedAtRoot: true }, {unescaped: "exists", escaped: "exists"},
{ unescaped: "list", escaped: "list", reservedAtRoot: true }, {unescaped: "exists_one", escaped: "exists_one"},
{ unescaped: "map", escaped: "map", reservedAtRoot: true }, {unescaped: "filter", escaped: "filter"},
{ unescaped: "null_type", escaped: "null_type", reservedAtRoot: true }, {unescaped: "size", escaped: "size"},
{ unescaped: "type", escaped: "type", reservedAtRoot: true }, {unescaped: "contains", escaped: "contains"},
// To prevent escaping from colliding with other identifiers, all identifiers prefixed by _s are escaped by {unescaped: "startsWith", escaped: "startsWith"},
// prefixing them with N+1 _s. {unescaped: "endsWith", escaped: "endsWith"},
{ unescaped: "_if", escaped: "__if" }, {unescaped: "matches", escaped: "matches"},
{ unescaped: "__if", escaped: "___if" }, {unescaped: "duration", escaped: "duration"},
{ unescaped: "___if", escaped: "____if" }, {unescaped: "timestamp", escaped: "timestamp"},
{ unescaped: "_has", escaped: "__has" }, {unescaped: "getDate", escaped: "getDate"},
{ unescaped: "_int", escaped: "__int" }, {unescaped: "getDayOfMonth", escaped: "getDayOfMonth"},
{ unescaped: "_anything", escaped: "__anything" }, {unescaped: "getDayOfWeek", escaped: "getDayOfWeek"},
// CEL macro and function names do not need to be escaped because the parser can disambiguate them from the function and {unescaped: "getFullYear", escaped: "getFullYear"},
// macro identifiers. {unescaped: "getHours", escaped: "getHours"},
{ unescaped: "has", escaped: "has" }, {unescaped: "getMilliseconds", escaped: "getMilliseconds"},
{ unescaped: "all", escaped: "all" }, {unescaped: "getMinutes", escaped: "getMinutes"},
{ unescaped: "exists", escaped: "exists" }, {unescaped: "getMonth", escaped: "getMonth"},
{ unescaped: "exists_one", escaped: "exists_one" }, {unescaped: "getSeconds", escaped: "getSeconds"},
{ unescaped: "filter", escaped: "filter" }, // we don't escape a single _
{ unescaped: "size", escaped: "size" }, {unescaped: "_if", escaped: "_if"},
{ unescaped: "contains", escaped: "contains" }, {unescaped: "_has", escaped: "_has"},
{ unescaped: "startsWith", escaped: "startsWith" }, {unescaped: "_int", escaped: "_int"},
{ unescaped: "endsWith", escaped: "endsWith" }, {unescaped: "_anything", escaped: "_anything"},
{ unescaped: "matches", escaped: "matches" },
{ unescaped: "duration", escaped: "duration" },
{ unescaped: "timestamp", escaped: "timestamp" },
{ unescaped: "getDate", escaped: "getDate" },
{ unescaped: "getDayOfMonth", escaped: "getDayOfMonth" },
{ unescaped: "getDayOfWeek", escaped: "getDayOfWeek" },
{ unescaped: "getFullYear", escaped: "getFullYear" },
{ unescaped: "getHours", escaped: "getHours" },
{ unescaped: "getMilliseconds", escaped: "getMilliseconds" },
{ unescaped: "getMinutes", escaped: "getMinutes" },
{ unescaped: "getMonth", escaped: "getMonth" },
{ unescaped: "getSeconds", escaped: "getSeconds" },
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.unescaped, func(t *testing.T) { t.Run(tc.unescaped, func(t *testing.T) {
e := Escape(tc.unescaped) e, escapable := Escape(tc.unescaped)
if tc.unescapable {
if escapable {
t.Errorf("Expected escapable=false, but got %t", escapable)
}
return
}
if !escapable {
t.Fatalf("Expected escapable=true, but got %t", escapable)
}
if tc.escaped != e { if tc.escaped != e {
t.Errorf("Expected %s to escape to %s, but got %s", tc.unescaped, tc.escaped, e) t.Errorf("Expected %s to escape to %s, but got %s", tc.unescaped, tc.escaped, e)
} }
u := Unescape(tc.escaped)
if tc.unescaped != u { if !validCelIdent.MatchString(e) {
t.Errorf("Expected %s to unescape to %s, but got %s", tc.escaped, tc.unescaped, e) t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", tc.unescaped, e)
} }
isRootReserved := IsRootReserved(tc.unescaped) u, ok := Unescape(tc.escaped)
if tc.reservedAtRoot != isRootReserved { if !ok {
t.Errorf("Expected isRootReserved=%t for %s, but got %t", tc.reservedAtRoot, tc.unescaped, isRootReserved) t.Fatalf("Expected %s to be escapable, but it was not", tc.escaped)
}
if tc.unescaped != u {
t.Errorf("Expected %s to unescape to %s, but got %s", tc.escaped, tc.unescaped, u)
} }
}) })
} }
} }
func TestUnescapeMalformed(t *testing.T) {
for _, s := range []string{"__int__extra", "__illegal__"} {
t.Run(s, func(t *testing.T) {
e, ok := Unescape(s)
if ok {
t.Fatalf("Expected %s to be unescapable, but it escaped to: %s", s, e)
}
})
}
}
func TestEscapingFuzz(t *testing.T) {
fuzzer := fuzz.New()
for i := 0; i < 1000; i++ {
var unescaped string
fuzzer.Fuzz(&unescaped)
t.Run(fmt.Sprintf("%d - '%s'", i, unescaped), func(t *testing.T) {
if len(unescaped) == 0 {
return
}
escaped, ok := Escape(unescaped)
if !ok {
return
}
if !validCelIdent.MatchString(escaped) {
t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", unescaped, escaped)
}
u, ok := Unescape(escaped)
if !ok {
t.Fatalf("Expected %s to be unescapable, but it was not", escaped)
}
if unescaped != u {
t.Errorf("Expected %s to unescape to %s, but got %s", escaped, unescaped, u)
}
})
}
}
var validCelIdent = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)

View File

@ -18,45 +18,61 @@ import (
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
) )
// SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the
// SchemaDeclTypes constructs a top-down set of DeclType instances whose name is derived from the root // the structural schema should not be exposed in CEL expressions.
// type name provided on the call, if not set to a custom type. // Set isResourceRoot to true for the root of a custom resource or embedded resource.
func SchemaDeclTypes(s *schema.Structural, maybeRootType string) (*DeclType, map[string]*DeclType) { //
root := SchemaDeclType(s).MaybeAssignTypeName(maybeRootType) // Schemas with XPreserveUnknownFields not exposed unless they are objects. Array and "maps" schemas
types := FieldTypeMap(maybeRootType, root) // are not exposed if their items or additionalProperties schemas are not exposed. Object Properties are not exposed
return root, types // if their schema is not exposed.
} //
// The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
// SchemaDeclType returns the cel type name associated with the schema element. func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *DeclType {
func SchemaDeclType(s *schema.Structural) *DeclType {
if s == nil { if s == nil {
return nil return nil
} }
if s.XIntOrString { if s.XIntOrString {
// schemas using this extension are not required to have a type, so they must be handled before type lookup // schemas using XIntOrString are not required to have a type.
return intOrStringType
} // intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
declType, found := openAPISchemaTypes[s.Type] // In CEL, the type is represented as dynamic value, which can be thought of as a union type of all types.
if !found { // All type checking for XIntOrString is deferred to runtime, so all access to values of this type must
return nil // 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
//
return DynType
} }
// We ignore XPreserveUnknownFields since we don't support validation rules on // We ignore XPreserveUnknownFields since we don't support validation rules on
// data that we don't have schema information for. // data that we don't have schema information for.
if s.XEmbeddedResource { if isResourceRoot {
// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible // 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible to validator rules
// to validation rules since this part of the schema is well known and validated when CRDs // at the root of resources, even if not specified in the schema.
// are created and updated. // This includes the root of a custom resource and the root of XEmbeddedResource objects.
s = WithTypeAndObjectMeta(s) s = WithTypeAndObjectMeta(s)
} }
switch declType.TypeName() { switch s.Type {
case ListType.TypeName(): case "array":
return NewListType(SchemaDeclType(s.Items)) if s.Items != nil {
case MapType.TypeName(): itemsType := SchemaDeclType(s.Items, s.Items.XEmbeddedResource)
if itemsType != nil {
return NewListType(itemsType)
}
}
return nil
case "object":
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil { if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
return NewMapType(StringType, SchemaDeclType(s.AdditionalProperties.Structural)) propsType := SchemaDeclType(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource)
if propsType != nil {
return NewMapType(StringType, propsType)
}
return nil
} }
fields := make(map[string]*DeclField, len(s.Properties)) fields := make(map[string]*DeclField, len(s.Properties))
@ -73,23 +89,23 @@ func SchemaDeclType(s *schema.Structural) *DeclType {
enumValues = append(enumValues, e.Object) enumValues = append(enumValues, e.Object)
} }
} }
if fieldType := SchemaDeclType(&prop); fieldType != nil { if fieldType := SchemaDeclType(&prop, prop.XEmbeddedResource); fieldType != nil {
fields[Escape(name)] = &DeclField{ if propName, ok := Escape(name); ok {
Name: Escape(name), fields[propName] = &DeclField{
Required: required[name], Name: propName,
Type: fieldType, Required: required[name],
defaultValue: prop.Default.Object, Type: fieldType,
enumValues: enumValues, // Enum values are represented as strings in CEL defaultValue: prop.Default.Object,
enumValues: enumValues, // Enum values are represented as strings in CEL
}
} }
} }
} }
return NewObjectType("object", fields) return NewObjectType("object", fields)
case StringType.TypeName(): case "string":
if s.ValueValidation != nil { if s.ValueValidation != nil {
switch s.ValueValidation.Format { switch s.ValueValidation.Format {
case "byte": case "byte":
return StringType // OpenAPIv3 byte format represents base64 encoded string
case "binary":
return BytesType return BytesType
case "duration": case "duration":
return DurationType return DurationType
@ -97,39 +113,17 @@ func SchemaDeclType(s *schema.Structural) *DeclType {
return TimestampType return TimestampType
} }
} }
return StringType
case "boolean":
return BoolType
case "number":
return DoubleType
case "integer":
return IntType
} }
return declType return nil
} }
var (
openAPISchemaTypes = map[string]*DeclType{
"boolean": BoolType,
"number": DoubleType,
"integer": IntType,
"null": NullType,
"string": StringType,
"date": DateType,
"array": ListType,
"object": MapType,
}
// intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
// In CEL, the type is represented as an object where either the srtVal
// or intVal field is set. In CEL, this allows for typesafe expressions like:
//
// require that the string representation be a percentage:
// `has(intOrStringField.strVal) && intOrStringField.strVal.matches(r'(\d+(\.\d+)?%)')`
// validate requirements on both the int and string representation:
// `has(intOrStringField.intVal) ? intOrStringField.intVal < 5 : double(intOrStringField.strVal.replace('%', '')) < 0.5
//
intOrStringType = NewObjectType("intOrString", map[string]*DeclField{
"strVal": {Name: "strVal", Type: StringType},
"intVal": {Name: "intVal", Type: IntType},
})
)
// TODO: embedded objects should have objectMeta only, and name and generateName are both optional
// WithTypeAndObjectMeta ensures the kind, apiVersion and // WithTypeAndObjectMeta ensures the kind, apiVersion and
// metadata.name and metadata.generateName properties are specified, making a shallow copy of the provided schema if needed. // metadata.name and metadata.generateName properties are specified, making a shallow copy of the provided schema if needed.
func WithTypeAndObjectMeta(s *schema.Structural) *schema.Structural { func WithTypeAndObjectMeta(s *schema.Structural) *schema.Structural {

View File

@ -21,12 +21,13 @@ import (
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
) )
func TestSchemaDeclType(t *testing.T) { func TestSchemaDeclType(t *testing.T) {
ts := testSchema() ts := testSchema()
cust := SchemaDeclType(ts) cust := SchemaDeclType(ts, false)
if cust.TypeName() != "object" { if cust.TypeName() != "object" {
t.Errorf("incorrect type name, got %v, wanted object", cust.TypeName()) t.Errorf("incorrect type name, got %v, wanted object", cust.TypeName())
} }
@ -82,7 +83,8 @@ func TestSchemaDeclType(t *testing.T) {
func TestSchemaDeclTypes(t *testing.T) { func TestSchemaDeclTypes(t *testing.T) {
ts := testSchema() ts := testSchema()
cust, typeMap := SchemaDeclTypes(ts, "CustomObject") cust := SchemaDeclType(ts, true).MaybeAssignTypeName("CustomObject")
typeMap := FieldTypeMap("CustomObject", cust)
nested, _ := cust.FindField("nested") nested, _ := cust.FindField("nested")
metadata, _ := cust.FindField("metadata") metadata, _ := cust.FindField("metadata")
expectedObjTypeMap := map[string]*DeclType{ expectedObjTypeMap := map[string]*DeclType{

View File

@ -24,9 +24,9 @@ import (
"github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits" "github.com/google/cel-go/common/types/traits"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema" "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
) )
@ -306,15 +306,19 @@ func (f *DeclField) EnumValues() []ref.Val {
// NewRuleTypes returns an Open API Schema-based type-system which is CEL compatible. // NewRuleTypes returns an Open API Schema-based type-system which is CEL compatible.
func NewRuleTypes(kind string, func NewRuleTypes(kind string,
schema *schema.Structural, schema *schema.Structural,
isResourceRoot bool,
res Resolver) (*RuleTypes, error) { res Resolver) (*RuleTypes, error) {
// Note, if the schema indicates that it's actually based on another proto // Note, if the schema indicates that it's actually based on another proto
// then prefer the proto definition. For expressions in the proto, a new field // then prefer the proto definition. For expressions in the proto, a new field
// annotation will be needed to indicate the expected environment and type of // annotation will be needed to indicate the expected environment and type of
// the expression. // the expression.
schemaTypes, err := newSchemaTypeProvider(kind, schema) schemaTypes, err := newSchemaTypeProvider(kind, schema, isResourceRoot)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if schemaTypes == nil {
return nil, nil
}
return &RuleTypes{ return &RuleTypes{
Schema: schema, Schema: schema,
ruleSchemaDeclTypes: schemaTypes, ruleSchemaDeclTypes: schemaTypes,
@ -467,7 +471,6 @@ func (rt *RuleTypes) convertToCustomType(dyn *DynValue, declType *DeclType) *Dyn
dyn.SetValue(obj) dyn.SetValue(obj)
return dyn return dyn
} }
// TODO: handle complex map types which have non-string keys.
fieldType := declType.ElemType fieldType := declType.ElemType
for _, f := range v.fieldMap { for _, f := range v.fieldMap {
f.Ref = rt.convertToCustomType(f.Ref, fieldType) f.Ref = rt.convertToCustomType(f.Ref, fieldType)
@ -485,8 +488,12 @@ func (rt *RuleTypes) convertToCustomType(dyn *DynValue, declType *DeclType) *Dyn
} }
} }
func newSchemaTypeProvider(kind string, schema *schema.Structural) (*schemaTypeProvider, error) { func newSchemaTypeProvider(kind string, schema *schema.Structural, isResourceRoot bool) (*schemaTypeProvider, error) {
root := SchemaDeclType(schema).MaybeAssignTypeName(kind) delType := SchemaDeclType(schema, isResourceRoot)
if delType == nil {
return nil, nil
}
root := delType.MaybeAssignTypeName(kind)
types := FieldTypeMap(kind, root) types := FieldTypeMap(kind, root)
return &schemaTypeProvider{ return &schemaTypeProvider{
root: root, root: root,

View File

@ -67,7 +67,7 @@ func TestTypes_MapType(t *testing.T) {
func TestTypes_RuleTypesFieldMapping(t *testing.T) { func TestTypes_RuleTypesFieldMapping(t *testing.T) {
stdEnv, _ := cel.NewEnv() stdEnv, _ := cel.NewEnv()
reg := NewRegistry(stdEnv) reg := NewRegistry(stdEnv)
rt, err := NewRuleTypes("CustomObject", testSchema(), reg) rt, err := NewRuleTypes("CustomObject", testSchema(), true, reg)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -205,8 +205,6 @@ func (sv *structValue) ConvertToNative(typeDesc reflect.Type) (interface{}, erro
return nil, fmt.Errorf("type conversion error from object to '%v'", typeDesc) return nil, fmt.Errorf("type conversion error from object to '%v'", typeDesc)
} }
// TODO: Special case handling for protobuf Struct and Any if needed
// Unwrap pointers, but track their use. // Unwrap pointers, but track their use.
isPtr := false isPtr := false
if typeDesc.Kind() == reflect.Ptr { if typeDesc.Kind() == reflect.Ptr {