Merge pull request #123572 from alexzielenski/cel/name-formats
feature: add name formats library to CEL
This commit is contained in:
		@@ -130,6 +130,13 @@ var baseOpts = []VersionedOptions{
 | 
				
			|||||||
			library.CIDR(),
 | 
								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
 | 
					// 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"
 | 
				
			||||||
	"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"
 | 
				
			||||||
 | 
						"k8s.io/apiserver/pkg/cel"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CostEstimator implements CEL's interpretable.ActualCostEstimator and checker.CostEstimator.
 | 
					// 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))
 | 
								cost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor))
 | 
				
			||||||
			return &cost
 | 
								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":
 | 
						case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub":
 | 
				
			||||||
		cost := uint64(1)
 | 
							cost := uint64(1)
 | 
				
			||||||
		return &cost
 | 
							return &cost
 | 
				
			||||||
@@ -375,6 +405,13 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
 | 
				
			|||||||
			sz := l.sizeEstimate(args[0])
 | 
								sz := l.sizeEstimate(args[0])
 | 
				
			||||||
			return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor)}
 | 
								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":
 | 
						case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub":
 | 
				
			||||||
		return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
 | 
							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/cel"
 | 
				
			||||||
	"github.com/google/cel-go/checker"
 | 
						"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/ast"
 | 
				
			||||||
	"github.com/google/cel-go/common/types"
 | 
						"github.com/google/cel-go/common/types"
 | 
				
			||||||
	"github.com/google/cel-go/ext"
 | 
						"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) {
 | 
					func TestSetsCost(t *testing.T) {
 | 
				
			||||||
	cases := []struct {
 | 
						cases := []struct {
 | 
				
			||||||
		name                string
 | 
							name                string
 | 
				
			||||||
@@ -1027,6 +1064,8 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
 | 
				
			|||||||
		ext.Sets(),
 | 
							ext.Sets(),
 | 
				
			||||||
		IP(),
 | 
							IP(),
 | 
				
			||||||
		CIDR(),
 | 
							CIDR(),
 | 
				
			||||||
 | 
							Format(),
 | 
				
			||||||
 | 
							cel.OptionalTypes(),
 | 
				
			||||||
		// cel-go v0.17.7 introduced CostEstimatorOptions.
 | 
							// 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.
 | 
							// 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)),
 | 
							cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)),
 | 
				
			||||||
@@ -1040,7 +1079,11 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	compiled, issues := env.Compile(expr)
 | 
						compiled, issues := env.Compile(expr)
 | 
				
			||||||
	if len(issues.Errors()) > 0 {
 | 
						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)
 | 
						estCost, err := env.EstimateCost(compiled, est)
 | 
				
			||||||
	if err != nil {
 | 
						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"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/google/cel-go/cel"
 | 
						"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"
 | 
				
			||||||
	"github.com/google/cel-go/common/types/ref"
 | 
						"github.com/google/cel-go/common/types/ref"
 | 
				
			||||||
	"github.com/google/cel-go/ext"
 | 
						"github.com/google/cel-go/ext"
 | 
				
			||||||
 | 
						"github.com/google/go-cmp/cmp"
 | 
				
			||||||
	"github.com/stretchr/testify/require"
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"k8s.io/apimachinery/pkg/api/resource"
 | 
						"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) {
 | 
					func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
 | 
				
			||||||
	env, err := cel.NewEnv(
 | 
						env, err := cel.NewEnv(
 | 
				
			||||||
 | 
							cel.OptionalTypes(),
 | 
				
			||||||
		ext.Strings(),
 | 
							ext.Strings(),
 | 
				
			||||||
		library.URLs(),
 | 
							library.URLs(),
 | 
				
			||||||
		library.Regex(),
 | 
							library.Regex(),
 | 
				
			||||||
		library.Lists(),
 | 
							library.Lists(),
 | 
				
			||||||
		library.Quantity(),
 | 
							library.Quantity(),
 | 
				
			||||||
 | 
							library.Format(),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		t.Fatalf("%v", err)
 | 
							t.Fatalf("%v", err)
 | 
				
			||||||
@@ -79,7 +83,18 @@ func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntime
 | 
				
			|||||||
		require.Empty(t, missingCompileErrs, "expected compilation errors")
 | 
							require.Empty(t, missingCompileErrs, "expected compilation errors")
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	} else if len(issues.Errors()) > 0 {
 | 
						} 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
 | 
				
			||||||
 | 
						_, err = cel.AstToCheckedExpr(compiled)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("%v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	prog, err := env.Program(compiled)
 | 
						prog, err := env.Program(compiled)
 | 
				
			||||||
@@ -99,7 +114,7 @@ func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntime
 | 
				
			|||||||
		t.Fatalf("%v", err)
 | 
							t.Fatalf("%v", err)
 | 
				
			||||||
	} else if expectResult != nil {
 | 
						} else if expectResult != nil {
 | 
				
			||||||
		converted := res.Equal(expectResult).Value().(bool)
 | 
							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 {
 | 
						} else {
 | 
				
			||||||
		t.Fatal("expected result must not be nil")
 | 
							t.Fatal("expected result must not be nil")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,4 +47,6 @@ const (
 | 
				
			|||||||
	MinBoolSize = 4
 | 
						MinBoolSize = 4
 | 
				
			||||||
	// MinNumberSize is the length of literal 0
 | 
						// MinNumberSize is the length of literal 0
 | 
				
			||||||
	MinNumberSize = 1
 | 
						MinNumberSize = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						MaxNameFormatRegexSize = 128
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user