mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
Update third_party/forked/celopenapi to support kubernetes schema extensions and CEL property name escaping
This commit is contained in:
parent
66af4ecfd5
commit
f0a80eda46
@ -17,14 +17,11 @@ limitations under the License.
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"regexp"
|
||||
|
||||
"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.
|
||||
// No identifiers are allowed to collide with these symbols.
|
||||
// https://github.com/google/cel-spec/blob/master/doc/langdef.md#syntax
|
||||
@ -36,47 +33,78 @@ var celReservedSymbols = sets.NewString(
|
||||
"var", "void", "while",
|
||||
)
|
||||
|
||||
// celLanguageIdentifiers is a list of identifiers that are part of the CEL language.
|
||||
// This does NOT include builtin macro or function identifiers.
|
||||
// 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",
|
||||
)
|
||||
// expandMatcher matches the escape sequence, characters that are escaped, and characters that are unsupported
|
||||
var expandMatcher = regexp.MustCompile(`(__|[-./]|[^a-zA-Z0-9-./_])`)
|
||||
|
||||
// IsRootReserved returns true if an identifier is reserved by CEL. Declaring root variables in CEL with
|
||||
// these identifiers is not allowed and would result in an "overlapping identifier for name '<identifier>'"
|
||||
// CEL compilation error.
|
||||
func IsRootReserved(prop string) bool {
|
||||
return celLanguageIdentifiers.Has(prop)
|
||||
}
|
||||
|
||||
// Escape escapes identifiers in the AlwaysReservedIdentifiers set by prefixing ident with "_" and by prefixing
|
||||
// any ident already prefixed with N '_' with N+1 '_'.
|
||||
// For an identifier that does not require escaping, the identifier is returned as-is.
|
||||
func Escape(ident string) string {
|
||||
if strings.HasPrefix(ident, "_") || celReservedSymbols.Has(ident) {
|
||||
return "_" + ident
|
||||
// Escape escapes ident and returns a CEL identifier (of the form '[a-zA-Z_][a-zA-Z0-9_]*'), or returns
|
||||
// false if the ident does not match the supported input format of `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*`.
|
||||
// Escaping Rules:
|
||||
// - '__' escapes to '__underscores__'
|
||||
// - '.' escapes to '__dot__'
|
||||
// - '-' escapes to '__dash__'
|
||||
// - '/' escapes to '__slash__'
|
||||
// - Identifiers that exactly match a CEL RESERVED keyword escape to '__{keyword}__'. The keywords are: "true", "false",
|
||||
// "null", "in", "as", "break", "const", "continue", "else", "for", "function", "if", "import", "let", loop", "package",
|
||||
// "namespace", "return".
|
||||
func Escape(ident string) (string, bool) {
|
||||
if len(ident) == 0 || ('0' <= ident[0] && ident[0] <= '9') {
|
||||
return "", false
|
||||
}
|
||||
return ident
|
||||
}
|
||||
|
||||
// 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)
|
||||
if celReservedSymbols.Has(ident) {
|
||||
return "__" + ident + "__", true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Unescape unescapes an identifier escaped by Escape.
|
||||
func Unescape(escaped string) string {
|
||||
if strings.HasPrefix(escaped, "_") {
|
||||
trimmed := strings.TrimPrefix(escaped, "_")
|
||||
if strings.HasPrefix(trimmed, "_") || celReservedSymbols.Has(trimmed) {
|
||||
return trimmed
|
||||
ok := true
|
||||
ident = expandMatcher.ReplaceAllStringFunc(ident, func(s string) string {
|
||||
switch s {
|
||||
case "__":
|
||||
return "__underscores__"
|
||||
case ".":
|
||||
return "__dot__"
|
||||
case "-":
|
||||
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
|
||||
}
|
||||
|
@ -17,99 +17,156 @@ limitations under the License.
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
fuzz "github.com/google/gofuzz"
|
||||
)
|
||||
|
||||
// TestEscaping tests that property names are escaped as expected.
|
||||
func TestEscaping(t *testing.T) {
|
||||
cases := []struct{
|
||||
unescaped string
|
||||
escaped string
|
||||
reservedAtRoot bool
|
||||
} {
|
||||
cases := []struct {
|
||||
unescaped string
|
||||
escaped string
|
||||
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
|
||||
{ unescaped: "true", escaped: "_true" },
|
||||
{ unescaped: "false", escaped: "_false" },
|
||||
{ unescaped: "null", escaped: "_null" },
|
||||
{ unescaped: "in", escaped: "_in" },
|
||||
{ unescaped: "as", escaped: "_as" },
|
||||
{ unescaped: "break", escaped: "_break" },
|
||||
{ unescaped: "const", escaped: "_const" },
|
||||
{ unescaped: "continue", escaped: "_continue" },
|
||||
{ unescaped: "else", escaped: "_else" },
|
||||
{ unescaped: "for", escaped: "_for" },
|
||||
{ unescaped: "function", escaped: "_function" },
|
||||
{ unescaped: "if", escaped: "_if" },
|
||||
{ unescaped: "import", escaped: "_import" },
|
||||
{ unescaped: "let", escaped: "_let" },
|
||||
{ unescaped: "loop", escaped: "_loop" },
|
||||
{ unescaped: "package", escaped: "_package" },
|
||||
{ unescaped: "namespace", escaped: "_namespace" },
|
||||
{ unescaped: "return", escaped: "_return" },
|
||||
{ unescaped: "var", escaped: "_var" },
|
||||
{ unescaped: "void", escaped: "_void" },
|
||||
{ unescaped: "while", escaped: "_while" },
|
||||
// CEL language identifiers do not need to be escaped, but collide with builtin language identifier if bound as
|
||||
// root variable names.
|
||||
// i.e. "self.int == 1" is legal, but "int == 1" is not.
|
||||
{ unescaped: "int", escaped: "int", reservedAtRoot: true },
|
||||
{ unescaped: "uint", escaped: "uint", reservedAtRoot: true },
|
||||
{ unescaped: "double", escaped: "double", reservedAtRoot: true },
|
||||
{ unescaped: "bool", escaped: "bool", reservedAtRoot: true },
|
||||
{ unescaped: "string", escaped: "string", reservedAtRoot: true },
|
||||
{ unescaped: "bytes", escaped: "bytes", reservedAtRoot: true },
|
||||
{ unescaped: "list", escaped: "list", reservedAtRoot: true },
|
||||
{ unescaped: "map", escaped: "map", reservedAtRoot: true },
|
||||
{ unescaped: "null_type", escaped: "null_type", reservedAtRoot: true },
|
||||
{ unescaped: "type", escaped: "type", reservedAtRoot: true },
|
||||
// To prevent escaping from colliding with other identifiers, all identifiers prefixed by _s are escaped by
|
||||
// prefixing them with N+1 _s.
|
||||
{ unescaped: "_if", escaped: "__if" },
|
||||
{ unescaped: "__if", escaped: "___if" },
|
||||
{ unescaped: "___if", escaped: "____if" },
|
||||
{ unescaped: "_has", escaped: "__has" },
|
||||
{ unescaped: "_int", escaped: "__int" },
|
||||
{ unescaped: "_anything", escaped: "__anything" },
|
||||
// CEL macro and function names do not need to be escaped because the parser can disambiguate them from the function and
|
||||
// macro identifiers.
|
||||
{ unescaped: "has", escaped: "has" },
|
||||
{ unescaped: "all", escaped: "all" },
|
||||
{ unescaped: "exists", escaped: "exists" },
|
||||
{ unescaped: "exists_one", escaped: "exists_one" },
|
||||
{ unescaped: "filter", escaped: "filter" },
|
||||
{ unescaped: "size", escaped: "size" },
|
||||
{ unescaped: "contains", escaped: "contains" },
|
||||
{ unescaped: "startsWith", escaped: "startsWith" },
|
||||
{ unescaped: "endsWith", escaped: "endsWith" },
|
||||
{ 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" },
|
||||
{unescaped: "true", escaped: "__true__"},
|
||||
{unescaped: "false", escaped: "__false__"},
|
||||
{unescaped: "null", escaped: "__null__"},
|
||||
{unescaped: "in", escaped: "__in__"},
|
||||
{unescaped: "as", escaped: "__as__"},
|
||||
{unescaped: "break", escaped: "__break__"},
|
||||
{unescaped: "const", escaped: "__const__"},
|
||||
{unescaped: "continue", escaped: "__continue__"},
|
||||
{unescaped: "else", escaped: "__else__"},
|
||||
{unescaped: "for", escaped: "__for__"},
|
||||
{unescaped: "function", escaped: "__function__"},
|
||||
{unescaped: "if", escaped: "__if__"},
|
||||
{unescaped: "import", escaped: "__import__"},
|
||||
{unescaped: "let", escaped: "__let__"},
|
||||
{unescaped: "loop", escaped: "__loop__"},
|
||||
{unescaped: "package", escaped: "__package__"},
|
||||
{unescaped: "namespace", escaped: "__namespace__"},
|
||||
{unescaped: "return", escaped: "__return__"},
|
||||
{unescaped: "var", escaped: "__var__"},
|
||||
{unescaped: "void", escaped: "__void__"},
|
||||
{unescaped: "while", escaped: "__while__"},
|
||||
// Not all property names are escapable
|
||||
{unescaped: "@", unescapable: true},
|
||||
{unescaped: "1up", unescapable: true},
|
||||
{unescaped: "👑", unescapable: true},
|
||||
// CEL macro and function names do not need to be escaped because the parser keeps identifiers in a
|
||||
// different namespace than function and macro names.
|
||||
{unescaped: "has", escaped: "has"},
|
||||
{unescaped: "all", escaped: "all"},
|
||||
{unescaped: "exists", escaped: "exists"},
|
||||
{unescaped: "exists_one", escaped: "exists_one"},
|
||||
{unescaped: "filter", escaped: "filter"},
|
||||
{unescaped: "size", escaped: "size"},
|
||||
{unescaped: "contains", escaped: "contains"},
|
||||
{unescaped: "startsWith", escaped: "startsWith"},
|
||||
{unescaped: "endsWith", escaped: "endsWith"},
|
||||
{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"},
|
||||
// we don't escape a single _
|
||||
{unescaped: "_if", escaped: "_if"},
|
||||
{unescaped: "_has", escaped: "_has"},
|
||||
{unescaped: "_int", escaped: "_int"},
|
||||
{unescaped: "_anything", escaped: "_anything"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
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 {
|
||||
t.Errorf("Expected %s to escape to %s, but got %s", tc.unescaped, tc.escaped, e)
|
||||
}
|
||||
u := Unescape(tc.escaped)
|
||||
if tc.unescaped != u {
|
||||
t.Errorf("Expected %s to unescape to %s, but got %s", tc.escaped, tc.unescaped, e)
|
||||
|
||||
if !validCelIdent.MatchString(e) {
|
||||
t.Errorf("Expected %s to escape to a valid CEL identifier, but got %s", tc.unescaped, e)
|
||||
}
|
||||
|
||||
isRootReserved := IsRootReserved(tc.unescaped)
|
||||
if tc.reservedAtRoot != isRootReserved {
|
||||
t.Errorf("Expected isRootReserved=%t for %s, but got %t", tc.reservedAtRoot, tc.unescaped, isRootReserved)
|
||||
u, ok := Unescape(tc.escaped)
|
||||
if !ok {
|
||||
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_]*$`)
|
||||
|
@ -18,45 +18,61 @@ import (
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
)
|
||||
|
||||
|
||||
// SchemaDeclTypes constructs a top-down set of DeclType instances whose name is derived from the root
|
||||
// type name provided on the call, if not set to a custom type.
|
||||
func SchemaDeclTypes(s *schema.Structural, maybeRootType string) (*DeclType, map[string]*DeclType) {
|
||||
root := SchemaDeclType(s).MaybeAssignTypeName(maybeRootType)
|
||||
types := FieldTypeMap(maybeRootType, root)
|
||||
return root, types
|
||||
}
|
||||
|
||||
// SchemaDeclType returns the cel type name associated with the schema element.
|
||||
func SchemaDeclType(s *schema.Structural) *DeclType {
|
||||
// SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the
|
||||
// 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.Structural, isResourceRoot bool) *DeclType {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if s.XIntOrString {
|
||||
// schemas using this extension are not required to have a type, so they must be handled before type lookup
|
||||
return intOrStringType
|
||||
}
|
||||
declType, found := openAPISchemaTypes[s.Type]
|
||||
if !found {
|
||||
return nil
|
||||
// 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
|
||||
//
|
||||
return DynType
|
||||
}
|
||||
|
||||
// We ignore XPreserveUnknownFields since we don't support validation rules on
|
||||
// data that we don't have schema information for.
|
||||
|
||||
if s.XEmbeddedResource {
|
||||
// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible
|
||||
// to validation rules since this part of the schema is well known and validated when CRDs
|
||||
// are created and updated.
|
||||
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 declType.TypeName() {
|
||||
case ListType.TypeName():
|
||||
return NewListType(SchemaDeclType(s.Items))
|
||||
case MapType.TypeName():
|
||||
switch s.Type {
|
||||
case "array":
|
||||
if s.Items != nil {
|
||||
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 {
|
||||
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))
|
||||
|
||||
@ -73,23 +89,23 @@ func SchemaDeclType(s *schema.Structural) *DeclType {
|
||||
enumValues = append(enumValues, e.Object)
|
||||
}
|
||||
}
|
||||
if fieldType := SchemaDeclType(&prop); fieldType != nil {
|
||||
fields[Escape(name)] = &DeclField{
|
||||
Name: Escape(name),
|
||||
Required: required[name],
|
||||
Type: fieldType,
|
||||
defaultValue: prop.Default.Object,
|
||||
enumValues: enumValues, // Enum values are represented as strings in CEL
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewObjectType("object", fields)
|
||||
case StringType.TypeName():
|
||||
case "string":
|
||||
if s.ValueValidation != nil {
|
||||
switch s.ValueValidation.Format {
|
||||
case "byte":
|
||||
return StringType // OpenAPIv3 byte format represents base64 encoded string
|
||||
case "binary":
|
||||
return BytesType
|
||||
case "duration":
|
||||
return DurationType
|
||||
@ -97,39 +113,17 @@ func SchemaDeclType(s *schema.Structural) *DeclType {
|
||||
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
|
||||
// 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 {
|
||||
|
@ -21,12 +21,13 @@ import (
|
||||
"github.com/google/cel-go/common/types"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
)
|
||||
|
||||
func TestSchemaDeclType(t *testing.T) {
|
||||
ts := testSchema()
|
||||
cust := SchemaDeclType(ts)
|
||||
cust := SchemaDeclType(ts, false)
|
||||
if cust.TypeName() != "object" {
|
||||
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) {
|
||||
ts := testSchema()
|
||||
cust, typeMap := SchemaDeclTypes(ts, "CustomObject")
|
||||
cust := SchemaDeclType(ts, true).MaybeAssignTypeName("CustomObject")
|
||||
typeMap := FieldTypeMap("CustomObject", cust)
|
||||
nested, _ := cust.FindField("nested")
|
||||
metadata, _ := cust.FindField("metadata")
|
||||
expectedObjTypeMap := map[string]*DeclType{
|
||||
|
@ -24,9 +24,9 @@ import (
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/common/types/traits"
|
||||
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
"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.
|
||||
func NewRuleTypes(kind string,
|
||||
schema *schema.Structural,
|
||||
isResourceRoot bool,
|
||||
res Resolver) (*RuleTypes, error) {
|
||||
// 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
|
||||
// annotation will be needed to indicate the expected environment and type of
|
||||
// the expression.
|
||||
schemaTypes, err := newSchemaTypeProvider(kind, schema)
|
||||
schemaTypes, err := newSchemaTypeProvider(kind, schema, isResourceRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if schemaTypes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &RuleTypes{
|
||||
Schema: schema,
|
||||
ruleSchemaDeclTypes: schemaTypes,
|
||||
@ -467,7 +471,6 @@ func (rt *RuleTypes) convertToCustomType(dyn *DynValue, declType *DeclType) *Dyn
|
||||
dyn.SetValue(obj)
|
||||
return dyn
|
||||
}
|
||||
// TODO: handle complex map types which have non-string keys.
|
||||
fieldType := declType.ElemType
|
||||
for _, f := range v.fieldMap {
|
||||
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) {
|
||||
root := SchemaDeclType(schema).MaybeAssignTypeName(kind)
|
||||
func newSchemaTypeProvider(kind string, schema *schema.Structural, isResourceRoot bool) (*schemaTypeProvider, error) {
|
||||
delType := SchemaDeclType(schema, isResourceRoot)
|
||||
if delType == nil {
|
||||
return nil, nil
|
||||
}
|
||||
root := delType.MaybeAssignTypeName(kind)
|
||||
types := FieldTypeMap(kind, root)
|
||||
return &schemaTypeProvider{
|
||||
root: root,
|
||||
|
@ -67,7 +67,7 @@ func TestTypes_MapType(t *testing.T) {
|
||||
func TestTypes_RuleTypesFieldMapping(t *testing.T) {
|
||||
stdEnv, _ := cel.NewEnv()
|
||||
reg := NewRegistry(stdEnv)
|
||||
rt, err := NewRuleTypes("CustomObject", testSchema(), reg)
|
||||
rt, err := NewRuleTypes("CustomObject", testSchema(), true, reg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
// TODO: Special case handling for protobuf Struct and Any if needed
|
||||
|
||||
// Unwrap pointers, but track their use.
|
||||
isPtr := false
|
||||
if typeDesc.Kind() == reflect.Ptr {
|
||||
|
Loading…
Reference in New Issue
Block a user