feature: add name formats library to CEL

This commit is contained in:
Alexander Zielenski 2024-02-28 18:04:33 -08:00
parent 11a6edfc88
commit 0ed65fca7a
8 changed files with 674 additions and 3 deletions

View File

@ -130,6 +130,13 @@ var baseOpts = []VersionedOptions{
library.CIDR(),
},
},
// Format Library
{
IntroducedVersion: version.MajorMinor(1, 31),
EnvOptions: []cel.EnvOption{
library.Format(),
},
},
}
// MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics

View File

@ -0,0 +1,73 @@
/*
Copyright 2024 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 (
"fmt"
"reflect"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
var (
FormatObject = decls.NewObjectType("kubernetes.NamedFormat")
FormatType = cel.ObjectType("kubernetes.NamedFormat")
)
// Format provdes a CEL representation of kubernetes format
type Format struct {
Name string
ValidateFunc func(string) []string
// Size of the regex string or estimated equivalent regex string used
// for cost estimation
MaxRegexSize int
}
func (d *Format) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
return nil, fmt.Errorf("type conversion error from 'Format' to '%v'", typeDesc)
}
func (d *Format) ConvertToType(typeVal ref.Type) ref.Val {
switch typeVal {
case FormatType:
return d
case types.TypeType:
return FormatType
default:
return types.NewErr("type conversion error from '%s' to '%s'", FormatType, typeVal)
}
}
func (d *Format) Equal(other ref.Val) ref.Val {
otherDur, ok := other.(*Format)
if !ok {
return types.MaybeNoSuchOverloadErr(other)
}
return types.Bool(d.Name == otherDur.Name)
}
func (d *Format) Type() ref.Type {
return FormatType
}
func (d *Format) Value() interface{} {
return d
}

View File

@ -25,6 +25,7 @@ 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/apiserver/pkg/cel"
)
// CostEstimator implements CEL's interpretable.ActualCostEstimator and checker.CostEstimator.
@ -152,6 +153,35 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
cost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor))
return &cost
}
case "validate":
if len(args) >= 2 {
format, isFormat := args[0].Value().(*cel.Format)
if isFormat {
strSize := actualSize(args[1])
// Dont have access to underlying regex, estimate a long regexp
regexSize := format.MaxRegexSize
// Copied from CEL implementation for regex cost
//
// https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL
// Add one to string length for purposes of cost calculation to prevent product of string and regex to be 0
// in case where string is empty but regex is still expensive.
strCost := uint64(math.Ceil((1.0 + float64(strSize)) * common.StringTraversalCostFactor))
// We don't know how many expressions are in the regex, just the string length (a huge
// improvement here would be to somehow get a count the number of expressions in the regex or
// how many states are in the regex state machine and use that to measure regex cost).
// For now, we're making a guess that each expression in a regex is typically at least 4 chars
// in length.
regexCost := uint64(math.Ceil(float64(regexSize) * common.RegexStringLengthCostFactor))
cost := strCost * regexCost
return &cost
}
}
case "format.named":
// Simply dictionary lookup
cost := uint64(1)
return &cost
case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub":
cost := uint64(1)
return &cost
@ -375,6 +405,13 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
sz := l.sizeEstimate(args[0])
return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor)}
}
case "validate":
if target != nil {
sz := l.sizeEstimate(args[0])
return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor).MultiplyByCostFactor(cel.MaxNameFormatRegexSize * common.RegexStringLengthCostFactor)}
}
case "format.named":
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub":
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
}

View File

@ -23,6 +23,7 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/ext"
@ -785,6 +786,42 @@ func TestQuantityCost(t *testing.T) {
}
}
func TestNameFormatCost(t *testing.T) {
cases := []struct {
name string
expr string
expectEstimatedCost checker.CostEstimate
expectRuntimeCost uint64
}{
{
name: "format.named",
expr: `format.named("dns1123subdomain")`,
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
expectRuntimeCost: 1,
},
{
name: "format.dns1123Subdomain.validate",
expr: `format.named("dns1123Subdomain").value().validate("my-name")`,
// Estimated cost doesnt know value at runtime so it is
// using an estimated maximum regex length
expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34},
expectRuntimeCost: 17,
},
{
name: "format.dns1123label.validate",
expr: `format.named("dns1123Label").value().validate("my-name")`,
expectEstimatedCost: checker.CostEstimate{Min: 34, Max: 34},
expectRuntimeCost: 10,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
})
}
}
func TestSetsCost(t *testing.T) {
cases := []struct {
name string
@ -1027,6 +1064,8 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
ext.Sets(),
IP(),
CIDR(),
Format(),
cel.OptionalTypes(),
// cel-go v0.17.7 introduced CostEstimatorOptions.
// Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes.
cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)),
@ -1040,7 +1079,11 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
}
compiled, issues := env.Compile(expr)
if len(issues.Errors()) > 0 {
t.Fatalf("%v", issues.Errors())
var errList []string
for _, issue := range issues.Errors() {
errList = append(errList, issue.ToDisplayString(common.NewTextSource(expr)))
}
t.Fatalf("%v", errList)
}
estCost, err := env.EstimateCost(compiled, est)
if err != nil {

View File

@ -0,0 +1,270 @@
/*
Copyright 2024 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 library
import (
"fmt"
"net/url"
"github.com/asaskevich/govalidator"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/validation"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/kube-openapi/pkg/validation/strfmt"
)
// Format provides a CEL library exposing common named Kubernetes string
// validations. Can be used in CRD ValidationRules messageExpression.
//
// Example:
//
// rule: format.dns1123label.validate(object.metadata.name).hasValue()
// messageExpression: format.dns1123label.validate(object.metadata.name).value().join("\n")
//
// format.named(name: string) -> ?Format
//
// Returns the Format with the given name, if it exists. Otherwise, optional.none
// Allowed names are:
// - `dns1123Label`
// - `dns1123Subdomain`
// - `dns1035Label`
// - `qualifiedName`
// - `dns1123LabelPrefix`
// - `dns1123SubdomainPrefix`
// - `dns1035LabelPrefix`
// - `labelValue`
// - `uri`
// - `uuid`
// - `byte`
// - `date`
// - `datetime`
//
// format.<formatName>() -> Format
//
// Convenience functions for all the named formats are also available
//
// Examples:
// format.dns1123Label().validate("my-label-name")
// format.dns1123Subdomain().validate("apiextensions.k8s.io")
// format.dns1035Label().validate("my-label-name")
// format.qualifiedName().validate("apiextensions.k8s.io/v1beta1")
// format.dns1123LabelPrefix().validate("my-label-prefix-")
// format.dns1123SubdomainPrefix().validate("mysubdomain.prefix.-")
// format.dns1035LabelPrefix().validate("my-label-prefix-")
// format.uri().validate("http://example.com")
// Uses same pattern as isURL, but returns an error
// format.uuid().validate("123e4567-e89b-12d3-a456-426614174000")
// format.byte().validate("aGVsbG8=")
// format.date().validate("2021-01-01")
// format.datetime().validate("2021-01-01T00:00:00Z")
//
// <Format>.validate(str: string) -> ?list<string>
//
// Validates the given string against the given format. Returns optional.none
// if the string is valid, otherwise a list of validation error strings.
func Format() cel.EnvOption {
return cel.Lib(formatLib)
}
var formatLib = &format{}
type format struct{}
func (*format) LibraryName() string {
return "format"
}
func ZeroArgumentFunctionBinding(binding func() ref.Val) decls.OverloadOpt {
return func(o *decls.OverloadDecl) (*decls.OverloadDecl, error) {
wrapped, err := decls.FunctionBinding(func(values ...ref.Val) ref.Val { return binding() })(o)
if err != nil {
return nil, err
}
if len(wrapped.ArgTypes()) != 0 {
return nil, fmt.Errorf("function binding must have 0 arguments")
}
return o, nil
}
}
func (*format) CompileOptions() []cel.EnvOption {
options := make([]cel.EnvOption, 0, len(formatLibraryDecls))
for name, overloads := range formatLibraryDecls {
options = append(options, cel.Function(name, overloads...))
}
for name, constantValue := range ConstantFormats {
prefixedName := "format." + name
options = append(options, cel.Function(prefixedName, cel.Overload(prefixedName, []*cel.Type{}, apiservercel.FormatType, ZeroArgumentFunctionBinding(func() ref.Val {
return constantValue
}))))
}
return options
}
func (*format) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}
var ConstantFormats map[string]*apiservercel.Format = map[string]*apiservercel.Format{
"dns1123Label": {
Name: "DNS1123Label",
ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, false) },
MaxRegexSize: 30,
},
"dns1123Subdomain": {
Name: "DNS1123Subdomain",
ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSSubdomain(s, false) },
MaxRegexSize: 60,
},
"dns1035Label": {
Name: "DNS1035Label",
ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNS1035Label(s, false) },
MaxRegexSize: 30,
},
"qualifiedName": {
Name: "QualifiedName",
ValidateFunc: validation.IsQualifiedName,
MaxRegexSize: 60, // uses subdomain regex
},
"dns1123LabelPrefix": {
Name: "DNS1123LabelPrefix",
ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSLabel(s, true) },
MaxRegexSize: 30,
},
"dns1123SubdomainPrefix": {
Name: "DNS1123SubdomainPrefix",
ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNSSubdomain(s, true) },
MaxRegexSize: 60,
},
"dns1035LabelPrefix": {
Name: "DNS1035LabelPrefix",
ValidateFunc: func(s string) []string { return apimachineryvalidation.NameIsDNS1035Label(s, true) },
MaxRegexSize: 30,
},
"labelValue": {
Name: "LabelValue",
ValidateFunc: validation.IsValidLabelValue,
MaxRegexSize: 40,
},
// CRD formats
// Implementations sourced from strfmt, which kube-openapi uses as its
// format library. There are other CRD formats supported, but they are
// covered by other portions of the CEL library (like IP/CIDR), or their
// use is discouraged (like bsonobjectid, email, etc)
"uri": {
Name: "URI",
ValidateFunc: func(s string) []string {
// Directly call ParseRequestURI since we can get a better error message
_, err := url.ParseRequestURI(s)
if err != nil {
return []string{err.Error()}
}
return nil
},
// Use govalidator url regex to estimate, since ParseRequestURI
// doesnt use regex
MaxRegexSize: len(govalidator.URL),
},
"uuid": {
Name: "uuid",
ValidateFunc: func(s string) []string {
if !strfmt.Default.Validates("uuid", s) {
return []string{"does not match the UUID format"}
}
return nil
},
MaxRegexSize: len(strfmt.UUIDPattern),
},
"byte": {
Name: "byte",
ValidateFunc: func(s string) []string {
if !strfmt.Default.Validates("byte", s) {
return []string{"invalid base64"}
}
return nil
},
MaxRegexSize: len(govalidator.Base64),
},
"date": {
Name: "date",
ValidateFunc: func(s string) []string {
if !strfmt.Default.Validates("date", s) {
return []string{"invalid date"}
}
return nil
},
// Estimated regex size for RFC3339FullDate which is
// a date format. Assume a date-time pattern is longer
// so use that to conservatively estimate this
MaxRegexSize: len(strfmt.DateTimePattern),
},
"datetime": {
Name: "datetime",
ValidateFunc: func(s string) []string {
if !strfmt.Default.Validates("datetime", s) {
return []string{"invalid datetime"}
}
return nil
},
MaxRegexSize: len(strfmt.DateTimePattern),
},
}
var formatLibraryDecls = map[string][]cel.FunctionOpt{
"validate": {
cel.MemberOverload("format-validate", []*cel.Type{apiservercel.FormatType, cel.StringType}, cel.OptionalType(cel.ListType(cel.StringType)), cel.BinaryBinding(formatValidate)),
},
"format.named": {
cel.Overload("format-named", []*cel.Type{cel.StringType}, cel.OptionalType(apiservercel.FormatType), cel.UnaryBinding(func(name ref.Val) ref.Val {
nameString, ok := name.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(name)
}
f, ok := ConstantFormats[nameString]
if !ok {
return types.OptionalNone
}
return types.OptionalOf(f)
})),
},
}
func formatValidate(arg1, arg2 ref.Val) ref.Val {
f, ok := arg1.Value().(*apiservercel.Format)
if !ok {
return types.MaybeNoSuchOverloadErr(arg1)
}
str, ok := arg2.Value().(string)
if !ok {
return types.MaybeNoSuchOverloadErr(arg2)
}
res := f.ValidateFunc(str)
if len(res) == 0 {
return types.OptionalNone
}
return types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, res))
}

View File

@ -0,0 +1,230 @@
/*
Copyright 2024 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 library_test
import (
"fmt"
"testing"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"k8s.io/apiserver/pkg/cel/library"
)
func TestFormat(t *testing.T) {
type testcase struct {
name string
expr string
expectValue ref.Val
expectedCompileErr []string
expectedRuntimeErr string
}
cases := []testcase{
{
name: "example_usage_dns1123Label",
expr: `format.dns1123Label().validate("my-label-name")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_dns1123Subdomain",
expr: `format.dns1123Subdomain().validate("apiextensions.k8s.io")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_qualifiedName",
expr: `format.qualifiedName().validate("apiextensions.k8s.io/v1beta1")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_dns1123LabelPrefix",
expr: `format.dns1123LabelPrefix().validate("my-label-prefix-")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_dns1123SubdomainPrefix",
expr: `format.dns1123SubdomainPrefix().validate("mysubdomain.prefix.-")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_dns1035LabelPrefix",
expr: `format.dns1035LabelPrefix().validate("my-label-prefix-")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_uri",
expr: `format.uri().validate("http://example.com")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_uuid",
expr: `format.uuid().validate("123e4567-e89b-12d3-a456-426614174000")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_byte",
expr: `format.byte().validate("aGVsbG8=")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_date",
expr: `format.date().validate("2021-01-01")`,
expectValue: types.OptionalNone,
},
{
name: "example_usage_datetime",
expr: `format.datetime().validate("2021-01-01T00:00:00Z")`,
expectValue: types.OptionalNone,
},
{
name: "dns1123Label",
expr: `format.dns1123Label().validate("contains a space")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"})),
},
{
name: "dns1123Subdomain",
expr: `format.dns1123Subdomain().validate("contains a space")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`})),
},
{
name: "dns1035Label",
expr: `format.dns1035Label().validate("contains a space")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')`})),
},
{
name: "qualifiedName",
expr: `format.qualifiedName().validate("contains a space")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`})),
},
{
name: "dns1123LabelPrefix",
expr: `format.dns1123LabelPrefix().validate("contains a space-")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')"})),
},
{
name: "dns1123SubdomainPrefix",
expr: `format.dns1123SubdomainPrefix().validate("contains a space-")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`})),
},
{
name: "dns1035LabelPrefix",
expr: `format.dns1035LabelPrefix().validate("contains a space-")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{`a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123', regex used for validation is '[a-z]([-a-z0-9]*[a-z0-9])?')`})),
},
{
name: "dns1123Label_Success",
expr: `format.dns1123Label().validate("my-label-name")`,
expectValue: types.OptionalNone,
},
{
name: "dns1123Subdomain_Success",
expr: `format.dns1123Subdomain().validate("example.com")`,
expectValue: types.OptionalNone,
},
{
name: "dns1035Label_Success",
expr: `format.dns1035Label().validate("my-label-name")`,
expectValue: types.OptionalNone,
},
{
name: "qualifiedName_Success",
expr: `format.qualifiedName().validate("my.name")`,
expectValue: types.OptionalNone,
},
{
// byte is base64
name: "byte_success",
expr: `format.byte().validate("aGVsbG8=")`,
expectValue: types.OptionalNone,
},
{
// byte is base64
name: "byte_failure",
expr: `format.byte().validate("aGVsbG8")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"invalid base64"})),
},
{
name: "date_success",
expr: `format.date().validate("2020-01-01")`,
// date is a valid date
expectValue: types.OptionalNone,
},
{
name: "date_failure",
expr: `format.date().validate("2020-01-32")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"invalid date"})),
},
{
name: "datetime_success",
expr: `format.datetime().validate("2020-01-01T00:00:00Z")`,
// datetime is a valid date
expectValue: types.OptionalNone,
},
{
name: "datetime_failure",
expr: `format.datetime().validate("2020-01-32T00:00:00Z")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{"invalid datetime"})),
},
{
name: "unknown_format",
expr: `format.named("unknown").hasValue()`,
expectValue: types.False,
},
{
name: "labelValue_success",
expr: `format.labelValue().validate("my-cool-label-Value")`,
expectValue: types.OptionalNone,
},
{
name: "labelValue_failure",
expr: `format.labelValue().validate("my-cool-label-Value!!\n\n!!!")`,
expectValue: types.OptionalOf(types.NewStringList(types.DefaultTypeAdapter, []string{
"a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')",
})),
},
}
// Also test format names and comparisons of all constants
for keyLHS := range library.ConstantFormats {
cases = append(cases, testcase{
name: "lookup and comparison",
expr: fmt.Sprintf(`format.named("%s").hasValue()`, keyLHS),
expectValue: types.True,
}, testcase{
name: "comparison with lookup succeeds",
expr: fmt.Sprintf(`format.named("%s").value() == format.%s()`, keyLHS, keyLHS),
expectValue: types.True,
})
for keyRHS := range library.ConstantFormats {
if keyLHS == keyRHS {
continue
}
cases = append(cases, testcase{
name: fmt.Sprintf("compare_%s_%s", keyLHS, keyRHS),
expr: fmt.Sprintf(`format.%s() == format.%s()`, keyLHS, keyRHS),
expectValue: types.False,
})
}
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
testQuantity(t, tc.expr, tc.expectValue, tc.expectedRuntimeErr, tc.expectedCompileErr)
})
}
}

View File

@ -21,9 +21,11 @@ import (
"testing"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/resource"
@ -34,11 +36,13 @@ import (
func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
env, err := cel.NewEnv(
cel.OptionalTypes(),
ext.Strings(),
library.URLs(),
library.Regex(),
library.Lists(),
library.Quantity(),
library.Format(),
)
if err != nil {
t.Fatalf("%v", err)
@ -79,7 +83,12 @@ func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntime
require.Empty(t, missingCompileErrs, "expected compilation errors")
return
} else if len(issues.Errors()) > 0 {
t.Fatalf("%v", issues.Errors())
errorStrings := []string{}
source := common.NewTextSource(expr)
for _, issue := range issues.Errors() {
errorStrings = append(errorStrings, issue.ToDisplayString(source))
}
t.Fatalf("%v", errorStrings)
}
// Typecheck expression
@ -105,7 +114,7 @@ func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntime
t.Fatalf("%v", err)
} else if expectResult != nil {
converted := res.Equal(expectResult).Value().(bool)
require.True(t, converted, "expectation not equal to output")
require.True(t, converted, "expectation not equal to output: %v", cmp.Diff(expectResult.Value(), res.Value()))
} else {
t.Fatal("expected result must not be nil")
}

View File

@ -47,4 +47,6 @@ const (
MinBoolSize = 4
// MinNumberSize is the length of literal 0
MinNumberSize = 1
MaxNameFormatRegexSize = 128
)