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: ©, 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: ©, 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: ©, 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: ©, 379 } 380 }