mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-02 00:07:50 +00:00
feature: add name formats library to CEL
This commit is contained in:
parent
11a6edfc88
commit
0ed65fca7a
@ -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
|
||||
|
73
staging/src/k8s.io/apiserver/pkg/cel/format.go
Normal file
73
staging/src/k8s.io/apiserver/pkg/cel/format.go
Normal 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
|
||||
}
|
@ -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}}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
270
staging/src/k8s.io/apiserver/pkg/cel/library/format.go
Normal file
270
staging/src/k8s.io/apiserver/pkg/cel/library/format.go
Normal 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))
|
||||
}
|
230
staging/src/k8s.io/apiserver/pkg/cel/library/format_test.go
Normal file
230
staging/src/k8s.io/apiserver/pkg/cel/library/format_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -47,4 +47,6 @@ const (
|
||||
MinBoolSize = 4
|
||||
// MinNumberSize is the length of literal 0
|
||||
MinNumberSize = 1
|
||||
|
||||
MaxNameFormatRegexSize = 128
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user