k8s.io/apiserver@v0.31.1/pkg/cel/library/quantity.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package library
    18  
    19  import (
    20  	"errors"
    21  
    22  	"github.com/google/cel-go/cel"
    23  	"github.com/google/cel-go/common/types"
    24  	"github.com/google/cel-go/common/types/ref"
    25  
    26  	"k8s.io/apimachinery/pkg/api/resource"
    27  	apiservercel "k8s.io/apiserver/pkg/cel"
    28  )
    29  
    30  // Quantity provides a CEL function library extension of Kubernetes
    31  // resource.Quantity parsing functions. See `resource.Quantity`
    32  // documentation for more detailed information about the format itself:
    33  // https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity
    34  //
    35  // quantity
    36  //
    37  // Converts a string to a Quantity or results in an error if the string is not a valid Quantity. Refer
    38  // to resource.Quantity documentation for information on accepted patterns.
    39  //
    40  //	quantity(<string>) <Quantity>
    41  //
    42  // Examples:
    43  //
    44  //	quantity('1.5G') // returns a Quantity
    45  //	quantity('200k') // returns a Quantity
    46  //	quantity('200K') // error
    47  //	quantity('Three') // error
    48  //	quantity('Mi') // error
    49  //
    50  // isQuantity
    51  //
    52  // Returns true if a string is a valid Quantity. isQuantity returns true if and
    53  // only if quantity does not result in error.
    54  //
    55  //	isQuantity( <string>) <bool>
    56  //
    57  // Examples:
    58  //
    59  //	isQuantity('1.3G') // returns true
    60  //	isQuantity('1.3Gi') // returns true
    61  //	isQuantity('1,3G') // returns false
    62  //	isQuantity('10000k') // returns true
    63  //	isQuantity('200K') // returns false
    64  //	isQuantity('Three') // returns false
    65  //	isQuantity('Mi') // returns false
    66  //
    67  // Conversion to Scalars:
    68  //
    69  //   - isInteger: returns true if and only if asInteger is safe to call without an error
    70  //
    71  //   - asInteger: returns a representation of the current value as an int64 if
    72  //     possible or results in an error if conversion would result in overflow
    73  //	   or loss of precision.
    74  //
    75  //   - asApproximateFloat: returns a float64 representation of the quantity which may
    76  //     lose precision. If the value of the quantity is outside the range of a float64
    77  //     +Inf/-Inf will be returned.
    78  //
    79  //     <Quantity>.isInteger() <bool>
    80  //     <Quantity>.asInteger() <int>
    81  //     <Quantity>.asApproximateFloat() <float>
    82  //
    83  // Examples:
    84  //
    85  // quantity("50000000G").isInteger() // returns true
    86  // quantity("50k").isInteger() // returns true
    87  // quantity("9999999999999999999999999999999999999G").asInteger() // error: cannot convert value to integer
    88  // quantity("9999999999999999999999999999999999999G").isInteger() // returns false
    89  // quantity("50k").asInteger() == 50000 // returns true
    90  // quantity("50k").sub(20000).asApproximateFloat() == 30000 // returns true
    91  //
    92  // Arithmetic
    93  //
    94  //   - sign: Returns `1` if the quantity is positive, `-1` if it is negative. `0` if it is zero
    95  //
    96  //   - add: Returns sum of two quantities or a quantity and an integer
    97  //
    98  //   - sub: Returns difference between two quantities or a quantity and an integer
    99  //
   100  //     <Quantity>.sign() <int>
   101  //     <Quantity>.add(<quantity>) <quantity>
   102  //     <Quantity>.add(<integer>) <quantity>
   103  //     <Quantity>.sub(<quantity>) <quantity>
   104  //     <Quantity>.sub(<integer>) <quantity>
   105  //
   106  // Examples:
   107  //
   108  // quantity("50k").add("20k") == quantity("70k") // returns true
   109  // quantity("50k").add(20) == quantity("50020") // returns true
   110  // quantity("50k").sub("20k") == quantity("30k") // returns true
   111  // quantity("50k").sub(20000) == quantity("30k") // returns true
   112  // quantity("50k").add(20).sub(quantity("100k")).sub(-50000) == quantity("20") // returns true
   113  //
   114  // Comparisons
   115  //
   116  //   - isGreaterThan: Returns true if and only if the receiver is greater than the operand
   117  //
   118  //   - isLessThan: Returns true if and only if the receiver is less than the operand
   119  //
   120  //   - 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
   121  //
   122  //
   123  //     <Quantity>.isLessThan(<quantity>) <bool>
   124  //     <Quantity>.isGreaterThan(<quantity>) <bool>
   125  //     <Quantity>.compareTo(<quantity>) <int>
   126  //
   127  // Examples:
   128  //
   129  // quantity("200M").compareTo(quantity("0.2G")) // returns 0
   130  // quantity("50M").compareTo(quantity("50Mi")) // returns -1
   131  // quantity("50Mi").compareTo(quantity("50M")) // returns 1
   132  // quantity("150Mi").isGreaterThan(quantity("100Mi")) // returns true
   133  // quantity("50Mi").isGreaterThan(quantity("100Mi")) // returns false
   134  // quantity("50M").isLessThan(quantity("100M")) // returns true
   135  // quantity("100M").isLessThan(quantity("50M")) // returns false
   136  
   137  func Quantity() cel.EnvOption {
   138  	return cel.Lib(quantityLib)
   139  }
   140  
   141  var quantityLib = &quantity{}
   142  
   143  type quantity struct{}
   144  
   145  func (*quantity) LibraryName() string {
   146  	return "k8s.quantity"
   147  }
   148  
   149  var quantityLibraryDecls = map[string][]cel.FunctionOpt{
   150  	"quantity": {
   151  		cel.Overload("string_to_quantity", []*cel.Type{cel.StringType}, apiservercel.QuantityType, cel.UnaryBinding((stringToQuantity))),
   152  	},
   153  	"isQuantity": {
   154  		cel.Overload("is_quantity_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(isQuantity)),
   155  	},
   156  	"sign": {
   157  		cel.Overload("quantity_sign", []*cel.Type{apiservercel.QuantityType}, cel.IntType, cel.UnaryBinding(quantityGetSign)),
   158  	},
   159  	"isGreaterThan": {
   160  		cel.MemberOverload("quantity_is_greater_than", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, cel.BoolType, cel.BinaryBinding(quantityIsGreaterThan)),
   161  	},
   162  	"isLessThan": {
   163  		cel.MemberOverload("quantity_is_less_than", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, cel.BoolType, cel.BinaryBinding(quantityIsLessThan)),
   164  	},
   165  	"compareTo": {
   166  		cel.MemberOverload("quantity_compare_to", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, cel.IntType, cel.BinaryBinding(quantityCompareTo)),
   167  	},
   168  	"asApproximateFloat": {
   169  		cel.MemberOverload("quantity_get_float", []*cel.Type{apiservercel.QuantityType}, cel.DoubleType, cel.UnaryBinding(quantityGetApproximateFloat)),
   170  	},
   171  	"asInteger": {
   172  		cel.MemberOverload("quantity_get_int", []*cel.Type{apiservercel.QuantityType}, cel.IntType, cel.UnaryBinding(quantityGetValue)),
   173  	},
   174  	"isInteger": {
   175  		cel.MemberOverload("quantity_is_integer", []*cel.Type{apiservercel.QuantityType}, cel.BoolType, cel.UnaryBinding(quantityCanValue)),
   176  	},
   177  	"add": {
   178  		cel.MemberOverload("quantity_add", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, apiservercel.QuantityType, cel.BinaryBinding(quantityAdd)),
   179  		cel.MemberOverload("quantity_add_int", []*cel.Type{apiservercel.QuantityType, cel.IntType}, apiservercel.QuantityType, cel.BinaryBinding(quantityAddInt)),
   180  	},
   181  	"sub": {
   182  		cel.MemberOverload("quantity_sub", []*cel.Type{apiservercel.QuantityType, apiservercel.QuantityType}, apiservercel.QuantityType, cel.BinaryBinding(quantitySub)),
   183  		cel.MemberOverload("quantity_sub_int", []*cel.Type{apiservercel.QuantityType, cel.IntType}, apiservercel.QuantityType, cel.BinaryBinding(quantitySubInt)),
   184  	},
   185  }
   186  
   187  func (*quantity) CompileOptions() []cel.EnvOption {
   188  	options := make([]cel.EnvOption, 0, len(quantityLibraryDecls))
   189  	for name, overloads := range quantityLibraryDecls {
   190  		options = append(options, cel.Function(name, overloads...))
   191  	}
   192  	return options
   193  }
   194  
   195  func (*quantity) ProgramOptions() []cel.ProgramOption {
   196  	return []cel.ProgramOption{}
   197  }
   198  
   199  func isQuantity(arg ref.Val) ref.Val {
   200  	str, ok := arg.Value().(string)
   201  	if !ok {
   202  		return types.MaybeNoSuchOverloadErr(arg)
   203  	}
   204  
   205  	_, err := resource.ParseQuantity(str)
   206  	if err != nil {
   207  		return types.Bool(false)
   208  	}
   209  
   210  	return types.Bool(true)
   211  }
   212  
   213  func stringToQuantity(arg ref.Val) ref.Val {
   214  	str, ok := arg.Value().(string)
   215  	if !ok {
   216  		return types.MaybeNoSuchOverloadErr(arg)
   217  	}
   218  
   219  	q, err := resource.ParseQuantity(str)
   220  	if err != nil {
   221  		return types.WrapErr(err)
   222  	}
   223  
   224  	return apiservercel.Quantity{Quantity: &q}
   225  }
   226  
   227  func quantityGetApproximateFloat(arg ref.Val) ref.Val {
   228  	q, ok := arg.Value().(*resource.Quantity)
   229  	if !ok {
   230  		return types.MaybeNoSuchOverloadErr(arg)
   231  	}
   232  	return types.Double(q.AsApproximateFloat64())
   233  }
   234  
   235  func quantityCanValue(arg ref.Val) ref.Val {
   236  	q, ok := arg.Value().(*resource.Quantity)
   237  	if !ok {
   238  		return types.MaybeNoSuchOverloadErr(arg)
   239  	}
   240  	_, success := q.AsInt64()
   241  	return types.Bool(success)
   242  }
   243  
   244  func quantityGetValue(arg ref.Val) ref.Val {
   245  	q, ok := arg.Value().(*resource.Quantity)
   246  	if !ok {
   247  		return types.MaybeNoSuchOverloadErr(arg)
   248  	}
   249  	v, success := q.AsInt64()
   250  	if !success {
   251  		return types.WrapErr(errors.New("cannot convert value to integer"))
   252  	}
   253  	return types.Int(v)
   254  }
   255  
   256  func quantityGetSign(arg ref.Val) ref.Val {
   257  	q, ok := arg.Value().(*resource.Quantity)
   258  	if !ok {
   259  		return types.MaybeNoSuchOverloadErr(arg)
   260  	}
   261  	return types.Int(q.Sign())
   262  }
   263  
   264  func quantityIsGreaterThan(arg ref.Val, other ref.Val) ref.Val {
   265  	q, ok := arg.Value().(*resource.Quantity)
   266  	if !ok {
   267  		return types.MaybeNoSuchOverloadErr(arg)
   268  	}
   269  
   270  	q2, ok := other.Value().(*resource.Quantity)
   271  	if !ok {
   272  		return types.MaybeNoSuchOverloadErr(arg)
   273  	}
   274  
   275  	return types.Bool(q.Cmp(*q2) == 1)
   276  }
   277  
   278  func quantityIsLessThan(arg ref.Val, other ref.Val) ref.Val {
   279  	q, ok := arg.Value().(*resource.Quantity)
   280  	if !ok {
   281  		return types.MaybeNoSuchOverloadErr(arg)
   282  	}
   283  
   284  	q2, ok := other.Value().(*resource.Quantity)
   285  	if !ok {
   286  		return types.MaybeNoSuchOverloadErr(arg)
   287  	}
   288  
   289  	return types.Bool(q.Cmp(*q2) == -1)
   290  }
   291  
   292  func quantityCompareTo(arg ref.Val, other ref.Val) ref.Val {
   293  	q, ok := arg.Value().(*resource.Quantity)
   294  	if !ok {
   295  		return types.MaybeNoSuchOverloadErr(arg)
   296  	}
   297  
   298  	q2, ok := other.Value().(*resource.Quantity)
   299  	if !ok {
   300  		return types.MaybeNoSuchOverloadErr(arg)
   301  	}
   302  
   303  	return types.Int(q.Cmp(*q2))
   304  }
   305  
   306  func quantityAdd(arg ref.Val, other ref.Val) ref.Val {
   307  	q, ok := arg.Value().(*resource.Quantity)
   308  	if !ok {
   309  		return types.MaybeNoSuchOverloadErr(arg)
   310  	}
   311  
   312  	q2, ok := other.Value().(*resource.Quantity)
   313  	if !ok {
   314  		return types.MaybeNoSuchOverloadErr(arg)
   315  	}
   316  
   317  	copy := *q
   318  	copy.Add(*q2)
   319  	return &apiservercel.Quantity{
   320  		Quantity: &copy,
   321  	}
   322  }
   323  
   324  func quantityAddInt(arg ref.Val, other ref.Val) ref.Val {
   325  	q, ok := arg.Value().(*resource.Quantity)
   326  	if !ok {
   327  		return types.MaybeNoSuchOverloadErr(arg)
   328  	}
   329  
   330  	q2, ok := other.Value().(int64)
   331  	if !ok {
   332  		return types.MaybeNoSuchOverloadErr(arg)
   333  	}
   334  
   335  	q2Converted := *resource.NewQuantity(q2, resource.DecimalExponent)
   336  
   337  	copy := *q
   338  	copy.Add(q2Converted)
   339  	return &apiservercel.Quantity{
   340  		Quantity: &copy,
   341  	}
   342  }
   343  
   344  func quantitySub(arg ref.Val, other ref.Val) ref.Val {
   345  	q, ok := arg.Value().(*resource.Quantity)
   346  	if !ok {
   347  		return types.MaybeNoSuchOverloadErr(arg)
   348  	}
   349  
   350  	q2, ok := other.Value().(*resource.Quantity)
   351  	if !ok {
   352  		return types.MaybeNoSuchOverloadErr(arg)
   353  	}
   354  
   355  	copy := *q
   356  	copy.Sub(*q2)
   357  	return &apiservercel.Quantity{
   358  		Quantity: &copy,
   359  	}
   360  }
   361  
   362  func quantitySubInt(arg ref.Val, other ref.Val) ref.Val {
   363  	q, ok := arg.Value().(*resource.Quantity)
   364  	if !ok {
   365  		return types.MaybeNoSuchOverloadErr(arg)
   366  	}
   367  
   368  	q2, ok := other.Value().(int64)
   369  	if !ok {
   370  		return types.MaybeNoSuchOverloadErr(arg)
   371  	}
   372  
   373  	q2Converted := *resource.NewQuantity(q2, resource.DecimalExponent)
   374  
   375  	copy := *q
   376  	copy.Sub(q2Converted)
   377  	return &apiservercel.Quantity{
   378  		Quantity: &copy,
   379  	}
   380  }