mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-10 20:42:26 +00:00
Add quantity library to CEL (#118803)
* add quantity library to CEL * add more tests to quantity * use 1.29 env for quantity * set CEL default env to 1.28 for 1.28 release * add compare function * docs and arith lib * fixup addInt and subInt overload, add docs * more tests * cleanup docs * remove old comments * remove unnecessary cast * add isInteger * add overflow tests * boilerplate * refactor expectedResult for tests * doc typo fix * returns bool * add docs link * different dos link * add isInteger true case * expand iff * add quantity back to 1.28 version, and revert change to DefaultCompatibilityVersion * formatting
This commit is contained in:
parent
fc798a8dc1
commit
423f4dfc79
@ -78,6 +78,7 @@ var baseOpts = []VersionedOptions{
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.CrossTypeNumericComparisons(true),
|
||||
cel.OptionalTypes(),
|
||||
library.Quantity(),
|
||||
},
|
||||
},
|
||||
// TODO: switch to ext.Strings version 2 once format() is fixed to work with HomogeneousAggregateLiterals.
|
||||
|
375
staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go
Normal file
375
staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go
Normal file
@ -0,0 +1,375 @@
|
||||
/*
|
||||
Copyright 2023 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 (
|
||||
"errors"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
)
|
||||
|
||||
// Quantity provides a CEL function library extension of Kubernetes
|
||||
// resource.Quantity parsing functions. See `resource.Quantity`
|
||||
// documentation for more detailed information about the format itself:
|
||||
// https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity
|
||||
//
|
||||
// quantity
|
||||
//
|
||||
// Converts a string to a Quantity or results in an error if the string is not a valid Quantity. Refer
|
||||
// to resource.Quantity documentation for information on accepted patterns.
|
||||
//
|
||||
// quantity(<string>) <Quantity>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// quantity('1.5G') // returns a Quantity
|
||||
// quantity('200k') // returns a Quantity
|
||||
// quantity('200K') // error
|
||||
// quantity('Three') // error
|
||||
// quantity('Mi') // error
|
||||
//
|
||||
// isQuantity
|
||||
//
|
||||
// Returns true if a string is a valid Quantity. isQuantity returns true if and
|
||||
// only if quantity does not result in error.
|
||||
//
|
||||
// isQuantity( <string>) <bool>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// isQuantity('1.3G') // returns true
|
||||
// isQuantity('1.3Gi') // returns true
|
||||
// isQuantity('1,3G') // returns false
|
||||
// isQuantity('10000k') // returns true
|
||||
// isQuantity('200K') // returns false
|
||||
// isQuantity('Three') // returns false
|
||||
// isQuantity('Mi') // returns false
|
||||
//
|
||||
// Conversion to Scalars:
|
||||
//
|
||||
// - isInteger: returns true if and only if asInteger is safe to call without an error
|
||||
//
|
||||
// - asInteger: returns a representation of the current value as an int64 if
|
||||
// possible or results in an error if conversion would result in overflow
|
||||
// or loss of precision.
|
||||
//
|
||||
// - asApproximateFloat: returns a float64 representation of the quantity which may
|
||||
// lose precision. If the value of the quantity is outside the range of a float64
|
||||
// +Inf/-Inf will be returned.
|
||||
//
|
||||
// <Quantity>.isInteger() <bool>
|
||||
// <Quantity>.asInteger() <int>
|
||||
// <Quantity>.asApproximateFloat() <float>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// quantity("50000000G").isInteger() // returns true
|
||||
// quantity("50k").isInteger() // returns true
|
||||
// quantity("9999999999999999999999999999999999999G").asInteger() // error: cannot convert value to integer
|
||||
// quantity("9999999999999999999999999999999999999G").isInteger() // returns false
|
||||
// quantity("50k").asInteger() == 50000 // returns true
|
||||
// quantity("50k").sub(20000).asApproximateFloat() == 30000 // returns true
|
||||
//
|
||||
// Arithmetic
|
||||
//
|
||||
// - sign: Returns `1` if the quantity is positive, `-1` if it is negative. `0` if it is zero
|
||||
//
|
||||
// - add: Returns sum of two quantities or a quantity and an integer
|
||||
//
|
||||
// - sub: Returns difference between two quantities or a quantity and an integer
|
||||
//
|
||||
// <Quantity>.sign() <int>
|
||||
// <Quantity>.add(<quantity>) <quantity>
|
||||
// <Quantity>.add(<integer>) <quantity>
|
||||
// <Quantity>.sub(<quantity>) <quantity>
|
||||
// <Quantity>.sub(<integer>) <quantity>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// quantity("50k").add("20k") == quantity("70k") // returns true
|
||||
// quantity("50k").add(20) == quantity("50020") // returns true
|
||||
// quantity("50k").sub("20k") == quantity("30k") // returns true
|
||||
// quantity("50k").sub(20000) == quantity("30k") // returns true
|
||||
// quantity("50k").add(20).sub(quantity("100k")).sub(-50000) == quantity("20") // returns true
|
||||
//
|
||||
// Comparisons
|
||||
//
|
||||
// - isGreaterThan: Returns true if and only if the receiver is greater than the operand
|
||||
//
|
||||
// - isLessThan: Returns true if and only if the receiver is less than the operand
|
||||
//
|
||||
// - compareTo: Compares receiver to operand and returns 0 if they are equal, 1 if the receiver is greater, or -1 if the receiver is less than the operand
|
||||
//
|
||||
//
|
||||
// <Quantity>.isLessThan(<quantity>) <bool>
|
||||
// <Quantity>.isGreaterThan(<quantity>) <bool>
|
||||
// <Quantity>.compareTo(<quantity>) <int>
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// quantity("200M").compareTo(quantity("0.2G")) // returns 0
|
||||
// quantity("50M").compareTo(quantity("50Mi")) // returns -1
|
||||
// quantity("50Mi").compareTo(quantity("50M")) // returns 1
|
||||
// quantity("150Mi").isGreaterThan(quantity("100Mi")) // returns true
|
||||
// quantity("50Mi").isGreaterThan(quantity("100Mi")) // returns false
|
||||
// quantity("50M").isLessThan(quantity("100M")) // returns true
|
||||
// quantity("100M").isLessThan(quantity("50M")) // returns false
|
||||
|
||||
func Quantity() cel.EnvOption {
|
||||
return cel.Lib(quantityLib)
|
||||
}
|
||||
|
||||
var quantityLib = &quantity{}
|
||||
|
||||
type quantity struct{}
|
||||
|
||||
var quantityLibraryDecls = map[string][]cel.FunctionOpt{
|
||||
"quantity": {
|
||||
cel.Overload("string_to_quantity", []*cel.Type{cel.StringType}, apiservercel.QuantityType, cel.UnaryBinding((stringToQuantity))),
|
||||
},
|
||||
"isQuantity": {
|
||||
cel.Overload("is_quantity_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(isQuantity)),
|
||||
},
|
||||
"sign": {
|
||||
cel.Overload("quantity_sign", []*cel.Type{apiservercel.QuantityType}, cel.IntType, cel.UnaryBinding(quantityGetSign)),
|
||||
},
|
||||
"isGreaterThan": {
|
||||
cel.MemberOverload("quantity_is_greater_than", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, cel.BoolType, cel.BinaryBinding(quantityIsGreaterThan)),
|
||||
},
|
||||
"isLessThan": {
|
||||
cel.MemberOverload("quantity_is_less_than", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, cel.BoolType, cel.BinaryBinding(quantityIsLessThan)),
|
||||
},
|
||||
"compareTo": {
|
||||
cel.MemberOverload("quantity_compare_to", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, cel.IntType, cel.BinaryBinding(quantityCompareTo)),
|
||||
},
|
||||
"asApproximateFloat": {
|
||||
cel.MemberOverload("quantity_get_float", []*cel.Type{apiservercel.QuantityType}, cel.DoubleType, cel.UnaryBinding(quantityGetApproximateFloat)),
|
||||
},
|
||||
"asInteger": {
|
||||
cel.MemberOverload("quantity_get_int", []*cel.Type{apiservercel.QuantityType}, cel.IntType, cel.UnaryBinding(quantityGetValue)),
|
||||
},
|
||||
"isInteger": {
|
||||
cel.MemberOverload("quantity_is_integer", []*cel.Type{apiservercel.QuantityType}, cel.BoolType, cel.UnaryBinding(quantityCanValue)),
|
||||
},
|
||||
"add": {
|
||||
cel.MemberOverload("quantity_add", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, apiservercel.QuantityType, cel.BinaryBinding(quantityAdd)),
|
||||
cel.MemberOverload("quantity_add_int", []*cel.Type{apiservercel.QuantityType, cel.IntType}, apiservercel.QuantityType, cel.BinaryBinding(quantityAddInt)),
|
||||
},
|
||||
"sub": {
|
||||
cel.MemberOverload("quantity_sub", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, apiservercel.QuantityType, cel.BinaryBinding(quantitySub)),
|
||||
cel.MemberOverload("quantity_sub_int", []*cel.Type{apiservercel.QuantityType, cel.IntType}, apiservercel.QuantityType, cel.BinaryBinding(quantitySubInt)),
|
||||
},
|
||||
}
|
||||
|
||||
func (*quantity) CompileOptions() []cel.EnvOption {
|
||||
options := make([]cel.EnvOption, 0, len(quantityLibraryDecls))
|
||||
for name, overloads := range quantityLibraryDecls {
|
||||
options = append(options, cel.Function(name, overloads...))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (*quantity) ProgramOptions() []cel.ProgramOption {
|
||||
return []cel.ProgramOption{}
|
||||
}
|
||||
|
||||
func isQuantity(arg ref.Val) ref.Val {
|
||||
str, ok := arg.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
_, err := resource.ParseQuantity(str)
|
||||
if err != nil {
|
||||
return types.Bool(false)
|
||||
}
|
||||
|
||||
return types.Bool(true)
|
||||
}
|
||||
|
||||
func stringToQuantity(arg ref.Val) ref.Val {
|
||||
str, ok := arg.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q, err := resource.ParseQuantity(str)
|
||||
if err != nil {
|
||||
return types.WrapErr(err)
|
||||
}
|
||||
|
||||
return apiservercel.Quantity{Quantity: &q}
|
||||
}
|
||||
|
||||
func quantityGetApproximateFloat(arg ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
return types.Double(q.AsApproximateFloat64())
|
||||
}
|
||||
|
||||
func quantityCanValue(arg ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
_, success := q.AsInt64()
|
||||
return types.Bool(success)
|
||||
}
|
||||
|
||||
func quantityGetValue(arg ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
v, success := q.AsInt64()
|
||||
if !success {
|
||||
return types.WrapErr(errors.New("cannot convert value to integer"))
|
||||
}
|
||||
return types.Int(v)
|
||||
}
|
||||
|
||||
func quantityGetSign(arg ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
return types.Int(q.Sign())
|
||||
}
|
||||
|
||||
func quantityIsGreaterThan(arg ref.Val, other ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q2, ok := other.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
return types.Bool(q.Cmp(*q2) == 1)
|
||||
}
|
||||
|
||||
func quantityIsLessThan(arg ref.Val, other ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q2, ok := other.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
return types.Bool(q.Cmp(*q2) == -1)
|
||||
}
|
||||
|
||||
func quantityCompareTo(arg ref.Val, other ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q2, ok := other.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
return types.Int(q.Cmp(*q2))
|
||||
}
|
||||
|
||||
func quantityAdd(arg ref.Val, other ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q2, ok := other.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
copy := *q
|
||||
copy.Add(*q2)
|
||||
return &apiservercel.Quantity{
|
||||
Quantity: ©,
|
||||
}
|
||||
}
|
||||
|
||||
func quantityAddInt(arg ref.Val, other ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q2, ok := other.Value().(int64)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q2Converted := *resource.NewQuantity(q2, resource.DecimalExponent)
|
||||
|
||||
copy := *q
|
||||
copy.Add(q2Converted)
|
||||
return &apiservercel.Quantity{
|
||||
Quantity: ©,
|
||||
}
|
||||
}
|
||||
|
||||
func quantitySub(arg ref.Val, other ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q2, ok := other.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
copy := *q
|
||||
copy.Sub(*q2)
|
||||
return &apiservercel.Quantity{
|
||||
Quantity: ©,
|
||||
}
|
||||
}
|
||||
|
||||
func quantitySubInt(arg ref.Val, other ref.Val) ref.Val {
|
||||
q, ok := arg.Value().(*resource.Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q2, ok := other.Value().(int64)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg)
|
||||
}
|
||||
|
||||
q2Converted := *resource.NewQuantity(q2, resource.DecimalExponent)
|
||||
|
||||
copy := *q
|
||||
copy.Sub(q2Converted)
|
||||
return &apiservercel.Quantity{
|
||||
Quantity: ©,
|
||||
}
|
||||
}
|
295
staging/src/k8s.io/apiserver/pkg/cel/library/quantity_test.go
Normal file
295
staging/src/k8s.io/apiserver/pkg/cel/library/quantity_test.go
Normal file
@ -0,0 +1,295 @@
|
||||
/*
|
||||
Copyright 2023 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 (
|
||||
"regexp"
|
||||
"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/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
func testQuantity(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
|
||||
env, err := cel.NewEnv(
|
||||
ext.Strings(),
|
||||
library.URLs(),
|
||||
library.Regex(),
|
||||
library.Lists(),
|
||||
library.Quantity(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
compiled, issues := env.Compile(expr)
|
||||
|
||||
if len(expectCompileErrs) > 0 {
|
||||
missingCompileErrs := []string{}
|
||||
matchedCompileErrs := sets.New[int]()
|
||||
for _, expectedCompileErr := range expectCompileErrs {
|
||||
compiledPattern, err := regexp.Compile(expectedCompileErr)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to compile expected err regex: %v", err)
|
||||
}
|
||||
|
||||
didMatch := false
|
||||
|
||||
for i, compileError := range issues.Errors() {
|
||||
if compiledPattern.Match([]byte(compileError.Message)) {
|
||||
didMatch = true
|
||||
matchedCompileErrs.Insert(i)
|
||||
}
|
||||
}
|
||||
|
||||
if !didMatch {
|
||||
missingCompileErrs = append(missingCompileErrs, expectedCompileErr)
|
||||
} else if len(matchedCompileErrs) != len(issues.Errors()) {
|
||||
unmatchedErrs := []common.Error{}
|
||||
for i, issue := range issues.Errors() {
|
||||
if !matchedCompileErrs.Has(i) {
|
||||
unmatchedErrs = append(unmatchedErrs, issue)
|
||||
}
|
||||
}
|
||||
require.Empty(t, unmatchedErrs, "unexpected compilation errors")
|
||||
}
|
||||
}
|
||||
|
||||
require.Empty(t, missingCompileErrs, "expected compilation errors")
|
||||
return
|
||||
} else if len(issues.Errors()) > 0 {
|
||||
t.Fatalf("%v", issues.Errors())
|
||||
}
|
||||
|
||||
prog, err := env.Program(compiled)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
res, _, err := prog.Eval(map[string]interface{}{})
|
||||
if len(expectRuntimeErrPattern) > 0 {
|
||||
if err == nil {
|
||||
t.Fatalf("no runtime error thrown. Expected: %v", expectRuntimeErrPattern)
|
||||
} else if matched, regexErr := regexp.MatchString(expectRuntimeErrPattern, err.Error()); regexErr != nil {
|
||||
t.Fatalf("failed to compile expected err regex: %v", regexErr)
|
||||
} else if !matched {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
} else if expectResult != nil {
|
||||
converted := res.Equal(expectResult).Value().(bool)
|
||||
require.True(t, converted, "expectation not equal to output")
|
||||
} else {
|
||||
t.Fatal("expected result must not be nil")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestQuantity(t *testing.T) {
|
||||
twelveMi := resource.MustParse("12Mi")
|
||||
trueVal := types.Bool(true)
|
||||
falseVal := types.Bool(false)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
expr string
|
||||
expectValue ref.Val
|
||||
expectedCompileErr []string
|
||||
expectedRuntimeErr string
|
||||
}{
|
||||
{
|
||||
name: "parse",
|
||||
expr: `quantity("12Mi")`,
|
||||
expectValue: apiservercel.Quantity{Quantity: &twelveMi},
|
||||
},
|
||||
{
|
||||
name: "parseInvalidSuffix",
|
||||
expr: `quantity("10Mo")`,
|
||||
expectedRuntimeErr: "quantities must match the regular expression.*",
|
||||
},
|
||||
{
|
||||
// The above case fails due to a regex check. This case passes the
|
||||
// regex check and fails a suffix check
|
||||
name: "parseInvalidSuffixPassesRegex",
|
||||
expr: `quantity("10Mm")`,
|
||||
expectedRuntimeErr: "unable to parse quantity's suffix",
|
||||
},
|
||||
{
|
||||
name: "isQuantity",
|
||||
expr: `isQuantity("20")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "isQuantity_megabytes",
|
||||
expr: `isQuantity("20M")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "isQuantity_mebibytes",
|
||||
expr: `isQuantity("20Mi")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "isQuantity_invalidSuffix",
|
||||
expr: `isQuantity("20Mo")`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "isQuantity_passingRegex",
|
||||
expr: `isQuantity("10Mm")`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "isQuantity_noOverload",
|
||||
expr: `isQuantity([1, 2, 3])`,
|
||||
expectedCompileErr: []string{"found no matching overload for 'isQuantity' applied to.*"},
|
||||
},
|
||||
{
|
||||
name: "equality_reflexivity",
|
||||
expr: `quantity("200M") == quantity("200M")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "equality_symmetry",
|
||||
expr: `quantity("200M") == quantity("0.2G") && quantity("0.2G") == quantity("200M")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "equality_transitivity",
|
||||
expr: `quantity("2M") == quantity("0.002G") && quantity("2000k") == quantity("2M") && quantity("0.002G") == quantity("2000k")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "inequality",
|
||||
expr: `quantity("200M") == quantity("0.3G")`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "quantity_less",
|
||||
expr: `quantity("50M").isLessThan(quantity("50Mi"))`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "quantity_less_obvious",
|
||||
expr: `quantity("50M").isLessThan(quantity("100M"))`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "quantity_less_false",
|
||||
expr: `quantity("100M").isLessThan(quantity("50M"))`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "quantity_greater",
|
||||
expr: `quantity("50Mi").isGreaterThan(quantity("50M"))`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "quantity_greater_obvious",
|
||||
expr: `quantity("150Mi").isGreaterThan(quantity("100Mi"))`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "quantity_greater_false",
|
||||
expr: `quantity("50M").isGreaterThan(quantity("100M"))`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "compare_equal",
|
||||
expr: `quantity("200M").compareTo(quantity("0.2G"))`,
|
||||
expectValue: types.Int(0),
|
||||
},
|
||||
{
|
||||
name: "compare_less",
|
||||
expr: `quantity("50M").compareTo(quantity("50Mi"))`,
|
||||
expectValue: types.Int(-1),
|
||||
},
|
||||
{
|
||||
name: "compare_greater",
|
||||
expr: `quantity("50Mi").compareTo(quantity("50M"))`,
|
||||
expectValue: types.Int(1),
|
||||
},
|
||||
{
|
||||
name: "add_quantity",
|
||||
expr: `quantity("50k").add(quantity("20")) == quantity("50.02k")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "add_int",
|
||||
expr: `quantity("50k").add(20).isLessThan(quantity("50020"))`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "sub_quantity",
|
||||
expr: `quantity("50k").sub(quantity("20")) == quantity("49.98k")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "sub_int",
|
||||
expr: `quantity("50k").sub(20) == quantity("49980")`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "arith_chain_1",
|
||||
expr: `quantity("50k").add(20).sub(quantity("100k")).asInteger()`,
|
||||
expectValue: types.Int(-49980),
|
||||
},
|
||||
{
|
||||
name: "arith_chain",
|
||||
expr: `quantity("50k").add(20).sub(quantity("100k")).sub(-50000).asInteger()`,
|
||||
expectValue: types.Int(20),
|
||||
},
|
||||
{
|
||||
name: "as_integer",
|
||||
expr: `quantity("50k").asInteger()`,
|
||||
expectValue: types.Int(50000),
|
||||
},
|
||||
{
|
||||
name: "as_integer_error",
|
||||
expr: `quantity("9999999999999999999999999999999999999G").asInteger()`,
|
||||
expectedRuntimeErr: `cannot convert value to integer`,
|
||||
},
|
||||
{
|
||||
name: "is_integer",
|
||||
expr: `quantity("9999999999999999999999999999999999999G").isInteger()`,
|
||||
expectValue: falseVal,
|
||||
},
|
||||
{
|
||||
name: "is_integer",
|
||||
expr: `quantity("50").isInteger()`,
|
||||
expectValue: trueVal,
|
||||
},
|
||||
{
|
||||
name: "as_float",
|
||||
expr: `quantity("50.703k").asApproximateFloat()`,
|
||||
expectValue: types.Double(50703),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
testQuantity(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr)
|
||||
})
|
||||
}
|
||||
}
|
76
staging/src/k8s.io/apiserver/pkg/cel/quantity.go
Normal file
76
staging/src/k8s.io/apiserver/pkg/cel/quantity.go
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
Copyright 2023 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"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
|
||||
var (
|
||||
QuantityObject = decls.NewObjectType("kubernetes.Quantity")
|
||||
quantityTypeValue = types.NewTypeValue("kubernetes.Quantity")
|
||||
QuantityType = cel.ObjectType("kubernetes.Quantity")
|
||||
)
|
||||
|
||||
// Quantity provdes a CEL representation of a resource.Quantity
|
||||
type Quantity struct {
|
||||
*resource.Quantity
|
||||
}
|
||||
|
||||
func (d Quantity) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
|
||||
if reflect.TypeOf(d.Quantity).AssignableTo(typeDesc) {
|
||||
return d.Quantity, nil
|
||||
}
|
||||
if reflect.TypeOf("").AssignableTo(typeDesc) {
|
||||
return d.Quantity.String(), nil
|
||||
}
|
||||
return nil, fmt.Errorf("type conversion error from 'Quantity' to '%v'", typeDesc)
|
||||
}
|
||||
|
||||
func (d Quantity) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
switch typeVal {
|
||||
case typeValue:
|
||||
return d
|
||||
case types.TypeType:
|
||||
return quantityTypeValue
|
||||
default:
|
||||
return types.NewErr("type conversion error from '%s' to '%s'", quantityTypeValue, typeVal)
|
||||
}
|
||||
}
|
||||
|
||||
func (d Quantity) Equal(other ref.Val) ref.Val {
|
||||
otherDur, ok := other.(Quantity)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(other)
|
||||
}
|
||||
return types.Bool(d.Quantity.Equal(*otherDur.Quantity))
|
||||
}
|
||||
|
||||
func (d Quantity) Type() ref.Type {
|
||||
return quantityTypeValue
|
||||
}
|
||||
|
||||
func (d Quantity) Value() interface{} {
|
||||
return d.Quantity
|
||||
}
|
Loading…
Reference in New Issue
Block a user