From f872d1b6eeed318b88362e8714aa8eaa2e46f47a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:48:51 +0000 Subject: [PATCH] chore(deps): bump github.com/tektoncd/triggers from 0.36.0 to 0.37.0 Bumps [github.com/tektoncd/triggers](https://github.com/tektoncd/triggers) from 0.36.0 to 0.37.0. - [Release notes](https://github.com/tektoncd/triggers/releases) - [Changelog](https://github.com/tektoncd/triggers/blob/main/releases.md) - [Commits](https://github.com/tektoncd/triggers/compare/v0.36.0...v0.37.0) --- updated-dependencies: - dependency-name: github.com/tektoncd/triggers dependency-version: 0.37.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 +- go.sum | 8 +- .../github.com/google/cel-go/cel/BUILD.bazel | 3 + .../google/cel-go/cel/async/BUILD.bazel | 35 ++ .../google/cel-go/cel/async/async.go | 235 ++++++++ vendor/github.com/google/cel-go/cel/cel.go | 21 + vendor/github.com/google/cel-go/cel/decls.go | 26 + vendor/github.com/google/cel-go/cel/env.go | 36 ++ .../github.com/google/cel-go/cel/folding.go | 184 +++++- vendor/github.com/google/cel-go/cel/io.go | 28 +- .../github.com/google/cel-go/cel/options.go | 72 +++ .../github.com/google/cel-go/cel/program.go | 242 ++++++++ .../github.com/google/cel-go/cel/validator.go | 123 +++- .../google/cel-go/common/ast/ast.go | 8 + .../google/cel-go/common/ast/navigable.go | 23 + .../google/cel-go/common/decls/decls.go | 92 ++- .../google/cel-go/common/stdlib/standard.go | 18 + .../google/cel-go/common/types/bytes.go | 5 +- .../google/cel-go/common/types/string.go | 6 +- .../google/cel-go/common/types/timestamp.go | 75 +++ .../google/cel-go/common/types/unknown.go | 5 +- vendor/github.com/google/cel-go/ext/README.md | 8 + .../github.com/google/cel-go/ext/encoders.go | 96 +++- vendor/github.com/google/cel-go/ext/lists.go | 286 ++++++++-- vendor/github.com/google/cel-go/ext/math.go | 61 +- vendor/github.com/google/cel-go/ext/native.go | 23 +- .../github.com/google/cel-go/ext/network.go | 203 ++++++- .../google/cel-go/interpreter/BUILD.bazel | 3 + .../google/cel-go/interpreter/activation.go | 13 + .../google/cel-go/interpreter/async.go | 530 ++++++++++++++++++ .../cel-go/interpreter/attribute_patterns.go | 3 + .../google/cel-go/interpreter/frame.go | 136 ++++- .../cel-go/interpreter/interpretable.go | 126 +++-- .../google/cel-go/interpreter/interpreter.go | 9 + .../google/cel-go/interpreter/planner.go | 22 + .../google/cel-go/interpreter/runtimecost.go | 46 +- .../github.com/google/cel-go/parser/helper.go | 4 + .../google/cel-go/parser/options.go | 13 + .../github.com/google/cel-go/parser/parser.go | 18 + .../tektoncd/triggers/test/e2e-common.sh | 4 +- vendor/modules.txt | 7 +- 41 files changed, 2687 insertions(+), 173 deletions(-) create mode 100644 vendor/github.com/google/cel-go/cel/async/BUILD.bazel create mode 100644 vendor/github.com/google/cel-go/cel/async/async.go create mode 100644 vendor/github.com/google/cel-go/interpreter/async.go diff --git a/go.mod b/go.mod index 4dbf1ded5d..e7f7758c54 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/stretchr/testify v1.12.0 github.com/tektoncd/pipeline v1.15.0 github.com/tektoncd/plumbing v0.0.0-20250430145243-3b7cd59879c1 - github.com/tektoncd/triggers v0.36.0 + github.com/tektoncd/triggers v0.37.0 github.com/theupdateframework/go-tuf v0.7.0 github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 go.opentelemetry.io/otel v1.45.0 @@ -103,7 +103,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.29.2 // indirect + github.com/google/cel-go v0.30.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/go.sum b/go.sum index b0167081b6..6a53516a56 100644 --- a/go.sum +++ b/go.sum @@ -218,8 +218,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= -github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.30.0 h1:ll54AkzKunWkBn9wSoiUXbFZXYZTkdJGNXTBXUoolGo= +github.com/google/cel-go v0.30.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -458,8 +458,8 @@ github.com/tektoncd/pipeline v1.15.0 h1:ZGboFUaEdpYurZWqeGT5rn9e6qmomGL/gLSKhHp2 github.com/tektoncd/pipeline v1.15.0/go.mod h1:HghtkZ1D9b91o0YiXABDUd+c2C6R4kbk+PumnHqs3Bk= github.com/tektoncd/plumbing v0.0.0-20250430145243-3b7cd59879c1 h1:nv7BsOAZ1ifQX9Lw1hYFo1f7e62dTDyyVPJBuljgZKw= github.com/tektoncd/plumbing v0.0.0-20250430145243-3b7cd59879c1/go.mod h1:eDs4O8vTNkyKZ/+AEuo4nYDfpyn1AzbgIcQ1QMQaKJk= -github.com/tektoncd/triggers v0.36.0 h1:nyNkMN3L+TvpRHQXDIXc8hqicj494Xx9Dv2+8wgjxe8= -github.com/tektoncd/triggers v0.36.0/go.mod h1:O3kGLFBWDgjgvlGNdGWcrV2rVoRvgousDKhTwCSeakU= +github.com/tektoncd/triggers v0.37.0 h1:xXljDsLAtV2rvXXp9yF4MQwWDJiRs/aUcTtApx1uBBw= +github.com/tektoncd/triggers v0.37.0/go.mod h1:vvP2wD1xxFkWRAGifM35d4r/rh9hX9bvI0VomPAQ+bs= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= diff --git a/vendor/github.com/google/cel-go/cel/BUILD.bazel b/vendor/github.com/google/cel-go/cel/BUILD.bazel index 46cb26d625..62a56036a0 100644 --- a/vendor/github.com/google/cel-go/cel/BUILD.bazel +++ b/vendor/github.com/google/cel-go/cel/BUILD.bazel @@ -26,6 +26,7 @@ go_library( importpath = "github.com/google/cel-go/cel", visibility = ["//visibility:public"], deps = [ + "//cel/async:go_default_library", "//checker:go_default_library", "//checker/decls:go_default_library", "//common:go_default_library", @@ -70,6 +71,7 @@ go_test( "inlining_test.go", "io_test.go", "optimizer_test.go", + "program_async_test.go", "prompt_test.go", "validator_test.go", ], @@ -84,6 +86,7 @@ go_test( "//cel/testdata:test_fds_with_source_info", ], deps = [ + "//cel/async:go_default_library", "//common/operators:go_default_library", "//common/overloads:go_default_library", "//common/types:go_default_library", diff --git a/vendor/github.com/google/cel-go/cel/async/BUILD.bazel b/vendor/github.com/google/cel-go/cel/async/BUILD.bazel new file mode 100644 index 0000000000..85b28bcdb5 --- /dev/null +++ b/vendor/github.com/google/cel-go/cel/async/BUILD.bazel @@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package( + licenses = ["notice"], # Apache 2.0 +) + +go_library( + name = "go_default_library", + srcs = [ + "async.go", + ], + importpath = "github.com/google/cel-go/cel/async", + visibility = ["//visibility:public"], + deps = [ + "//common/decls:go_default_library", + "//common/functions:go_default_library", + "//common/types:go_default_library", + "//common/types/ref:go_default_library", + "//interpreter:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "async_test.go", + ], + deps = [ + ":go_default_library", + "//common/decls:go_default_library", + "//common/functions:go_default_library", + "//common/types:go_default_library", + "//common/types/ref:go_default_library", + ], +) diff --git a/vendor/github.com/google/cel-go/cel/async/async.go b/vendor/github.com/google/cel-go/cel/async/async.go new file mode 100644 index 0000000000..a011114bdc --- /dev/null +++ b/vendor/github.com/google/cel-go/cel/async/async.go @@ -0,0 +1,235 @@ +// Copyright 2026 Google LLC +// +// 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 async provides helpers for configuring and executing asynchronous CEL functions, +// including drain strategies, retry, timeout, concurrency limiting, and caching wrappers. +package async + +import ( + "context" + "errors" + "time" + + "github.com/google/cel-go/common/decls" + "github.com/google/cel-go/common/functions" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/interpreter" +) + +// Call describes a pending or completed asynchronous function call. +// This interface exposes a safe, read-only view of the internal interpreter state. +type Call = interpreter.AsyncCall + +// Observer provides callbacks for monitoring the lifecycle of asynchronous function calls. +// +// Implementations must be safe for concurrent use: the start and finish callbacks run on different +// goroutines, and finish callbacks for distinct calls may run concurrently. See +// interpreter.AsyncObserver for details. +type Observer = interpreter.AsyncObserver + +// BlockingOp is a blocking asynchronous function operation. +type BlockingOp = functions.BlockingAsyncOp + +// DrainAction dictates what ConcurrentEval should do after inspecting completions. +type DrainAction struct { + // Reevaluate indicates that the AST should be re-evaluated immediately. + // If true, WaitDuration is ignored. + Reevaluate bool + // WaitDuration indicates how long the evaluator should wait for additional + // completions before deciding to re-evaluate. A duration of 0 means wait + // indefinitely (block on the next completion). + WaitDuration time.Duration +} + +// DrainStrategy controls when ConcurrentEval re-evaluates after async completions. +// +// The evaluator consults the strategy each time a completion is received. +type DrainStrategy interface { + // NextAction evaluates the current state of asynchronous evaluation and + // determines the next step. + // + // - completed: The set of completions accumulated in the current batch. + // - active: The number of async calls currently launched but unresolved. + NextAction(completed []Call, active int) DrainAction +} + +// DrainNone returns a strategy that re-evaluates after every single completion. +// This is the default strategy. +func DrainNone() DrainStrategy { + return drainNone{} +} + +type drainNone struct{} + +func (drainNone) NextAction(completed []Call, active int) DrainAction { + return DrainAction{Reevaluate: active == 0 || len(completed) > 0} +} + +// DrainReady returns a strategy that waits for a short duration after the first +// completion to batch any other functions that complete at roughly the same time. +func DrainReady(debounce time.Duration) DrainStrategy { + return drainReady{debounce: debounce} +} + +type drainReady struct { + debounce time.Duration +} + +func (d drainReady) NextAction(completed []Call, active int) DrainAction { + if active == 0 { + return DrainAction{Reevaluate: true} // Nothing left to wait for + } + if len(completed) == 0 { + return DrainAction{Reevaluate: false, WaitDuration: 0} // Wait indefinitely for first + } + return DrainAction{Reevaluate: false, WaitDuration: d.debounce} // Wait for debounce period +} + +// DrainAll returns a strategy that waits for all currently pending calls to +// complete before re-evaluating. +// +// Note: This strategy is optimal for independent async calls, but will over-wait +// if some calls depend on the results of others. +func DrainAll() DrainStrategy { + return drainAll{} +} + +type drainAll struct{} + +func (drainAll) NextAction(completed []Call, active int) DrainAction { + return DrainAction{Reevaluate: active == 0} +} + +// Timeout wraps a BlockingAsyncOp with a per-call timeout. +// +// The timeout is enforced even when the wrapped function ignores its context: the function runs on +// its own goroutine and Timeout selects on the deadline, returning a timeout error when it +// fires. A function that ignores cancellation cannot be forcibly stopped (Go cannot kill a +// goroutine), so its goroutine continues running in the background until it returns on its own; +// only its result is abandoned. This is the recommended way to bound functions that may hang or +// are not under the caller's control. The extra goroutine is incurred only by Timeout-wrapped +// calls, not by async evaluation in general. +func Timeout(fn functions.BlockingAsyncOp, timeout time.Duration) functions.BlockingAsyncOp { + return func(ctx context.Context, args ...ref.Val) ref.Val { + tCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + resCh := make(chan ref.Val, 1) + go func() { resCh <- fn(tCtx, args...) }() + select { + case res := <-resCh: + return res + case <-tCtx.Done(): + return types.NewErr("operation timed out after %v: %v", timeout, tCtx.Err()) + } + } +} + +// TimeoutBinding wraps a BlockingAsyncOp with a per-call timeout and returns an OverloadOpt. +func TimeoutBinding(fn functions.BlockingAsyncOp, timeout time.Duration) decls.OverloadOpt { + return decls.AsyncBinding(Timeout(fn, timeout)) +} + +// RetryOption configures the behavior of RetryBinding. +type RetryOption func(*retryConfig) + +type retryConfig struct { + maxAttempts int + backoff time.Duration +} + +// RetryAttempts sets the maximum number of attempts (including the first one). +func RetryAttempts(attempts int) RetryOption { + return func(c *retryConfig) { + c.maxAttempts = attempts + } +} + +// RetryBackoff sets the fixed backoff duration between attempts. +func RetryBackoff(backoff time.Duration) RetryOption { + return func(c *retryConfig) { + c.backoff = backoff + } +} + +// RetryableError is an interface that errors can implement to signal whether they are retryable. +type RetryableError interface { + error + IsRetryable() bool +} + +// Retry wraps a BlockingAsyncOp with a retry policy. +// It will retry the operation if it returns a types.Err that wraps a RetryableError returning true for IsRetryable. +func Retry(fn functions.BlockingAsyncOp, opts ...RetryOption) functions.BlockingAsyncOp { + config := &retryConfig{ + maxAttempts: 3, + backoff: 100 * time.Millisecond, + } + for _, opt := range opts { + opt(config) + } + + return func(ctx context.Context, args ...ref.Val) ref.Val { + var lastErr ref.Val + var backoff *time.Timer + defer func() { + if backoff != nil { + backoff.Stop() + } + }() + for i := 0; i < config.maxAttempts; i++ { + if i > 0 { + // Reuse a single timer across attempts and stop it on cancellation so the + // pending timer is not left to fire after the call returns. + if backoff == nil { + backoff = time.NewTimer(config.backoff) + } else { + backoff.Reset(config.backoff) + } + select { + case <-backoff.C: + case <-ctx.Done(): + backoff.Stop() + return types.NewErr("operation cancelled during retry: %v", ctx.Err()) + } + } + + res := fn(ctx, args...) + if !types.IsError(res) { + return res + } + + err := res.(*types.Err) + lastErr = res + + if !isRetryable(err) { + return res + } + } + return lastErr + } +} + +// RetryBinding wraps a BlockingAsyncOp with a retry policy and returns an OverloadOpt. +func RetryBinding(fn functions.BlockingAsyncOp, opts ...RetryOption) decls.OverloadOpt { + return decls.AsyncBinding(Retry(fn, opts...)) +} + +func isRetryable(err *types.Err) bool { + var re RetryableError + if errors.As(err, &re) { + return re.IsRetryable() + } + return false +} diff --git a/vendor/github.com/google/cel-go/cel/cel.go b/vendor/github.com/google/cel-go/cel/cel.go index eb5a9f4cc5..9ba957a7e3 100644 --- a/vendor/github.com/google/cel-go/cel/cel.go +++ b/vendor/github.com/google/cel-go/cel/cel.go @@ -17,3 +17,24 @@ // CEL is a non-Turing complete expression language designed to parse, check, and evaluate // expressions against user-defined environments. package cel + +// Compile is a convenience function that constructs a new Env using the provided EnvOption values, +// compiles the expression string, and plans an executable Program. +// +// Warning: Creating a new environment for every compilation is expensive. Environment setup should be done once +// and shared across expression compilations when the options remain the same. +func Compile(expression string, opts ...EnvOption) (Program, error) { + env, err := NewEnv(opts...) + if err != nil { + return nil, err + } + ast, iss := env.Compile(expression) + if iss.Err() != nil { + return nil, iss.Err() + } + prg, err := env.Program(ast, EvalOptions(OptOptimize)) + if err != nil { + return nil, err + } + return prg, nil +} diff --git a/vendor/github.com/google/cel-go/cel/decls.go b/vendor/github.com/google/cel-go/cel/decls.go index 4d4873bd6b..c7c23fd51b 100644 --- a/vendor/github.com/google/cel-go/cel/decls.go +++ b/vendor/github.com/google/cel-go/cel/decls.go @@ -346,6 +346,32 @@ func LateFunctionBinding() OverloadOpt { return decls.LateFunctionBinding() } +// AsyncBinding provides the implementation of an asynchronous overload. The provided function +// is called in its own goroutine with the provided context. The function should block until +// the result is available, and the framework manages goroutine and channel lifecycle. +// +// This follows the same pattern used by gRPC-Go and other major Go frameworks where user +// code is synchronous and the framework manages concurrency. +// +// Context contract: the function MUST return promptly once its context is cancelled. The +// framework cannot forcibly terminate the goroutine running the function, so a function that +// ignores cancellation will leak its goroutine and hold a concurrency slot (see +// AsyncMaxConcurrency) until it returns on its own. For functions that may hang or that are not +// under your control, wrap them with async.TimeoutBinding to bound their runtime. +func AsyncBinding(fn functions.BlockingAsyncOp) OverloadOpt { + return decls.AsyncBinding(fn) +} + +// SingletonAsyncBinding creates a singleton async function definition from a blocking function, +// to be used with all function overloads. The provided function is called in its own goroutine +// with the provided context. +// +// Note, this approach works well if operand is expected to have a specific trait which it implements, +// e.g. traits.ContainerType. Otherwise, prefer per-overload async bindings. +func SingletonAsyncBinding(fn functions.BlockingAsyncOp, traits ...int) FunctionOpt { + return decls.SingletonAsyncBinding(fn, traits...) +} + // OverloadIsNonStrict enables the function to be called with error and unknown argument values. // // Note: do not use this option unless absoluately necessary as it should be an uncommon feature. diff --git a/vendor/github.com/google/cel-go/cel/env.go b/vendor/github.com/google/cel-go/cel/env.go index 7139e415ea..784790ba2c 100644 --- a/vendor/github.com/google/cel-go/cel/env.go +++ b/vendor/github.com/google/cel-go/cel/env.go @@ -48,6 +48,10 @@ type Source = common.Source type Ast struct { source Source impl *celast.AST + // loadErr captures an error detected while loading the AST (e.g. an over-deep AST ingested via + // ParsedExprToAst / CheckedExprToAst) so it can be surfaced when the Ast is checked or planned + // instead of recursing into the checker or planner on adversarially deep input. + loadErr error } // NativeRep converts the AST to a Go-native representation. @@ -395,6 +399,20 @@ func NewCustomEnv(opts ...EnvOption) (*Env, error) { // It is possible to have both non-nil Ast and Issues values returned from this call: however, // the mere presence of an Ast does not imply that it is valid for use. func (e *Env) Check(ast *Ast) (*Ast, *Issues) { + // Surface any error recorded while the Ast was loaded (e.g. an over-deep AST rejected by + // ParsedExprToAst / CheckedExprToAst) before recursing into the type checker on it. + if ast != nil && ast.loadErr != nil { + errs := common.NewErrors(ast.Source()) + errs.ReportErrorString(common.NoLocation, ast.loadErr.Error()) + return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo()) + } + if nodeLimit := e.configuredExpressionNodeLimit(); nodeLimit > 0 && ast != nil && ast.NativeRep() != nil { + if count := celast.NodeCount(ast.NativeRep()); count > nodeLimit { + errs := common.NewErrors(ast.Source()) + errs.ReportErrorString(common.NoLocation, fmt.Sprintf("expression node count exceeds limit: count %d, limit %d", count, nodeLimit)) + return nil, NewIssuesWithSourceInfo(errs, ast.NativeRep().SourceInfo()) + } + } // Construct the internal checker env, erroring if there is an issue adding the declarations. chk, err := e.initChecker() if err != nil { @@ -445,6 +463,15 @@ func (e *Env) configuredExpressionSizeLimit() int { return 100_000 } +// configuredExpressionNodeLimit returns the effective expression node limit. +// A zero value means "use default". +func (e *Env) configuredExpressionNodeLimit() int { + if l := e.limits[limitExpressionNodeCount]; l != 0 { + return l + } + return 100_000 +} + // Compile combines the Parse and Check phases CEL program compilation to produce an Ast and // associated issues. // @@ -687,6 +714,12 @@ func (e *Env) ParseSource(src Source) (*Ast, *Issues) { // Program generates an evaluable instance of the Ast within the environment (Env). func (e *Env) Program(ast *Ast, opts ...ProgramOption) (Program, error) { + // Surface any error recorded while the Ast was loaded (e.g. an over-deep AST rejected by + // ParsedExprToAst / CheckedExprToAst) rather than recursing into the planner on it. This is a + // cheap field read; the depth traversal itself runs once at conversion time, not here. + if ast != nil && ast.loadErr != nil { + return nil, ast.loadErr + } return e.PlanProgram(ast.NativeRep(), opts...) } @@ -859,6 +892,9 @@ func (e *Env) configure(opts []EnvOption) (*Env, error) { if l := e.limits[limitParseRecursionDepth]; l != 0 { prsrOpts = append(prsrOpts, parser.MaxRecursionDepth(l)) } + if l := e.limits[limitExpressionNodeCount]; l != 0 { + prsrOpts = append(prsrOpts, parser.MaxExpressionNodeCount(l)) + } e.prsr, err = parser.NewParser(prsrOpts...) if err != nil { return nil, err diff --git a/vendor/github.com/google/cel-go/cel/folding.go b/vendor/github.com/google/cel-go/cel/folding.go index 0cff9517a4..5525f0805e 100644 --- a/vendor/github.com/google/cel-go/cel/folding.go +++ b/vendor/github.com/google/cel-go/cel/folding.go @@ -15,6 +15,8 @@ package cel import ( + "context" + "errors" "fmt" "github.com/google/cel-go/common/ast" @@ -93,7 +95,7 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST) for _, fold := range foldableExprs { // If the expression could be folded because it's a non-strict call, and the // branches are pruned, continue to the next fold. - if fold.Kind() == ast.CallKind && maybePruneBranches(ctx, fold) { + if fold.Kind() == ast.CallKind && maybePruneBranches(ctx, a, fold) { continue } // Late-bound function calls cannot be folded. @@ -102,9 +104,9 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST) } // Otherwise, assume all context is needed to evaluate the expression. err := opt.tryFold(ctx, a, fold) - // Ignore errors for identifiers, since there is no guarantee that the environment + // Ignore errors for identifiers or subexpressions that cannot be folded, since there is no guarantee that the environment // has a value for them. - if err != nil && fold.Kind() != ast.IdentKind { + if err != nil && fold.Kind() != ast.IdentKind && !errors.Is(err, errCannotFold) { ctx.ReportErrorAtID(fold.ID(), "constant-folding evaluation failed: %v", err.Error()) return a } @@ -142,24 +144,19 @@ func (opt *constantFoldingOptimizer) Optimize(ctx *OptimizerContext, a *ast.AST) return a } +var errCannotFold = errors.New("subexpression cannot be folded") + // tryFold attempts to evaluate a sub-expression to a literal. // // If the evaluation succeeds, the input expr value will be modified to become a literal, otherwise // the method will return an error. func (opt *constantFoldingOptimizer) tryFold(ctx *OptimizerContext, a *ast.AST, expr ast.Expr) error { - // Assume all context is needed to evaluate the expression. - subAST := &Ast{ - impl: ast.NewCheckedAST(ast.NewAST(expr, a.SourceInfo()), a.TypeMap(), a.ReferenceMap()), - } - prg, err := ctx.Program(subAST) - if err != nil { - return err - } activation := opt.knownValues if activation == nil { activation = NoVars() } - out, _, err := prg.Eval(activation) + navExpr := expr.(ast.NavigableExpr) + out, err := evaluateExpr(ctx, a, navExpr, activation) if err != nil { return err } @@ -168,6 +165,30 @@ func (opt *constantFoldingOptimizer) tryFold(ctx *OptimizerContext, a *ast.AST, return nil } +func evaluateExpr(ctx *OptimizerContext, a *ast.AST, navigableExpr ast.NavigableExpr, activation Activation) (ref.Val, error) { + partialActivation, err := ctx.PartialVars(activation) + if err != nil { + return nil, err + } + subAST := &Ast{ + impl: ast.NewCheckedAST(ast.NewAST(navigableExpr, a.SourceInfo()), a.TypeMap(), a.ReferenceMap()), + } + prg, err := ctx.Program(subAST) + if err != nil { + return nil, err + } + // Folding will not attempt to call async functions which are all marked as late-bound, + // but the presence of such functions requires the use of `ConcurrentEval` in order to + // avoid an early return error which blocks async functions from running in `Eval` and + // `ContextEval` call paths. + resCh := prg.ConcurrentEval(context.Background(), partialActivation) + res := <-resCh + if res.Err != nil || types.IsUnknown(res.Val) { + return nil, errCannotFold + } + return res.Val, nil +} + func isLateBoundFunctionCall(ctx *OptimizerContext, expr ast.Expr) bool { call := expr.AsCall() function := ctx.Functions()[call.FunctionName()] @@ -181,12 +202,12 @@ func isLateBoundFunctionCall(ctx *OptimizerContext, expr ast.Expr) bool { // a branch can be removed. Evaluation will naturally prune logical and / or calls, // but conditional will not be pruned cleanly, so this is one small area where the // constant folding step reimplements a portion of the evaluator. -func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool { +func maybePruneBranches(ctx *OptimizerContext, a *ast.AST, expr ast.NavigableExpr) bool { call := expr.AsCall() args := call.Args() switch call.FunctionName() { case operators.LogicalAnd, operators.LogicalOr: - return maybeShortcircuitLogic(ctx, call.FunctionName(), args, expr) + return maybeShortcircuitLogic(ctx, a, call.FunctionName(), args, expr) case operators.Conditional: cond := args[0] truthy := args[1] @@ -207,11 +228,17 @@ func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool { return true } needle := args[0] - if needle.Kind() == ast.LiteralKind && haystack.Kind() == ast.ListKind { - needleValue := needle.AsLiteral() + if (needle.Kind() == ast.LiteralKind || isSelfEqualIdent(needle)) && haystack.Kind() == ast.ListKind { + needleIsLit := needle.Kind() == ast.LiteralKind + needleLitVal := needle.AsLiteral() + needleIdentVal := needle.AsIdent() list := haystack.AsList() - for _, e := range list.Elements() { - if e.Kind() == ast.LiteralKind && e.AsLiteral().Equal(needleValue) == types.True { + for _, elem := range list.Elements() { + if needleIsLit && elem.Kind() == ast.LiteralKind && elem.AsLiteral().Equal(needleLitVal) == types.True { + ctx.UpdateExpr(expr, ctx.NewLiteral(types.True)) + return true + } + if !needleIsLit && elem.Kind() == ast.IdentKind && elem.AsIdent() == needleIdentVal { ctx.UpdateExpr(expr, ctx.NewLiteral(types.True)) return true } @@ -221,7 +248,7 @@ func maybePruneBranches(ctx *OptimizerContext, expr ast.NavigableExpr) bool { return false } -func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.Expr, expr ast.NavigableExpr) bool { +func maybeShortcircuitLogic(ctx *OptimizerContext, a *ast.AST, function string, args []ast.Expr, expr ast.NavigableExpr) bool { shortcircuit := types.False skip := types.True if function == operators.LogicalOr { @@ -244,10 +271,14 @@ func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.E } if len(newArgs) == 0 { newArgs = append(newArgs, args[0]) - ctx.UpdateExpr(expr, newArgs[0]) - return true + } + if len(newArgs) == len(args) { + return false } if len(newArgs) == 1 { + if !isBoolType(a, newArgs[0]) { + return false + } ctx.UpdateExpr(expr, newArgs[0]) return true } @@ -255,6 +286,16 @@ func maybeShortcircuitLogic(ctx *OptimizerContext, function string, args []ast.E return true } +func isBoolType(a *ast.AST, e ast.Expr) bool { + if a != nil && a.GetType(e.ID()) == types.BoolType { + return true + } + if e.Kind() == ast.LiteralKind && e.AsLiteral().Type() == types.BoolType { + return true + } + return false +} + // pruneOptionalElements works from the bottom up to resolve optional elements within // aggregate literals. // @@ -285,9 +326,9 @@ func pruneOptionalListElements(ctx *OptimizerContext, e ast.Expr) { updatedElems := []ast.Expr{} updatedIndices := []int32{} newOptIndex := -1 - for _, e := range elems { + for i, e := range elems { newOptIndex++ - if !l.IsOptional(int32(newOptIndex)) { + if !l.IsOptional(int32(i)) { updatedElems = append(updatedElems, e) continue } @@ -501,7 +542,7 @@ func (opt *constantFoldingOptimizer) constantExprMatcher(ctx *OptimizerContext, sel := e.AsSelect() // guaranteed to be a navigable value return constantMatcher(sel.Operand().(ast.NavigableExpr)) case ast.IdentKind: - return opt.knownValues != nil && a.ReferenceMap()[e.ID()] != nil + return opt.knownValues != nil && a.ReferenceMap()[e.ID()] != nil && !hasComprehensionVar(e) case ast.ComprehensionKind: if isNestedComprehension(e) { return false @@ -513,6 +554,9 @@ func (opt *constantFoldingOptimizer) constantExprMatcher(ctx *OptimizerContext, nested := e.AsComprehension() vars[nested.AccuVar()] = true vars[nested.IterVar()] = true + if nested.IterVar2() != "" { + vars[nested.IterVar2()] = true + } } if e.Kind() == ast.IdentKind && !vars[e.AsIdent()] { constantExprs = false @@ -554,17 +598,33 @@ func constantCallMatcher(e ast.NavigableExpr) bool { return true } } + if fnName == operators.Equals || fnName == operators.NotEquals { + if hasComprehensionVar(e) { + return false + } + if isExprConstantOfKind(children[0], types.BoolType) || isExprConstantOfKind(children[1], types.BoolType) { + return true + } + } if fnName == operators.In { + if hasComprehensionVar(e) { + return false + } haystack := children[1] if haystack.Kind() == ast.ListKind && haystack.AsList().Size() == 0 { return true } needle := children[0] - if needle.Kind() == ast.LiteralKind && haystack.Kind() == ast.ListKind { - needleValue := needle.AsLiteral() + if (needle.Kind() == ast.LiteralKind || isSelfEqualIdent(needle)) && haystack.Kind() == ast.ListKind { + needleIsLit := needle.Kind() == ast.LiteralKind + needleLitVal := needle.AsLiteral() + needleIdentVal := needle.AsIdent() list := haystack.AsList() - for _, e := range list.Elements() { - if e.Kind() == ast.LiteralKind && e.AsLiteral().Equal(needleValue) == types.True { + for _, elem := range list.Elements() { + if needleIsLit && elem.Kind() == ast.LiteralKind && elem.AsLiteral().Equal(needleLitVal) == types.True { + return true + } + if !needleIsLit && elem.Kind() == ast.IdentKind && elem.AsIdent() == needleIdentVal { return true } } @@ -579,6 +639,74 @@ func constantCallMatcher(e ast.NavigableExpr) bool { return true } +// isSelfEqualIdent indicates whether the expression is an identifier whose static type +// guarantees that its runtime value is equal to itself. +// +// Matching an identifier against a list element by name only proves list membership when the +// value the name resolves to is self-equal. A double may be NaN, which is not equal to itself, +// and dyn, abstract, and struct types may all hold a NaN at runtime, so the check is limited +// to the scalar types which cannot, and to the aggregate types whose type parameters are +// themselves self-equal. +func isSelfEqualIdent(e ast.Expr) bool { + if e.Kind() != ast.IdentKind { + return false + } + nav, ok := e.(ast.NavigableExpr) + if !ok { + return false + } + return isSelfEqualType(nav.Type()) +} + +// isSelfEqualType indicates whether all runtime values of the given type are equal to themselves. +func isSelfEqualType(t *types.Type) bool { + if t == nil { + return false + } + switch t.Kind() { + case types.BoolKind, types.BytesKind, types.DurationKind, types.IntKind, + types.NullTypeKind, types.StringKind, types.TimestampKind, types.TypeKind, + types.UintKind: + return true + case types.ListKind, types.MapKind: + // Aggregates compare element-wise, so they are self-equal exactly when their type + // parameters are. A list(dyn) or map(string, double) may still contain a NaN. + for _, p := range t.Parameters() { + if !isSelfEqualType(p) { + return false + } + } + return true + default: + return false + } +} + +func isExprConstantOfKind(e ast.Expr, t *types.Type) bool { + return e.Kind() == ast.LiteralKind && e.AsLiteral().Type() == t +} + +func hasComprehensionVar(e ast.NavigableExpr) bool { + idents := ast.MatchDescendants(e, ast.KindMatcher(ast.IdentKind)) + for _, identNode := range idents { + identName := identNode.AsIdent() + curr := identNode + parent, found := curr.Parent() + for found { + if parent.Kind() == ast.ComprehensionKind { + compre := parent.AsComprehension() + if (compre.AccuVar() == identName || compre.IterVar() == identName || compre.IterVar2() == identName) && + curr.ID() != compre.IterRange().ID() && curr.ID() != compre.AccuInit().ID() { + return true + } + } + curr = parent + parent, found = parent.Parent() + } + } + return false +} + func isNestedComprehension(e ast.NavigableExpr) bool { parent, found := e.Parent() for found { diff --git a/vendor/github.com/google/cel-go/cel/io.go b/vendor/github.com/google/cel-go/cel/io.go index 2e611228d9..c991c95c3f 100644 --- a/vendor/github.com/google/cel-go/cel/io.go +++ b/vendor/github.com/google/cel-go/cel/io.go @@ -52,7 +52,12 @@ func CheckedExprToAstWithSource(checkedExpr *exprpb.CheckedExpr, src Source) (*A if err != nil { return nil, err } - return &Ast{source: src, impl: checked}, nil + out := &Ast{source: src, impl: checked} + if err := checkLoadedASTDepth(checked); err != nil { + out.loadErr = err + return out, err + } + return out, nil } // AstToCheckedExpr converts an Ast to an protobuf CheckedExpr value. @@ -83,7 +88,26 @@ func ParsedExprToAstWithSource(parsedExpr *exprpb.ParsedExpr, src Source) *Ast { src = common.NewInfoSource(parsedExpr.GetSourceInfo()) } e, _ := ast.ProtoToExpr(parsedExpr.GetExpr()) - return &Ast{source: src, impl: ast.NewAST(e, info)} + out := &Ast{source: src, impl: ast.NewAST(e, info)} + // ParsedExprToAstWithSource has no error return, so record an over-depth violation on the Ast + // to be surfaced when it is later checked or planned. + out.loadErr = checkLoadedASTDepth(out.impl) + return out +} + +// checkLoadedASTDepth guards ASTs that enter through the proto conversion helpers +// (ParsedExprToAst / CheckedExprToAst) against nesting deeper than the parser's recursion limit. +// Those entry points bypass the parser, so without this check a deeply nested loaded AST could +// exhaust the Go stack during later checking or planning. It returns a normal error rather than +// risking that overflow; the traversal itself is bounded so it stays safe on the same input. +// +// Embedders that fully control their AST inputs can skip this by building the AST through the +// common/ast package directly instead of these conversion helpers. +func checkLoadedASTDepth(a *ast.AST) error { + if ast.ExceedsDepth(a, defaultMaxASTDepth) { + return fmt.Errorf("input exceeds maximum expression nesting depth: %d", defaultMaxASTDepth) + } + return nil } // AstToParsedExpr converts an Ast to an protobuf ParsedExpr value. diff --git a/vendor/github.com/google/cel-go/cel/options.go b/vendor/github.com/google/cel-go/cel/options.go index 86a98d35db..540ad38ba0 100644 --- a/vendor/github.com/google/cel-go/cel/options.go +++ b/vendor/github.com/google/cel-go/cel/options.go @@ -24,6 +24,7 @@ import ( "google.golang.org/protobuf/reflect/protoregistry" "google.golang.org/protobuf/types/dynamicpb" + "github.com/google/cel-go/cel/async" "github.com/google/cel-go/checker" "github.com/google/cel-go/common/containers" "github.com/google/cel-go/common/decls" @@ -109,12 +110,23 @@ const ( limitCodePointSize // The number of attempts to recover from a parse error. limitParseErrorRecovery + // The maximum nesting depth permitted for ASTs loaded outside the parser. + limitMaxASTDepth + // The maximum number of expression nodes permitted in parsing (including macro expansion). + limitExpressionNodeCount ) +// defaultMaxASTDepth mirrors the parser's default maxRecursionDepth (250) and +// is applied to ASTs that enter through non-parser ingestion paths (e.g. via +// ParsedExprToAst / CheckedExprToAst) when no explicit limit is configured. +const defaultMaxASTDepth = 250 + var limitIDsToNames = map[limitID]string{ limitCodePointSize: "cel.limit.expression_code_points", limitParseErrorRecovery: "cel.limit.parse_error_recovery", limitParseRecursionDepth: "cel.limit.parse_recursion_depth", + limitMaxASTDepth: "cel.limit.max_ast_depth", + limitExpressionNodeCount: "cel.limit.expression_node_count", } func limitNameByID(id limitID) (string, bool) { @@ -735,6 +747,47 @@ func InterruptCheckFrequency(checkFrequency uint) ProgramOption { } } +// AsyncCallObserver sets the observer for monitoring asynchronous function calls during ConcurrentEval. +func AsyncCallObserver(observer async.Observer) ProgramOption { + return func(p *prog) (*prog, error) { + p.asyncObserver = observer + return p, nil + } +} + +// AsyncCompletionBufferSize sets the size of the buffer for the async completion channel. +// By default, the channel is unbuffered. +func AsyncCompletionBufferSize(size int) ProgramOption { + return func(p *prog) (*prog, error) { + p.asyncCompletionBufferSize = size + return p, nil + } +} + +// AsyncMaxConcurrency sets the maximum number of concurrently launched async calls during +// ConcurrentEval. This bounds the number of in-flight async goroutines, so a wide fan-out (such +// as an async call inside a comprehension over a large list) cannot exhaust memory. +// +// A value of 0 (unset) applies a built-in default bound. A positive value sets an explicit bound. +// A negative value disables the limiter (unbounded launches) and should only be used when +// concurrency is bounded by other means. +func AsyncMaxConcurrency(maxConcurrency int) ProgramOption { + return func(p *prog) (*prog, error) { + p.asyncMaxConcurrency = maxConcurrency + return p, nil + } +} + +// ConcurrentDrainStrategy configures the strategy for when to re-evaluate the program +// during a ConcurrentEval call after receiving asynchronous completion signals. +// By default, the program re-evaluates immediately after every completion. +func ConcurrentDrainStrategy(strategy async.DrainStrategy) ProgramOption { + return func(p *prog) (*prog, error) { + p.drainStrategy = strategy + return p, nil + } +} + // CostEstimatorOptions configure type-check time options for estimating expression cost. func CostEstimatorOptions(costOpts ...checker.CostOption) EnvOption { return func(e *Env) (*Env, error) { @@ -950,6 +1003,25 @@ func ParserExpressionSizeLimit(limit int) EnvOption { return setLimit(limitCodePointSize, limit) } +// ExpressionNodeLimit adjusts the maximum number of expression nodes permitted during parsing +// and checking, including nodes created by macro expansion. Defaults are defined in the parser +// package (100,000). A negative value means unbounded. +func ExpressionNodeLimit(limit int) EnvOption { + return setLimit(limitExpressionNodeCount, limit) +} + +// ExpressionNestingDepthLimit records the maximum nesting depth permitted for ASTs in the +// environment configuration so that the value round-trips through env.Config export/import. +// +// ASTs loaded outside the parser (e.g. via ParsedExprToAst / CheckedExprToAst) bypass the +// parser's recursion limit, so those conversion paths validate nesting depth against the +// parser-matching default (250) to avoid a Go stack overflow during later checking or planning. +// Embedders that fully control their AST inputs and want to skip the check can construct the AST +// through the common/ast package directly rather than the cel conversion helpers. +func ExpressionNestingDepthLimit(limit int) EnvOption { + return setLimit(limitMaxASTDepth, limit) +} + // EnableHiddenAccumulatorName sets the parser to use the identifier '@result' for accumulators // which is not normally accessible from CEL source. func EnableHiddenAccumulatorName(enabled bool) EnvOption { diff --git a/vendor/github.com/google/cel-go/cel/program.go b/vendor/github.com/google/cel-go/cel/program.go index 1fb6587774..3a7589a71c 100644 --- a/vendor/github.com/google/cel-go/cel/program.go +++ b/vendor/github.com/google/cel-go/cel/program.go @@ -18,7 +18,9 @@ import ( "context" "errors" "fmt" + "time" + "github.com/google/cel-go/cel/async" "github.com/google/cel-go/common/ast" "github.com/google/cel-go/common/functions" "github.com/google/cel-go/common/types" @@ -52,6 +54,21 @@ type Program interface { // // The output contract for `ContextEval` is otherwise identical to the `Eval` method. ContextEval(context.Context, any) (ref.Val, *EvalDetails, error) + + // ConcurrentEval evaluates the program concurrently, returning a channel that will receive + // the final EvalResult when all asynchronous operations complete, or the context expires. + // + // The vars value may either be an `Activation` or `map[string]any`. + // + // Liveness: ConcurrentEval relies on context cancellation to terminate. If an async function + // never returns and does not honor its context, and the supplied context has no deadline, the + // call will block indefinitely. Always pass a context with a deadline or cancellation. + // + // Error handling is fail-fast: as soon as a re-evaluation pass yields an error, that error is + // returned and any still in-flight async calls are cancelled (their contexts are done) and + // their results discarded. Async functions should therefore be free of unwanted side effects + // on partial evaluation, or guard them with idempotency/cancellation handling. + ConcurrentEval(context.Context, any) <-chan EvalResult } // Activation used to resolve identifiers by name and references by id. @@ -144,6 +161,13 @@ func (ed *EvalDetails) ActualCost() *uint64 { return &cost } +// EvalResult encapsulates the response from a ConcurrentEval call. +type EvalResult struct { + Val ref.Val + EvalDetails *EvalDetails + Err error +} + // prog is the internal implementation of the Program interface. type prog struct { *Env @@ -164,6 +188,16 @@ type prog struct { callCostEstimator interpreter.ActualCostEstimator costOptions []interpreter.CostTrackerOption costLimit *uint64 + + // hasAsync indicates the planned expression contains an asynchronous function call, which can + // only be resolved by ConcurrentEval. + hasAsync bool + + // Async evaluation configuration used by ConcurrentEval. + drainStrategy async.DrainStrategy + asyncObserver async.Observer + asyncCompletionBufferSize int + asyncMaxConcurrency int } // newProgram creates a program instance with an environment, an ast, and an optional list of @@ -181,6 +215,7 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) { plannerOptions: []interpreter.PlannerOption{}, dispatcher: disp, costOptions: []interpreter.CostTrackerOption{}, + drainStrategy: async.DrainReady(100 * time.Microsecond), } // Configure the program via the ProgramOption values. @@ -213,6 +248,17 @@ func newProgram(e *Env, a *ast.AST, opts []ProgramOption) (Program, error) { return nil, err } + // Determine whether the environment declares any asynchronous function. Async is a property of + // the binding, so its presence is known from the environment alone, without inspecting the + // program plan. The synchronous entry points (Eval, ContextEval) reject programs from an env + // with async functions; callers needing synchronous evaluation should use a non-async env. + for _, b := range e.functionBindings { + if b.Async != nil { + p.hasAsync = true + break + } + } + // Set the attribute factory after the options have been set. var attrFactory interpreter.AttributeFactory attrFactorOpts := []interpreter.AttrFactoryOption{ @@ -319,6 +365,11 @@ func (p *prog) Eval(input any) (out ref.Val, det *EvalDetails, err error) { } } }() + // Asynchronous calls cannot be resolved by a single-pass evaluation. Reject before doing any + // work (this also covers ContextEval, which delegates here); ConcurrentEval does not call Eval. + if p.hasAsync { + return nil, nil, errAsyncRequiresConcurrentEval + } // Build a hierarchical activation if there are default vars set. var frame *interpreter.ExecutionFrame if f, ok := input.(*interpreter.ExecutionFrame); ok { @@ -383,3 +434,194 @@ func (p *prog) newExecutionFrame(input any) (*interpreter.ExecutionFrame, error) return frame, nil } + +// newAsyncFrame creates an ExecutionFrame configured for asynchronous evaluation under the +// given context, wiring the observer and concurrency limit from the program options. +func (p *prog) newAsyncFrame(ctx context.Context, input any) (*interpreter.ExecutionFrame, error) { + frame, err := p.newExecutionFrame(input) + if err != nil { + return nil, err + } + if err := frame.SetContext(ctx, p.interruptCheckFrequency); err != nil { + frame.Close() + return nil, err + } + frame.SetAsyncObserver(p.asyncObserver) + frame.SetAsyncMaxConcurrency(resolveAsyncMaxConcurrency(p.asyncMaxConcurrency)) + return frame, nil +} + +// defaultAsyncMaxConcurrency bounds the number of concurrently launched async calls when the +// program does not configure AsyncMaxConcurrency. It exists so that a wide fan-out (e.g. an async +// call inside a comprehension over a large list) cannot spawn an unbounded number of goroutines. +const defaultAsyncMaxConcurrency = 100 + +// resolveAsyncMaxConcurrency maps the configured concurrency to the effective launch limit: +// - 0 (unset): apply defaultAsyncMaxConcurrency. +// - >0: use the configured value. +// - <0: unlimited (no launch limiter); use only if the caller bounds concurrency another way. +func resolveAsyncMaxConcurrency(configured int) int { + if configured == 0 { + return defaultAsyncMaxConcurrency + } + return configured +} + +// resolveCompletionBufferSize returns the size of the async completion channel. When unset, it +// defaults to the effective launch concurrency so that all in-flight calls can report completion +// without blocking. An unbuffered channel would make a completed call hold its launch slot until +// the evaluator drained it, throttling effective concurrency to the drain rate. +func (p *prog) resolveCompletionBufferSize() int { + if p.asyncCompletionBufferSize > 0 { + return p.asyncCompletionBufferSize + } + limit := resolveAsyncMaxConcurrency(p.asyncMaxConcurrency) + if limit < 0 { + // Unlimited launches: fall back to the default bound for the buffer so it stays finite. + return defaultAsyncMaxConcurrency + } + return limit +} + +// ConcurrentEval implements the Program interface. +func (p *prog) ConcurrentEval(ctx context.Context, input any) <-chan EvalResult { + resCh := make(chan EvalResult, 1) + if ctx == nil { + resCh <- EvalResult{Err: errors.New("context can not be nil")} + close(resCh) + return resCh + } + + go func() { + defer close(resCh) + // Ensure concurrent eval handles panic / recovery properly + defer func() { + if r := recover(); r != nil { + switch t := r.(type) { + case interpreter.EvalCancelledError: + resCh <- EvalResult{Err: t} + default: + resCh <- EvalResult{Err: fmt.Errorf("internal error: %v", r)} + } + } + }() + + frame, err := p.newAsyncFrame(ctx, input) + if err != nil { + resCh <- EvalResult{Err: err} + return + } + defer frame.Close() + + // Completions are signaled to this channel as async calls finish. The asyncCallState + // fan-in also selects on ctx.Done(), so the sender will not leak if this loop returns early. + completions := make(chan int64, p.resolveCompletionBufferSize()) + frame.SetCompletions(completions) + + for { + var out ref.Val + var det *EvalDetails + + if p.observable != nil { + det = &EvalDetails{} + out = p.observable.ObserveExec(frame, func(observed any) { + switch o := observed.(type) { + case interpreter.EvalState: + det.state = o + case *interpreter.CostTracker: + det.costTracker = o + } + }) + } else { + out = p.interpretable.Exec(frame) + } + + // Communicate errors quickly. + if types.IsError(out) { + var err error = out.(*types.Err) + if errors.Is(err, interpreter.InterruptError{}) { + err = fmt.Errorf("%w: %w", err, context.Cause(ctx)) + } + resCh <- EvalResult{Val: out, EvalDetails: det, Err: err} + return + } + + // A concrete (non-unknown) result is final. + unk, isUnknown := out.(*types.Unknown) + if !isUnknown || !unk.HasUnknownFunction() { + resCh <- EvalResult{Val: out, EvalDetails: det, Err: nil} + return + } + + // Post-execution dispatch: launch only the async calls required by the unknown result. + frame.DispatchPendingAsyncCalls(unk.IDs()) + + // The result depends on one or more unresolved async calls. Wait for completions and + // re-evaluate according to the configured drain strategy. + var batch []async.Call + + // Wait for at least one completion (or cancellation). + select { + case id := <-completions: + if call := frame.AsyncCall(id); call != nil { + batch = append(batch, call) + } + case <-ctx.Done(): + resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()} + return + } + + // Accumulate completions and consult the strategy. + var timer *time.Timer + reevaluate := false + for !reevaluate { + active := frame.ActiveAsyncCalls() + action := p.drainStrategy.NextAction(batch, active) + if action.Reevaluate { + break + } + + var timeoutCh <-chan time.Time + if action.WaitDuration > 0 { + if timer == nil { + timer = time.NewTimer(action.WaitDuration) + } else { + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(action.WaitDuration) + } + timeoutCh = timer.C + } + + select { + case id := <-completions: + if call := frame.AsyncCall(id); call != nil { + batch = append(batch, call) + } + case <-timeoutCh: + reevaluate = true + case <-ctx.Done(): + if timer != nil { + timer.Stop() + } + resCh <- EvalResult{Val: out, EvalDetails: det, Err: ctx.Err()} + return + } + } + if timer != nil { + timer.Stop() + } + } + }() + + return resCh +} + +// errAsyncRequiresConcurrentEval is returned by the synchronous entry points (Eval, ContextEval) +// when the expression contains asynchronous function calls, which only ConcurrentEval can resolve. +var errAsyncRequiresConcurrentEval = errors.New( + "expression contains asynchronous function calls; use ConcurrentEval") diff --git a/vendor/github.com/google/cel-go/cel/validator.go b/vendor/github.com/google/cel-go/cel/validator.go index 952f88f41b..cb7f4c29ea 100644 --- a/vendor/github.com/google/cel-go/cel/validator.go +++ b/vendor/github.com/google/cel-go/cel/validator.go @@ -15,6 +15,7 @@ package cel import ( + "context" "fmt" "reflect" "regexp" @@ -25,11 +26,12 @@ import ( ) const ( - durationValidatorName = "cel.validator.duration" - regexValidatorName = "cel.validator.matches" - timestampValidatorName = "cel.validator.timestamp" - homogeneousValidatorName = "cel.validator.homogeneous_literals" - nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit" + durationValidatorName = "cel.validator.duration" + regexValidatorName = "cel.validator.matches" + timestampValidatorName = "cel.validator.timestamp" + homogeneousValidatorName = "cel.validator.homogeneous_literals" + nestingLimitValidatorName = "cel.validator.comprehension_nesting_limit" + bindNestingLimitValidatorName = "cel.validator.bind_nesting_limit" // HomogeneousAggregateLiteralExemptFunctions is the ValidatorConfig key used to configure // the set of function names which are exempt from homogeneous type checks. The expected type @@ -60,6 +62,23 @@ var ( } return nil, fmt.Errorf("invalid validator: %s missing limit", nestingLimitValidatorName) }, + bindNestingLimitValidatorName: func(val *env.Validator) (ASTValidator, error) { + if limit, found := val.ConfigValue("limit"); found { + // In case of protos, config value is of type by google.protobuf.Value, which numeric values are always a double. + if val, isDouble := limit.(float64); isDouble { + if val != float64(int64(val)) { + return nil, fmt.Errorf("invalid validator: %s, limit value is not a whole number: %v", bindNestingLimitValidatorName, limit) + } + return ValidateBindNestingLimit(int(val)), nil + } + + if val, isInt := limit.(int); isInt { + return ValidateBindNestingLimit(val), nil + } + return nil, fmt.Errorf("invalid validator: %s unsupported limit type: %v", bindNestingLimitValidatorName, limit) + } + return nil, fmt.Errorf("invalid validator: %s missing limit", bindNestingLimitValidatorName) + }, durationValidatorName: func(*env.Validator) (ASTValidator, error) { return ValidateDurationLiterals(), nil }, @@ -80,12 +99,20 @@ type ASTValidatorFactory func(*env.Validator) (ASTValidator, error) // ASTValidators configures a set of ASTValidator instances into the target environment. // -// Validators are applied in the order in which the are specified and are treated as singletons. -// The same ASTValidator with a given name will not be applied more than once. +// Validators are applied in the order in which they are specified. +// If an ASTValidator with the same name is already configured, it will be replaced. func ASTValidators(validators ...ASTValidator) EnvOption { return func(e *Env) (*Env, error) { for _, v := range validators { - if !e.HasValidator(v.Name()) { + found := false + for i, existing := range e.validators { + if existing.Name() == v.Name() { + e.validators[i] = v + found = true + break + } + } + if !found { e.validators = append(e.validators, v) } } @@ -232,6 +259,13 @@ func ValidateComprehensionNestingLimit(limit int) ASTValidator { return nestingLimitValidator{limit: limit} } +// ValidateBindNestingLimit ensures that cel.bind() macro nesting does not exceed the specified limit. +// +// This validator can be useful for preventing arbitrarily nested cel.bind() macro calls. +func ValidateBindNestingLimit(limit int) ASTValidator { + return bindNestingLimitValidator{limit: limit} +} + type argChecker func(env *Env, call, arg ast.Expr) error func newFormatValidator(funcName string, argNum int, check argChecker) formatValidator { @@ -284,8 +318,9 @@ func evalCall(env *Env, call, arg ast.Expr) error { if err != nil { return err } - _, _, err = prg.Eval(NoVars()) - return err + resCh := prg.ConcurrentEval(context.Background(), NoVars()) + res := <-resCh + return res.Err } func compileRegex(_ *Env, _, arg ast.Expr) error { @@ -430,8 +465,7 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i } // When the comprehension has an empty range, continue to the next ancestor // as this comprehension does not have any associated cost. - iterRange := e.AsComprehension().IterRange() - if iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 { + if isEmptyRangeComprehension(e) { e, hasParent = e.Parent() continue } @@ -445,3 +479,68 @@ func (v nestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, i } } } + +type bindNestingLimitValidator struct { + limit int +} + +// Name returns the name of the cel.bind nesting limit validator. +func (v bindNestingLimitValidator) Name() string { + return bindNestingLimitValidatorName +} + +// ToConfig converts the ASTValidator to an env.Validator specifying the validator name and the nesting limit +// as an integer value: {"limit": int} +func (v bindNestingLimitValidator) ToConfig() *env.Validator { + return env.NewValidator(v.Name()).SetConfig(map[string]any{"limit": v.limit}) +} + +// Validate implements the ASTValidator interface method. +func (v bindNestingLimitValidator) Validate(e *Env, _ ValidatorConfig, a *ast.AST, iss *Issues) { + root := ast.NavigateAST(a) + comprehensions := ast.MatchDescendants(root, ast.KindMatcher(ast.ComprehensionKind)) + var celBinds []ast.NavigableExpr + for _, comp := range comprehensions { + if isCelBind(comp) { + celBinds = append(celBinds, comp) + } + } + if len(celBinds) <= v.limit { + return + } + for _, comp := range celBinds { + count := 0 + e := comp + hasParent := true + for hasParent { + if isCelBind(e) { + count++ + if count > v.limit { + iss.ReportErrorAtID(comp.ID(), "cel.bind exceeds nesting limit") + break + } + } + e, hasParent = e.Parent() + } + } +} + +func isEmptyRangeComprehension(e ast.NavigableExpr) bool { + if e.Kind() != ast.ComprehensionKind { + return false + } + iterRange := e.AsComprehension().IterRange() + return iterRange.Kind() == ast.ListKind && iterRange.AsList().Size() == 0 +} + +func isCelBind(e ast.NavigableExpr) bool { + if !isEmptyRangeComprehension(e) { + return false + } + compre := e.AsComprehension() + loopCond := compre.LoopCondition() + loopStep := compre.LoopStep() + return compre.IterVar() == unusedIterVar && + loopCond.Kind() == ast.LiteralKind && loopCond.AsLiteral().Value() == false && + loopStep.Kind() == ast.IdentKind && loopStep.AsIdent() == compre.AccuVar() +} diff --git a/vendor/github.com/google/cel-go/common/ast/ast.go b/vendor/github.com/google/cel-go/common/ast/ast.go index 3ae2e1063b..c8f8f8a022 100644 --- a/vendor/github.com/google/cel-go/common/ast/ast.go +++ b/vendor/github.com/google/cel-go/common/ast/ast.go @@ -172,6 +172,14 @@ func (a *AST) IDs() map[int64]bool { return visitor } +// NodeCount returns the total number of expression nodes in the AST, including macro calls. +func NodeCount(a *AST) int { + if a == nil { + return 0 + } + return len(a.IDs()) +} + // ClearUnusedIDs removes IDs not used in the AST or macro calls from SourceInfo. func (a *AST) ClearUnusedIDs() { ids := a.IDs() diff --git a/vendor/github.com/google/cel-go/common/ast/navigable.go b/vendor/github.com/google/cel-go/common/ast/navigable.go index 13e5777b50..364edfa3af 100644 --- a/vendor/github.com/google/cel-go/common/ast/navigable.go +++ b/vendor/github.com/google/cel-go/common/ast/navigable.go @@ -181,6 +181,29 @@ func PreOrderVisit(expr Expr, visitor Visitor) { visit(expr, visitor, preOrder, 0, 0) } +// ExceedsDepth determines whether the AST contains expressions nested deeper than the specified +// maxDepth. The root expression has depth 0, so a maxDepth of 250 permits expressions nested up +// to and including 250 levels deep. +// +// The traversal is bounded: it descends at most maxDepth+1 levels, so it remains safe to call on +// adversarially deep inputs that could otherwise exhaust the Go stack during later checking or +// planning. A non-positive maxDepth disables the check and returns false. +func ExceedsDepth(a *AST, maxDepth int) bool { + if a == nil || maxDepth <= 0 { + return false + } + exceedsDepth := false + visitor := NewExprVisitor(func(e Expr) { + if nav, ok := e.(NavigableExpr); ok && nav.Depth() >= maxDepth { + exceedsDepth = true + } + }) + // Bound the walk to maxDepth+1 levels so it never recurses past the first level that exceeds + // the limit, keeping the check itself safe on the deep inputs it guards against. + visit(NavigateAST(a), visitor, postOrder, 0, maxDepth+1) + return exceedsDepth +} + type visitOrder int const ( diff --git a/vendor/github.com/google/cel-go/common/decls/decls.go b/vendor/github.com/google/cel-go/common/decls/decls.go index cd4d3a5659..51cb689e58 100644 --- a/vendor/github.com/google/cel-go/common/decls/decls.go +++ b/vendor/github.com/google/cel-go/common/decls/decls.go @@ -16,6 +16,7 @@ package decls import ( + "context" "fmt" "strings" @@ -316,8 +317,12 @@ func (f *FunctionDecl) HasLateBinding() bool { if f == nil { return false } + if f.singleton != nil && f.singleton.Async != nil { + return true + } for _, oID := range f.overloadOrdinals { - if f.overloads[oID].HasLateBinding() { + o := f.overloads[oID] + if o.HasLateBinding() { return true } } @@ -342,6 +347,7 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) { Unary: o.guardedUnaryOp(f.Name(), f.disableTypeGuards), Binary: o.guardedBinaryOp(f.Name(), f.disableTypeGuards), Function: o.guardedFunctionOp(f.Name(), f.disableTypeGuards), + Async: o.guardedAsyncOp(f.Name(), f.disableTypeGuards), OperandTrait: o.OperandTrait(), NonStrict: o.IsNonStrict(), } @@ -362,6 +368,7 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) { Unary: f.singleton.Unary, Binary: f.singleton.Binary, Function: f.singleton.Function, + Async: f.singleton.Async, OperandTrait: f.singleton.OperandTrait, }, } @@ -380,6 +387,7 @@ func (f *FunctionDecl) Bindings() ([]*functions.Overload, error) { Unary: overloads[0].Unary, Binary: overloads[0].Binary, Function: overloads[0].Function, + Async: overloads[0].Async, NonStrict: overloads[0].NonStrict, OperandTrait: overloads[0].OperandTrait, }), nil @@ -538,6 +546,30 @@ func SingletonFunctionBinding(fn functions.FunctionOp, traits ...int) FunctionOp } } +// SingletonAsyncBinding creates a singleton async function definition to be used with all function overloads. +// The provided function is called in its own goroutine with the provided context. The function should +// block until the result is available, and the framework manages goroutine and channel lifecycle. +// +// Note, this approach works well if operand is expected to have a specific trait which it implements, +// e.g. traits.ContainerType. Otherwise, prefer per-overload async bindings. +func SingletonAsyncBinding(fn functions.BlockingAsyncOp, traits ...int) FunctionOpt { + trait := 0 + for _, t := range traits { + trait = trait | t + } + return func(f *FunctionDecl) (*FunctionDecl, error) { + if f.singleton != nil { + return nil, fmt.Errorf("function already has a singleton binding: %s", f.Name()) + } + f.singleton = &functions.Overload{ + Operator: f.Name(), + Async: wrapAsyncOp(fn), + OperandTrait: trait, + } + return f, nil + } +} + // Overload defines a new global overload with an overload id, argument types, and result type. Through the // use of OverloadOpt options, the overload may also be configured with a binding, an operand trait, and to // be non-strict. @@ -622,6 +654,8 @@ type OverloadDecl struct { binaryOp functions.BinaryOp // functionOp is a catch-all for zero-arity and three-plus arity functions. functionOp functions.FunctionOp + // asyncOp is an asynchronous function binding that returns a channel. + asyncOp functions.AsyncOp } // Examples returns a list of string examples for the overload. @@ -677,7 +711,7 @@ func (o *OverloadDecl) HasLateBinding() bool { if o == nil { return false } - return o.hasLateBinding + return o.hasLateBinding || o.asyncOp != nil } // OperandTrait returns the trait mask of the first operand to the overload call, e.g. @@ -750,7 +784,7 @@ func (o *OverloadDecl) SignatureOverlaps(other *OverloadDecl) bool { // HasBinding indicates whether the overload already has a definition. func (o *OverloadDecl) HasBinding() bool { - return o != nil && (o.unaryOp != nil || o.binaryOp != nil || o.functionOp != nil) + return o != nil && (o.unaryOp != nil || o.binaryOp != nil || o.functionOp != nil || o.asyncOp != nil) } // guardedUnaryOp creates an invocation guard around the provided unary operator, if one is defined. @@ -792,6 +826,22 @@ func (o *OverloadDecl) guardedFunctionOp(funcName string, disableTypeGuards bool } } +// guardedAsyncOp creates an invocation guard around the provided async function binding, if one is provided. +func (o *OverloadDecl) guardedAsyncOp(funcName string, disableTypeGuards bool) functions.AsyncOp { + if o.asyncOp == nil { + return nil + } + return func(ctx context.Context, args ...ref.Val) <-chan ref.Val { + if !o.matchesRuntimeSignature(disableTypeGuards, args...) { + ch := make(chan ref.Val, 1) + ch <- MaybeNoSuchOverload(funcName, args...) + close(ch) + return ch + } + return o.asyncOp(ctx, args...) + } +} + // matchesRuntimeUnarySignature indicates whether the argument type is runtime assiganble to the overload's expected argument. func (o *OverloadDecl) matchesRuntimeUnarySignature(disableTypeGuards bool, arg ref.Val) bool { return matchRuntimeArgType(o.IsNonStrict(), disableTypeGuards, o.ArgTypes()[0], arg) && @@ -825,6 +875,8 @@ func matchRuntimeArgType(nonStrict, disableTypeGuards bool, argType *types.Type, if nonStrict && (disableTypeGuards || types.IsUnknownOrError(arg)) { return true } + // Note, early returns and unknown aggregation happen in the interpretable.go file; however, this check is here + // for defense in depth or for scenarios where someone manipulates bindings to offer their own dispatch logic. if types.IsUnknownOrError(arg) { return false } @@ -897,6 +949,40 @@ func FunctionBinding(binding functions.FunctionOp) OverloadOpt { } } +// AsyncBinding provides the implementation of an asynchronous overload. The provided function +// is called in its own goroutine with the provided context. The function should block until +// the result is available, and the framework manages goroutine and channel lifecycle. +// +// This follows the same pattern used by gRPC-Go and other major Go frameworks where user +// code is synchronous and the framework manages concurrency. +func AsyncBinding(fn functions.BlockingAsyncOp) OverloadOpt { + return func(o *OverloadDecl) (*OverloadDecl, error) { + if o.HasBinding() { + return nil, fmt.Errorf("overload already has a binding: %s", o.ID()) + } + if o.hasLateBinding { + return nil, fmt.Errorf("overload already has a late binding: %s", o.ID()) + } + o.asyncOp = wrapAsyncOp(fn) + return o, nil + } +} + +// wrapAsyncOp adapts a blocking function into the channel-based AsyncOp used internally. +// +// The blocking function is invoked synchronously and its result delivered on a buffered channel. +// The interpreter always invokes an AsyncOp from a dedicated goroutine, so running the blocking +// call inline here keeps the framework to a single goroutine per async call rather than spawning +// an additional one to bridge blocking-to-channel. +func wrapAsyncOp(fn functions.BlockingAsyncOp) functions.AsyncOp { + return func(ctx context.Context, args ...ref.Val) <-chan ref.Val { + ch := make(chan ref.Val, 1) + ch <- fn(ctx, args...) + close(ch) + return ch + } +} + // LateFunctionBinding indicates that the function has a binding which is not known at compile time. // This is useful for functions which have side-effects or are not deterministically computable. func LateFunctionBinding() OverloadOpt { diff --git a/vendor/github.com/google/cel-go/common/stdlib/standard.go b/vendor/github.com/google/cel-go/common/stdlib/standard.go index bbbf2f7d9b..d2313bef1b 100644 --- a/vendor/github.com/google/cel-go/common/stdlib/standard.go +++ b/vendor/github.com/google/cel-go/common/stdlib/standard.go @@ -16,6 +16,7 @@ package stdlib import ( + "math" "strconv" "strings" "time" @@ -310,6 +311,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('1ms') < duration('1s') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntNegOne { return types.True @@ -367,6 +371,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('1ms') <= duration('1s') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntNegOne || cmp == types.IntZero { return types.True @@ -424,6 +431,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('1ms') > duration('1us') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntOne { return types.True @@ -481,6 +491,9 @@ func init() { argTypes(types.DurationType, types.DurationType), types.BoolType, decls.OverloadExamples(`duration('60s') >= duration('1m') // true`)), decls.SingletonBinaryBinding(func(lhs, rhs ref.Val) ref.Val { + if isNaN(lhs) || isNaN(rhs) { + return types.False + } cmp := lhs.(traits.Comparer).Compare(rhs) if cmp == types.IntOne || cmp == types.IntZero { return types.True @@ -926,6 +939,11 @@ func noBinaryOverrides(rhs, lhs ref.Val) ref.Val { return types.NoSuchOverloadErr() } +func isNaN(val ref.Val) bool { + d, ok := val.(types.Double) + return ok && math.IsNaN(float64(d)) +} + func noFunctionOverrides(args ...ref.Val) ref.Val { return types.NoSuchOverloadErr() } diff --git a/vendor/github.com/google/cel-go/common/types/bytes.go b/vendor/github.com/google/cel-go/common/types/bytes.go index 88da05315c..2eefb5d7f7 100644 --- a/vendor/github.com/google/cel-go/common/types/bytes.go +++ b/vendor/github.com/google/cel-go/common/types/bytes.go @@ -44,7 +44,10 @@ func (b Bytes) Add(other ref.Val) ref.Val { if !ok { return ValOrErr(other, "no such overload") } - return append(b, otherBytes...) + sum := make([]byte, 0, len(b)+len(otherBytes)) + sum = append(sum, b...) + sum = append(sum, otherBytes...) + return Bytes(sum) } // Compare implements traits.Comparer interface method by lexicographic ordering. diff --git a/vendor/github.com/google/cel-go/common/types/string.go b/vendor/github.com/google/cel-go/common/types/string.go index 5f5a43358e..1335903a7b 100644 --- a/vendor/github.com/google/cel-go/common/types/string.go +++ b/vendor/github.com/google/cel-go/common/types/string.go @@ -122,7 +122,11 @@ func (s String) ConvertToType(typeVal ref.Type) ref.Val { return durationOf(d) } case TimestampType: - if t, err := time.Parse(time.RFC3339, s.Value().(string)); err == nil { + str := s.Value().(string) + if !isStrictRFC3339(str) { + return NewErr("invalid RFC 3339 timestamp %q", str) + } + if t, err := time.Parse(time.RFC3339, str); err == nil { if t.Unix() < minUnixTime || t.Unix() > maxUnixTime { return celErrTimestampOverflow } diff --git a/vendor/github.com/google/cel-go/common/types/timestamp.go b/vendor/github.com/google/cel-go/common/types/timestamp.go index c4484291be..62a020d970 100644 --- a/vendor/github.com/google/cel-go/common/types/timestamp.go +++ b/vendor/github.com/google/cel-go/common/types/timestamp.go @@ -17,9 +17,11 @@ package types import ( "fmt" "reflect" + "regexp" "strconv" "strings" "time" + "unicode" "github.com/google/cel-go/common/overloads" "github.com/google/cel-go/common/types/ref" @@ -52,6 +54,79 @@ const ( maxUnixTime int64 = 253402300799 ) +// strictRFC3339Pattern gates the strings accepted by the `timestamp()` overload. +// time.Parse accepts inputs that RFC 3339 forbids: a ',' fractional-second +// separator, single-digit time fields, and numeric offsets whose hours exceed +// 23 or minutes exceed 59. Those slip past unnoticed and shift the parsed +// instant, so they are rejected before time.Parse runs. Month and day are held +// to the grammar ranges 01-12 and 01-31; the remaining calendar validation +// (day-of-month vs. month, leap years) is left to time.Parse. +// +// isStrictRFC3339 is the implementation used on the conversion path; the pattern +// is retained as the reference the scan is conformance tested against. +var strictRFC3339Pattern = regexp.MustCompile( + `^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[Tt]([01]\d|2[0-3]):[0-5]\d:([0-5]\d|60)(\.\d+)?([Zz]|[+-]([01]\d|2[0-3]):[0-5]\d)$`) + +// isStrictRFC3339 reports whether s matches strictRFC3339Pattern, hand-rolled to +// keep the conversion path off the regexp engine and its per-call cost. +func isStrictRFC3339(s string) bool { + // Shortest accepted form is "2006-01-02T15:04:05Z" (20 bytes): a 19-byte + // fixed-width date-time followed by at least a 'Z'/'z' zone. + if len(s) < 20 { + return false + } + // full-date "T" partial-time + if !isYear(s[0:4]) || !isChar(s[4], '-') || !isMonth(s[5:7]) || !isChar(s[7], '-') || !isDay(s[8:10]) || + !isChar(s[10], 't') || + !isHour(s[11:13]) || !isChar(s[13], ':') || !isMinute(s[14:16]) || !isChar(s[16], ':') || !isSecond(s[17:19]) { + return false + } + rest := s[19:] + // optional fractional seconds: "." 1*DIGIT + if rest[0] == '.' { + rest = rest[1:] + n := 0 + for n < len(rest) && isDigit(rest[n]) { + n++ + } + if n == 0 { + return false + } + rest = rest[n:] + } + // time-offset: "Z" or ("+" / "-") time-hour ":" time-minute + if len(rest) == 1 { + return isChar(rest[0], 'z') + } + if len(rest) == 6 && (rest[0] == '+' || rest[0] == '-') { + return isHour(rest[1:3]) && isChar(rest[3], ':') && isMinute(rest[4:6]) + } + return false +} + +func isDigit(c byte) bool { return c >= '0' && c <= '9' } + +// isChar reports whether got is want, case-insensitively; want must be lower case. +func isChar(got, want byte) bool { + g, w := rune(got), rune(want) + return g == w || unicode.ToLower(g) == w +} + +// inRange reports whether s is all decimal digits and its value lies in [lo, hi]. +func inRange(s string, lo, hi uint64) bool { + u, err := strconv.ParseUint(s, 10, 64) + return err == nil && u >= lo && u <= hi +} + +func isYear(s string) bool { return inRange(s, 0, 9999) } +func isMonth(s string) bool { return inRange(s, 1, 12) } +func isDay(s string) bool { return inRange(s, 1, 31) } +func isHour(s string) bool { return inRange(s, 0, 23) } +func isMinute(s string) bool { return inRange(s, 0, 59) } + +// isSecond permits 60 for a leap second. +func isSecond(s string) bool { return inRange(s, 0, 60) } + // Add implements traits.Adder.Add. func (t Timestamp) Add(other ref.Val) ref.Val { switch other.Type() { diff --git a/vendor/github.com/google/cel-go/common/types/unknown.go b/vendor/github.com/google/cel-go/common/types/unknown.go index 24d86518b4..f43aff18e0 100644 --- a/vendor/github.com/google/cel-go/common/types/unknown.go +++ b/vendor/github.com/google/cel-go/common/types/unknown.go @@ -16,6 +16,7 @@ package types import ( "fmt" + "maps" "math" "reflect" "sort" @@ -297,9 +298,7 @@ func MergeUnknowns(unk1, unk2 *Unknown) *Unknown { out := &Unknown{ attributeTrails: make(map[int64][]*AttributeTrail, len(unk1.attributeTrails)+len(unk2.attributeTrails)), } - for id, ats := range unk1.attributeTrails { - out.attributeTrails[id] = ats - } + maps.Copy(out.attributeTrails, unk1.attributeTrails) for id, ats := range unk2.attributeTrails { existing, found := out.attributeTrails[id] if !found { diff --git a/vendor/github.com/google/cel-go/ext/README.md b/vendor/github.com/google/cel-go/ext/README.md index 523d908564..6133b5cbf3 100644 --- a/vendor/github.com/google/cel-go/ext/README.md +++ b/vendor/github.com/google/cel-go/ext/README.md @@ -33,6 +33,8 @@ Encoding utilities for marshalling data into standardized representations. ### Base64.Decode +**Introduced in version 0 (cost support in version 1)** + Decodes base64-encoded string to bytes. This function will return an error if the string input is not @@ -47,6 +49,8 @@ Examples: ### Base64.Encode +**Introduced in version 0 (cost support in version 1)** + Encodes bytes to a base64-encoded string. base64.encode() -> @@ -80,6 +84,8 @@ intended; however, there is some chance for collision. ### Math.Greatest +**Introduced in version 0 (cost support in version 3)** + Returns the greatest valued number present in the arguments to the macro. Greatest is a variable argument count macro which must take at least one @@ -107,6 +113,8 @@ Examples: ### Math.Least +**Introduced in version 0 (cost support in version 3)** + Returns the least valued number present in the arguments to the macro. Least is a variable argument count macro which must take at least one diff --git a/vendor/github.com/google/cel-go/ext/encoders.go b/vendor/github.com/google/cel-go/ext/encoders.go index 10beea4f13..97fc932a57 100644 --- a/vendor/github.com/google/cel-go/ext/encoders.go +++ b/vendor/github.com/google/cel-go/ext/encoders.go @@ -16,12 +16,15 @@ package ext import ( "encoding/base64" + "encoding/json" "fmt" "math" "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/interpreter" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" ) @@ -106,6 +109,12 @@ func (lib *encoderLib) CompileOptions() []cel.EnvOption { }))), } if lib.version >= 1 { + estimators := []checker.CostOption{ + checker.OverloadCostEstimate("base64_decode_string", estimateDecode), + checker.OverloadCostEstimate("base64_encode_bytes", estimateEncode), + checker.OverloadCostEstimate("json_encode_dyn", estimateJSONEncode), + } + opts = append(opts, cel.CostEstimatorOptions(estimators...)) opts = append(opts, cel.Function("json.encode", cel.Overload("json_encode_dyn", []*cel.Type{cel.DynType}, cel.StringType, @@ -117,8 +126,17 @@ func (lib *encoderLib) CompileOptions() []cel.EnvOption { return opts } -func (*encoderLib) ProgramOptions() []cel.ProgramOption { - return []cel.ProgramOption{} +func (lib *encoderLib) ProgramOptions() []cel.ProgramOption { + var opts []cel.ProgramOption + if lib.version >= 1 { + trackers := []interpreter.CostTrackerOption{ + interpreter.OverloadCostTracker("base64_decode_string", trackDecode), + interpreter.OverloadCostTracker("base64_encode_bytes", trackEncode), + interpreter.OverloadCostTracker("json_encode_dyn", trackJSONEncode), + } + opts = append(opts, cel.CostTrackerOptions(trackers...)) + } + return opts } func base64DecodeString(str string) ([]byte, error) { @@ -136,6 +154,71 @@ func base64EncodeBytes(bytes []byte) (string, error) { return base64.StdEncoding.EncodeToString(bytes), nil } +func estimateEncode(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) != 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + cost := sz.MultiplyByCostFactor(stringCostFactor).Add(callCostEstimate) + resSize := estimateEncodeSize(sz) + return &checker.CallEstimate{CostEstimate: cost, ResultSize: &resSize} +} + +func estimateJSONEncode(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) != 1 { + return nil + } + size := estimateJSONEncodeSize() + return &checker.CallEstimate{CostEstimate: checker.UnknownCostEstimate(), ResultSize: &size} +} + +func estimateDecode(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) != 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + cost := sz.MultiplyByCostFactor(stringCostFactor).Add(callCostEstimate) + resSize := estimateDecodeSize(sz) + return &checker.CallEstimate{CostEstimate: cost, ResultSize: &resSize} +} + +func trackEncode(args []ref.Val, _ ref.Val) *uint64 { + sz := actualSize(args[0]) + cost := uint64(math.Ceil(float64(sz)*stringCostFactor)) + callCost + return &cost +} + +func trackJSONEncode(args []ref.Val, _ ref.Val) *uint64 { + maxCost := uint64(math.MaxUint64) + return &maxCost +} + +func trackDecode(args []ref.Val, _ ref.Val) *uint64 { + sz := actualSize(args[0]) + cost := uint64(math.Ceil(float64(sz)*stringCostFactor)) + callCost + return &cost +} + +func estimateEncodeSize(sz checker.SizeEstimate) checker.SizeEstimate { + minVal := (sz.Min*4 + 2) / 3 + maxVal := (sz.Max*4 + 2) / 3 + if sz.Max > math.MaxUint64/4 { + maxVal = math.MaxUint64 + } + return checker.SizeEstimate{Min: minVal, Max: maxVal} +} + +func estimateJSONEncodeSize() checker.SizeEstimate { + // TODO: provide a more sophisticated size estimate based on the CEL value's type. + return checker.UnknownSizeEstimate() +} + +func estimateDecodeSize(sz checker.SizeEstimate) checker.SizeEstimate { + minVal := sz.Min * 3 / 4 + maxVal := sz.Max * 3 / 4 + return checker.SizeEstimate{Min: minVal, Max: maxVal} +} + func jsonEncodeValue(val ref.Val) (string, error) { native, err := val.ConvertToNative(types.JSONValueType) if err != nil { @@ -149,5 +232,14 @@ func jsonEncodeValue(val ref.Val) (string, error) { if err != nil { return "", err } + var obj interface{} + if err := json.Unmarshal(jsonBytes, &obj); err != nil { + return "", fmt.Errorf("unmarshaling protojson: %w", err) + } + // Re-marshal with standard json.Marshal for deterministic compact output + jsonBytes, err = json.Marshal(obj) + if err != nil { + return "", fmt.Errorf("re-marshaling value: %w", err) + } return string(jsonBytes), nil } diff --git a/vendor/github.com/google/cel-go/ext/lists.go b/vendor/github.com/google/cel-go/ext/lists.go index b4ac258995..3d0e676426 100644 --- a/vendor/github.com/google/cel-go/ext/lists.go +++ b/vendor/github.com/google/cel-go/ext/lists.go @@ -363,23 +363,45 @@ func (lib listsLib) CompileOptions() []cel.EnvOption { if lib.version >= 3 { estimators := []checker.CostOption{ checker.OverloadCostEstimate("list_slice", estimateListSlice), - checker.OverloadCostEstimate("list_flatten", estimateListFlatten), - checker.OverloadCostEstimate("list_flatten_int", estimateListFlatten), checker.OverloadCostEstimate("lists_range", estimateListsRange), checker.OverloadCostEstimate("list_reverse", estimateListReverse), - checker.OverloadCostEstimate("list_distinct", estimateListDistinct), } - for _, t := range comparableTypes { + if lib.version == 3 { estimators = append(estimators, - checker.OverloadCostEstimate( - fmt.Sprintf("list_%s_sort", t.TypeName()), - estimateListSort(t), - ), - checker.OverloadCostEstimate( - fmt.Sprintf("list_%s_sortByAssociatedKeys", t.TypeName()), - estimateListSortBy(t), - ), + checker.OverloadCostEstimate("list_flatten", estimateListFlattenLegacy), + checker.OverloadCostEstimate("list_flatten_int", estimateListFlattenLegacy), + checker.OverloadCostEstimate("list_distinct", estimateListDistinctLegacy), + ) + for _, t := range comparableTypes { + estimators = append(estimators, + checker.OverloadCostEstimate( + fmt.Sprintf("list_%s_sort", t.TypeName()), + estimateListSortLegacy(t), + ), + checker.OverloadCostEstimate( + fmt.Sprintf("list_%s_sortByAssociatedKeys", t.TypeName()), + estimateListSortByLegacy(t), + ), + ) + } + } else { + estimators = append(estimators, + checker.OverloadCostEstimate("list_flatten", estimateListFlatten), + checker.OverloadCostEstimate("list_flatten_int", estimateListFlatten), + checker.OverloadCostEstimate("list_distinct", estimateListDistinct), ) + for _, t := range comparableTypes { + estimators = append(estimators, + checker.OverloadCostEstimate( + fmt.Sprintf("list_%s_sort", t.TypeName()), + estimateListSort(t), + ), + checker.OverloadCostEstimate( + fmt.Sprintf("list_%s_sortByAssociatedKeys", t.TypeName()), + estimateListSortBy(t), + ), + ) + } } opts = append(opts, cel.CostEstimatorOptions(estimators...)) } @@ -391,15 +413,23 @@ func (lib listsLib) CompileOptions() []cel.EnvOption { func (lib *listsLib) ProgramOptions() []cel.ProgramOption { var opts []cel.ProgramOption if lib.version >= 3 { - // TODO: Add cost trackers for list operations trackers := []interpreter.CostTrackerOption{ interpreter.OverloadCostTracker("list_slice", trackListOutputSize), - interpreter.OverloadCostTracker("list_flatten", trackListFlatten), - interpreter.OverloadCostTracker("list_flatten_int", trackListFlatten), interpreter.OverloadCostTracker("lists_range", trackListOutputSize), interpreter.OverloadCostTracker("list_reverse", trackListOutputSize), interpreter.OverloadCostTracker("list_distinct", trackListDistinct), } + if lib.version == 3 { + trackers = append(trackers, + interpreter.OverloadCostTracker("list_flatten", trackListFlattenLegacy), + interpreter.OverloadCostTracker("list_flatten_int", trackListFlattenLegacy), + ) + } else { + trackers = append(trackers, + interpreter.OverloadCostTracker("list_flatten", trackListFlatten), + interpreter.OverloadCostTracker("list_flatten_int", trackListFlatten), + ) + } for _, t := range comparableTypes { trackers = append(trackers, interpreter.OverloadCostTracker( @@ -657,8 +687,27 @@ func estimateListReverse(estimator checker.CostEstimator, target *checker.AstNod return estimateAllocatingListCall(1, estimateSize(estimator, *target)) } -// estimateListFlatten computes an O(n) flatten operation with a cost factor proportional to the flatten depth. +// estimateListFlatten computes an O(n) flatten operation with a cost factor proportional to the total number of flattened items. func estimateListFlatten(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) > 1 { + return nil + } + depth := uint64(1) + if len(args) == 1 { + depth = nodeAsUintValue(args[0], math.MaxUint) + } + var resSize checker.SizeEstimate + if (*target).Expr() != nil && (*target).Expr().Kind() == ast.ListKind { + szVal := estimateLiteralFlattenSize((*target).Expr(), depth) + resSize = checker.FixedSizeEstimate(szVal) + } else { + resSize = estimateFlattenSize(estimator, *target, depth) + } + cost := resSize.AsCost() + return estimateListCallWithDirectCost(cost, resSize, true) +} + +func estimateListFlattenLegacy(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { if target == nil || len(args) > 1 { return nil } @@ -669,6 +718,42 @@ func estimateListFlatten(estimator checker.CostEstimator, target *checker.AstNod return estimateAllocatingListCall(float64(depth), estimateSize(estimator, *target)) } +func estimateFlattenSize(estimator checker.CostEstimator, node checker.AstNode, depth uint64) checker.SizeEstimate { + sz := estimateSize(estimator, node) + if depth == 0 { + return sz + } + tType := node.Type() + if tType.Kind() != types.ListKind || len(tType.Parameters()) == 0 { + return sz + } + elemType := tType.Parameters()[0] + elemNode := pathAstNode{ + path: append(append([]string(nil), node.Path()...), "@items"), + t: elemType, + } + flatElemSize := estimateFlattenSize(estimator, elemNode, depth-1) + return sz.Multiply(flatElemSize) +} + +func estimateLiteralFlattenSize(expr ast.Expr, depth uint64) uint64 { + if depth == 0 { + if expr.Kind() == ast.ListKind { + return uint64(expr.AsList().Size()) + } + return 1 + } + if expr.Kind() != ast.ListKind { + return 1 + } + listExpr := expr.AsList() + totalSize := uint64(0) + for _, el := range listExpr.Elements() { + totalSize += estimateLiteralFlattenSize(el, depth-1) + } + return totalSize +} + // Compute an O(n^2) with a cost factor of 2, equivalent to sets.contains with a result list // which can vary in size from 1 element to the original list size. func estimateListDistinct(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { @@ -676,8 +761,23 @@ func estimateListDistinct(estimator checker.CostEstimator, target *checker.AstNo return nil } sz := estimateSize(estimator, *target) - costFactor := 2.0 - return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) + elemType := types.DynType + tType := (*target).Type() + if tType.Kind() == types.ListKind && len(tType.Parameters()) > 0 { + elemType = tType.Parameters()[0] + } + itemSize := estimateItemSize(estimator, *target) + elemCost := estimateElementEqualityCost(estimator, elemType, itemSize) + + costSize := sz.Multiply(sz) + cost := costSize.MultiplyByCost(elemCost).MultiplyByCostFactor(2.0) + + minSize := uint64(0) + if sz.Min > 0 { + minSize = 1 + } + resultSize := checker.SizeEstimate{Min: minSize, Max: sz.Max} + return estimateListCallWithDirectCost(cost, resultSize, true) } // estimateListSort computes an O(n^2) sort operation with a cost factor of 2 for the equality @@ -698,37 +798,53 @@ func estimateListSortBy(u *types.Type) checker.FunctionEstimator { if target == nil || len(args) != 1 { return nil } - // Estimate the size of the list used as the sort index - return estimateListSortCost(estimator, args[0], u) + // Estimate the size of the list used as the sort index, using target to resolve item size hints. + return estimateListSortByCost(estimator, *target, args[0], u) } } +func estimateListSortByCost(estimator checker.CostEstimator, target checker.AstNode, keysNode checker.AstNode, elemType *types.Type) *checker.CallEstimate { + sz := estimateSize(estimator, keysNode) + itemSize := estimateItemSize(estimator, target) + elemCost := estimateElementEqualityCost(estimator, elemType, itemSize) + + costSize := sz.Multiply(sz) + cost := costSize.MultiplyByCost(elemCost).MultiplyByCostFactor(2.0) + return estimateListCallWithDirectCost(cost, sz, true) +} + // estimateListSortCost estimates an O(n^2) sort operation with a cost factor of 2 for the equality // operations which occur during the sort computation. func estimateListSortCost(estimator checker.CostEstimator, node checker.AstNode, elemType *types.Type) *checker.CallEstimate { sz := estimateSize(estimator, node) - costFactor := 2.0 - switch elemType { - case types.StringType, types.BytesType: - costFactor += common.StringTraversalCostFactor - } - return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) + itemSize := estimateItemSize(estimator, node) + elemCost := estimateElementEqualityCost(estimator, elemType, itemSize) + + costSize := sz.Multiply(sz) + cost := costSize.MultiplyByCost(elemCost).MultiplyByCostFactor(2.0) + return estimateListCallWithDirectCost(cost, sz, true) } // estimateAllocatingListCall computes cost as a function of the size of the result list with a // baseline cost for the call dispatch and the associated list allocation. func estimateAllocatingListCall(costFactor float64, listSize checker.SizeEstimate) *checker.CallEstimate { - return estimateListCall(costFactor, listSize, true) + return estimateListCallWithResultSize(costFactor, listSize, listSize, true) } -// estimateListCall computes cost as a function of the size of the target list and whether the -// call allocates memory. -func estimateListCall(costFactor float64, listSize checker.SizeEstimate, allocates bool) *checker.CallEstimate { - cost := listSize.MultiplyByCostFactor(costFactor).Add(callCostEstimate) +// estimateListCallWithResultSize computes cost as a function of the size of the target list and whether the +// call allocates memory, using a separate result size estimate for the output list. +func estimateListCallWithResultSize(costFactor float64, costSize checker.SizeEstimate, resultSize checker.SizeEstimate, allocates bool) *checker.CallEstimate { + cost := costSize.MultiplyByCostFactor(costFactor) + return estimateListCallWithDirectCost(cost, resultSize, allocates) +} + +// estimateListCallWithDirectCost computes cost using a pre-calculated CostEstimate and a separate result size estimate. +func estimateListCallWithDirectCost(cost checker.CostEstimate, resultSize checker.SizeEstimate, allocates bool) *checker.CallEstimate { if allocates { cost = cost.Add(checker.FixedCostEstimate(common.ListCreateBaseCost)) } - return &checker.CallEstimate{CostEstimate: cost, ResultSize: &listSize} + cost = cost.Add(callCostEstimate) + return &checker.CallEstimate{CostEstimate: cost, ResultSize: &resultSize} } // trackListOutputSize computes cost as a function of the size of the result list. @@ -736,9 +852,13 @@ func trackListOutputSize(_ []ref.Val, result ref.Val) *uint64 { return trackAllocatingListCall(1, actualSize(result)) } -// trackListFlatten computes cost as a function of the size of the result list and the depth of -// the flatten operation. -func trackListFlatten(args []ref.Val, _ ref.Val) *uint64 { +// trackListFlatten computes cost as a function of the size of the result list. +func trackListFlatten(args []ref.Val, result ref.Val) *uint64 { + resSize := actualSize(result) + return trackAllocatingListCall(1.0, resSize) +} + +func trackListFlattenLegacy(args []ref.Val, _ ref.Val) *uint64 { depth := 1.0 if len(args) == 2 { depth = float64(args[1].(types.Int)) @@ -785,3 +905,99 @@ func trackAllocatingListCall(costFactor float64, size uint64) *uint64 { cost := safeAdd(uint64(float64(size)*costFactor), callCost, common.ListCreateBaseCost) return &cost } + +func estimateListDistinctLegacy(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) != 0 { + return nil + } + sz := estimateSize(estimator, *target) + costFactor := 2.0 + tType := (*target).Type() + if tType.Kind() == types.ListKind && len(tType.Parameters()) > 0 { + elemType := tType.Parameters()[0] + if elemType.Kind() == types.StringKind || elemType.Kind() == types.BytesKind { + costFactor += common.StringTraversalCostFactor + } + } + return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) +} + +func estimateListSortLegacy(t *types.Type) checker.FunctionEstimator { + return func(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) != 0 { + return nil + } + return estimateListSortCostLegacy(estimator, *target, t) + } +} + +func estimateListSortByLegacy(u *types.Type) checker.FunctionEstimator { + return func(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if target == nil || len(args) != 1 { + return nil + } + return estimateListSortCostLegacy(estimator, args[0], u) + } +} + +func estimateListSortCostLegacy(estimator checker.CostEstimator, node checker.AstNode, elemType *types.Type) *checker.CallEstimate { + sz := estimateSize(estimator, node) + costFactor := 2.0 + switch elemType { + case types.StringType, types.BytesType: + costFactor += common.StringTraversalCostFactor + } + return estimateAllocatingListCall(costFactor, sz.Multiply(sz)) +} + +type pathAstNode struct { + path []string + t *types.Type +} + +func (p pathAstNode) Path() []string { + return p.path +} + +func (p pathAstNode) Type() *types.Type { + return p.t +} + +func (p pathAstNode) Expr() ast.Expr { + return nil +} + +func (p pathAstNode) ComputedSize() *checker.SizeEstimate { + return nil +} + +func estimateItemSize(estimator checker.CostEstimator, node checker.AstNode) checker.SizeEstimate { + path := node.Path() + if len(path) == 0 { + return checker.SizeEstimate{Min: 0, Max: math.MaxUint64} + } + elemType := types.DynType + tType := node.Type() + if tType.Kind() == types.ListKind && len(tType.Parameters()) > 0 { + elemType = tType.Parameters()[0] + } + itemNode := pathAstNode{ + path: append(append([]string(nil), path...), "@items"), + t: elemType, + } + if l := estimator.EstimateSize(itemNode); l != nil { + return *l + } + return checker.SizeEstimate{Min: 0, Max: math.MaxUint64} +} + +func estimateElementEqualityCost(estimator checker.CostEstimator, elemType *types.Type, itemSize checker.SizeEstimate) checker.CostEstimate { + switch elemType.Kind() { + case types.StringKind, types.BytesKind: + return itemSize.MultiplyByCostFactor(common.StringTraversalCostFactor) + case types.ListKind, types.MapKind, types.StructKind: + return checker.UnknownCostEstimate() + default: + return checker.FixedCostEstimate(1) + } +} diff --git a/vendor/github.com/google/cel-go/ext/math.go b/vendor/github.com/google/cel-go/ext/math.go index 6df8e37736..e67b205def 100644 --- a/vendor/github.com/google/cel-go/ext/math.go +++ b/vendor/github.com/google/cel-go/ext/math.go @@ -20,10 +20,12 @@ import ( "strings" "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" "github.com/google/cel-go/common/ast" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/traits" + "github.com/google/cel-go/interpreter" ) // Math returns a cel.EnvOption to configure namespaced math helper macros and @@ -339,9 +341,9 @@ import ( // // Examples: // -// math.sqrt(81) // returns 9.0 -// math.sqrt(985.25) // returns 31.388692231439016 -// math.sqrt(-15) // returns NaN +// math.sqrt(81) // returns 9.0 +// math.sqrt(985.25) // returns 31.388692231439016 +// math.sqrt(-15) // returns NaN func Math(options ...MathOption) cel.EnvOption { m := &mathLib{version: math.MaxUint32} for _, o := range options { @@ -580,12 +582,35 @@ func (lib *mathLib) CompileOptions() []cel.EnvOption { ), ) } + if lib.version >= 3 { + estimators := []checker.CostOption{ + checker.OverloadCostEstimate("math_@min_list_double", estimateMathListCost), + checker.OverloadCostEstimate("math_@min_list_int", estimateMathListCost), + checker.OverloadCostEstimate("math_@min_list_uint", estimateMathListCost), + checker.OverloadCostEstimate("math_@max_list_double", estimateMathListCost), + checker.OverloadCostEstimate("math_@max_list_int", estimateMathListCost), + checker.OverloadCostEstimate("math_@max_list_uint", estimateMathListCost), + } + opts = append(opts, cel.CostEstimatorOptions(estimators...)) + } return opts } // ProgramOptions implements the Library interface method. -func (*mathLib) ProgramOptions() []cel.ProgramOption { - return []cel.ProgramOption{} +func (lib *mathLib) ProgramOptions() []cel.ProgramOption { + var opts []cel.ProgramOption + if lib.version >= 3 { + trackers := []interpreter.CostTrackerOption{ + interpreter.OverloadCostTracker("math_@min_list_double", trackMathListCost), + interpreter.OverloadCostTracker("math_@min_list_int", trackMathListCost), + interpreter.OverloadCostTracker("math_@min_list_uint", trackMathListCost), + interpreter.OverloadCostTracker("math_@max_list_double", trackMathListCost), + interpreter.OverloadCostTracker("math_@max_list_int", trackMathListCost), + interpreter.OverloadCostTracker("math_@max_list_uint", trackMathListCost), + } + opts = append(opts, cel.CostTrackerOptions(trackers...)) + } + return opts } func mathLeast(meh cel.MacroExprFactory, target ast.Expr, args []ast.Expr) (ast.Expr, *cel.Error) { @@ -723,21 +748,19 @@ func sign(val ref.Val) ref.Val { } } - func sqrt(val ref.Val) ref.Val { switch v := val.(type) { case types.Double: - return types.Double(math.Sqrt(float64(v))) + return types.Double(math.Sqrt(float64(v))) case types.Int: - return types.Double(math.Sqrt(float64(v))) + return types.Double(math.Sqrt(float64(v))) case types.Uint: - return types.Double(math.Sqrt(float64(v))) + return types.Double(math.Sqrt(float64(v))) default: - return types.NewErr("no such overload: sqrt") + return types.NewErr("no such overload: sqrt") } } - func bitAndPairInt(first, second ref.Val) ref.Val { l := first.(types.Int) r := second.(types.Int) @@ -946,3 +969,19 @@ func maybeSuffixError(val ref.Val, suffix string) ref.Val { } return val } + +func estimateMathListCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) != 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + cost := sz.MultiplyByCostFactor(1.0).Add(callCostEstimate) + resultSize := checker.FixedSizeEstimate(1) + return &checker.CallEstimate{CostEstimate: cost, ResultSize: &resultSize} +} + +func trackMathListCost(args []ref.Val, _ ref.Val) *uint64 { + sz := actualSize(args[0]) + cost := safeAdd(sz, callCost) + return &cost +} diff --git a/vendor/github.com/google/cel-go/ext/native.go b/vendor/github.com/google/cel-go/ext/native.go index c30f26ad31..d9f5fab0de 100644 --- a/vendor/github.com/google/cel-go/ext/native.go +++ b/vendor/github.com/google/cel-go/ext/native.go @@ -164,6 +164,10 @@ func fieldNameByTag(structTagToParse string) func(field reflect.StructField) str } } +func isSkippedFieldName(name string) bool { + return name == "" || name == "-" +} + type nativeTypeOptions struct { // fieldNameHandler controls how CEL should perform struct field renames. // This is most commonly used for switching to parsing based off the struct field tag, @@ -286,9 +290,13 @@ func toFieldName(fieldNameHandler NativeTypesFieldNameHandler, f reflect.StructF func (tp *nativeTypeProvider) FindStructFieldNames(typeName string) ([]string, bool) { if t, found := tp.nativeTypes[typeName]; found { fieldCount := t.refType.NumField() - fields := make([]string, fieldCount) + fields := make([]string, 0, fieldCount) for i := 0; i < fieldCount; i++ { - fields[i] = toFieldName(tp.options.fieldNameHandler, t.refType.Field(i)) + fieldName := toFieldName(tp.options.fieldNameHandler, t.refType.Field(i)) + if isSkippedFieldName(fieldName) { + continue + } + fields = append(fields, fieldName) } return fields, true } @@ -509,6 +517,9 @@ func (o *nativeObj) ConvertToNative(typeDesc reflect.Type) (any, error) { continue } fieldName := toFieldName(o.valType.fieldNameHandler, fieldType) + if isSkippedFieldName(fieldName) { + continue + } fieldCELVal := o.NativeToValue(fieldValue.Interface()) fieldJSONVal, err := fieldCELVal.ConvertToNative(jsonValueType) if err != nil { @@ -667,7 +678,9 @@ func newNativeType(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect for idx := 0; idx < refType.NumField(); idx++ { field := refType.Field(idx) fieldName := toFieldName(fieldNameHandler, field) - + if isSkippedFieldName(fieldName) { + continue + } if _, found := fieldNames[fieldName]; found { return nil, fmt.Errorf("invalid field name `%s` in struct `%s`: %w", fieldName, refType.Name(), errDuplicatedFieldName) } else { @@ -737,6 +750,10 @@ func (t *nativeType) Value() any { // fieldByName returns the corresponding reflect.StructField for the give name either by matching // field tag or field name. func (t *nativeType) fieldByName(fieldName string) (reflect.StructField, bool) { + if isSkippedFieldName(fieldName) { + return reflect.StructField{}, false + } + if t.fieldNameHandler == nil { return t.refType.FieldByName(fieldName) } diff --git a/vendor/github.com/google/cel-go/ext/network.go b/vendor/github.com/google/cel-go/ext/network.go index affe59e2d3..bca0657077 100644 --- a/vendor/github.com/google/cel-go/ext/network.go +++ b/vendor/github.com/google/cel-go/ext/network.go @@ -16,13 +16,16 @@ package ext import ( "fmt" + "math" "net/netip" "reflect" "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker" "github.com/google/cel-go/common/ast" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/interpreter" ) const ( @@ -182,7 +185,11 @@ const ( var ( // Definitions for the Opaque Types - IPType = types.NewOpaqueType("net.IP") + + // IPType represents a network IP address. + IPType = types.NewOpaqueType("net.IP") + + // CIDRType represents a CIDR-format network range. CIDRType = types.NewOpaqueType("net.CIDR") ) @@ -196,13 +203,11 @@ func (*networkLib) LibraryName() string { func (*networkLib) CompileOptions() []cel.EnvOption { return []cel.EnvOption{ - // 1. Register Types cel.Types( IPType, CIDRType, ), - // 2. Register Functions cel.Function(cidrFunc, // K8s Parity: Following the pattern, this is "string_to_cidr" cel.Overload("string_to_cidr", []*cel.Type{cel.StringType}, CIDRType, @@ -288,11 +293,58 @@ func (*networkLib) CompileOptions() []cel.EnvOption { networkFormatValidator{funcName: ipFunc, argNum: 0, check: checkIP}, networkFormatValidator{funcName: cidrFunc, argNum: 0, check: checkCIDR}, ), + cel.CostEstimatorOptions( + checker.OverloadCostEstimate("string_to_cidr", estimateNetworkParseCost), + checker.OverloadCostEstimate("cidr_to_string", estimateNetworkNominalStringCost), + checker.OverloadCostEstimate("cidr_contains_cidr", estimateNetworkContainsCIDRCIDRCost), + checker.OverloadCostEstimate("cidr_contains_cidr_string", estimateNetworkContainsCIDRStringCost), + checker.OverloadCostEstimate("cidr_contains_ip_ip", estimateNetworkContainsIPIPCost), + checker.OverloadCostEstimate("cidr_contains_ip_string", estimateNetworkContainsIPStringCost), + checker.OverloadCostEstimate("ip_family", estimateNetworkNominalCost), + checker.OverloadCostEstimate("string_to_ip", estimateNetworkParseCost), + checker.OverloadCostEstimate("cidr_ip", estimateNetworkNominalOpaqueCost), + checker.OverloadCostEstimate("ip_to_string", estimateNetworkNominalStringCost), + checker.OverloadCostEstimate("ip_is_canonical", estimateIPIsCanonicalCost), + checker.OverloadCostEstimate("is_cidr", estimateNetworkParseBoolCost), + checker.OverloadCostEstimate("ip_is_global_unicast", estimateNetworkNominalCost), + checker.OverloadCostEstimate("is_ip", estimateNetworkParseBoolCost), + checker.OverloadCostEstimate("ip_is_link_local_multicast", estimateNetworkNominalCost), + checker.OverloadCostEstimate("ip_is_link_local_unicast", estimateNetworkNominalCost), + checker.OverloadCostEstimate("ip_is_loopback", estimateNetworkNominalCost), + checker.OverloadCostEstimate("cidr_is_mask", estimateNetworkNominalCost), + checker.OverloadCostEstimate("ip_is_unspecified", estimateNetworkNominalCost), + checker.OverloadCostEstimate("cidr_masked", estimateNetworkNominalOpaqueCost), + checker.OverloadCostEstimate("cidr_prefix_length", estimateNetworkNominalCost), + ), } } func (*networkLib) ProgramOptions() []cel.ProgramOption { - return []cel.ProgramOption{} + return []cel.ProgramOption{ + cel.CostTrackerOptions( + interpreter.OverloadCostTracker("string_to_cidr", trackNetworkParseCost), + interpreter.OverloadCostTracker("cidr_to_string", trackNetworkNominalCost), + interpreter.OverloadCostTracker("cidr_contains_cidr", trackNetworkContainsCIDRCIDRCost), + interpreter.OverloadCostTracker("cidr_contains_cidr_string", trackNetworkContainsCIDRStringCost), + interpreter.OverloadCostTracker("cidr_contains_ip_ip", trackNetworkContainsIPIPCost), + interpreter.OverloadCostTracker("cidr_contains_ip_string", trackNetworkContainsIPStringCost), + interpreter.OverloadCostTracker("ip_family", trackNetworkNominalCost), + interpreter.OverloadCostTracker("string_to_ip", trackNetworkParseCost), + interpreter.OverloadCostTracker("cidr_ip", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_to_string", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_is_canonical", trackIPIsCanonicalCost), + interpreter.OverloadCostTracker("is_cidr", trackNetworkParseCost), + interpreter.OverloadCostTracker("ip_is_global_unicast", trackNetworkNominalCost), + interpreter.OverloadCostTracker("is_ip", trackNetworkParseCost), + interpreter.OverloadCostTracker("ip_is_link_local_multicast", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_is_link_local_unicast", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_is_loopback", trackNetworkNominalCost), + interpreter.OverloadCostTracker("cidr_is_mask", trackNetworkNominalCost), + interpreter.OverloadCostTracker("ip_is_unspecified", trackNetworkNominalCost), + interpreter.OverloadCostTracker("cidr_masked", trackNetworkNominalCost), + interpreter.OverloadCostTracker("cidr_prefix_length", trackNetworkNominalCost), + ), + } } // networkAdapter adapts netip types while preserving existing adapters. @@ -478,8 +530,7 @@ func parseIPAddr(raw string) (netip.Addr, error) { return addr, nil } -// --- Opaque Type Wrappers --- - +// IP represents an IP address type. type IP struct { netip.Addr } @@ -527,6 +578,13 @@ func (i IP) Value() any { return i.Addr } +// Size returns the size of the IP address in bytes. +// /Used in the size estimation of the runtime cost. +func (i IP) Size() ref.Val { + return types.Int(int64(math.Ceil(float64(i.Addr.BitLen()) / 8))) +} + +// CIDR represents the CIDR network mask format. type CIDR struct { netip.Prefix } @@ -574,6 +632,12 @@ func (c CIDR) Value() any { return c.Prefix } +// Size returns the size of the CIDR prefix address in bytes. +// Used in the size estimation of the runtime cost. +func (c CIDR) Size() ref.Val { + return types.Int(int64(math.Ceil(float64(c.Prefix.Bits()) / 8))) +} + // --- Static Validators --- type argChecker func(e *cel.Env, call, arg ast.Expr) error @@ -617,3 +681,130 @@ func checkCIDR(e *cel.Env, call, arg ast.Expr) error { _, err := parseCIDR(pattern) return err } + +// Cost estimation functions for network extensions. + +func estimateNetworkParseCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + resultSize := rangedSizeEstimate(4, 16) + return callEstimate(sz.MultiplyByCostFactor(stringCostFactor), &resultSize) +} + +func estimateNetworkParseBoolCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + return callEstimate(sz.MultiplyByCostFactor(stringCostFactor), nil) +} + +func estimateIPIsCanonicalCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := estimateSize(estimator, args[0]) + return callEstimate(sz.MultiplyByCostFactor(2*stringCostFactor), nil) +} + +func estimateNetworkNominalCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + return callEstimate(callCostEstimate, nil) +} + +func estimateNetworkNominalOpaqueCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + resultSize := rangedSizeEstimate(4, 16) + return callEstimate(callCostEstimate, &resultSize) +} + +func estimateNetworkNominalStringCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + resultSize := rangedSizeEstimate(3, 45) + return callEstimate(callCostEstimate, &resultSize) +} + +func estimateNetworkContainsIPIPCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + sz := rangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(stringCostFactor) + return callEstimate(ipCompCost, nil) +} + +func estimateNetworkContainsIPStringCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := rangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(stringCostFactor) + argSz := estimateSize(estimator, args[0]) + ipCompCost = ipCompCost.Add(argSz.MultiplyByCostFactor(stringCostFactor)) + return callEstimate(ipCompCost, nil) +} + +func estimateNetworkContainsCIDRCIDRCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + sz := rangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(stringCostFactor) + ipCompCost = ipCompCost.Add(sz.MultiplyByCostFactor(stringCostFactor)) + // K8s adds one for the extra IP traversal + ipCompCost = ipCompCost.Add(callCostEstimate) + return callEstimate(ipCompCost, nil) +} + +func estimateNetworkContainsCIDRStringCost(estimator checker.CostEstimator, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate { + if len(args) < 1 { + return nil + } + sz := rangedSizeEstimate(4, 16) + ipCompCost := sz.Add(sz).MultiplyByCostFactor(stringCostFactor) + ipCompCost = ipCompCost.Add(sz.MultiplyByCostFactor(stringCostFactor)) + argSz := estimateSize(estimator, args[0]) + ipCompCost = ipCompCost.Add(argSz.MultiplyByCostFactor(stringCostFactor)) + // K8s adds one for the extra IP traversal + ipCompCost = ipCompCost.Add(callCostEstimate) + return callEstimate(ipCompCost, nil) +} + +// Runtime cost tracking functions for network extensions. + +func trackNetworkParseCost(args []ref.Val, result ref.Val) *uint64 { + cost := uint64(math.Ceil(float64(actualSize(args[0])) * stringCostFactor)) + return &cost +} + +func trackIPIsCanonicalCost(args []ref.Val, result ref.Val) *uint64 { + cost := uint64(math.Ceil(float64(actualSize(args[0])) * 2 * stringCostFactor)) + return &cost +} + +func trackNetworkNominalCost(args []ref.Val, result ref.Val) *uint64 { + return &callCost +} + +func trackNetworkContainsIPIPCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := actualSize(args[0]) + cost := uint64(math.Ceil(float64(cidrSize+cidrSize) * stringCostFactor)) + return &cost +} + +func trackNetworkContainsIPStringCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := actualSize(args[0]) + otherSize := actualSize(args[1]) + cost := uint64(math.Ceil(float64(cidrSize+cidrSize) * stringCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(otherSize)*stringCostFactor))) + return &cost +} + +func trackNetworkContainsCIDRCIDRCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := actualSize(args[0]) + cost := uint64(math.Ceil(float64(cidrSize+cidrSize) * stringCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(cidrSize)*stringCostFactor)), 1) + return &cost +} + +func trackNetworkContainsCIDRStringCost(args []ref.Val, result ref.Val) *uint64 { + cidrSize := actualSize(args[0]) + otherSize := actualSize(args[1]) + cost := uint64(math.Ceil(float64(cidrSize+cidrSize) * stringCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(cidrSize)*stringCostFactor)), 1) + cost = safeAdd(cost, uint64(math.Ceil(float64(otherSize)*stringCostFactor))) + return &cost +} diff --git a/vendor/github.com/google/cel-go/interpreter/BUILD.bazel b/vendor/github.com/google/cel-go/interpreter/BUILD.bazel index 1274a6f12d..40ac2ba696 100644 --- a/vendor/github.com/google/cel-go/interpreter/BUILD.bazel +++ b/vendor/github.com/google/cel-go/interpreter/BUILD.bazel @@ -9,6 +9,7 @@ go_library( name = "go_default_library", srcs = [ "activation.go", + "async.go", "attribute_patterns.go", "attributes.go", "decorators.go", @@ -46,6 +47,7 @@ go_test( name = "go_default_test", srcs = [ "activation_test.go", + "async_test.go", "attribute_patterns_test.go", "attributes_test.go", "frame_test.go", @@ -65,6 +67,7 @@ go_test( "//common/operators:go_default_library", "//common/stdlib:go_default_library", "//common/types:go_default_library", + "//common/types/ref:go_default_library", "//parser:go_default_library", "//test:go_default_library", "//test/proto2pb:go_default_library", diff --git a/vendor/github.com/google/cel-go/interpreter/activation.go b/vendor/github.com/google/cel-go/interpreter/activation.go index 023efbb807..bc9296ed4e 100644 --- a/vendor/github.com/google/cel-go/interpreter/activation.go +++ b/vendor/github.com/google/cel-go/interpreter/activation.go @@ -134,6 +134,19 @@ func (a *hierarchicalActivation) Unwrap() Activation { return a.parent } +// IsLocalVariable reports whether the variable name is locally bound in the hierarchical activation. +func (a *hierarchicalActivation) IsLocalVariable(name string) bool { + if holder, ok := a.child.(localVariableHolder); ok { + if holder.IsLocalVariable(name) { + return true + } + } + if holder, ok := a.parent.(localVariableHolder); ok { + return holder.IsLocalVariable(name) + } + return false +} + // AsPartialActivation checks the child first via direct type assertion (to // avoid recursion through the folder → frame → hierarchicalActivation cycle), // then walks the parent hierarchy via the free function. diff --git a/vendor/github.com/google/cel-go/interpreter/async.go b/vendor/github.com/google/cel-go/interpreter/async.go new file mode 100644 index 0000000000..4e391196bf --- /dev/null +++ b/vendor/github.com/google/cel-go/interpreter/async.go @@ -0,0 +1,530 @@ +// Copyright 2026 Google LLC +// +// 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 interpreter + +import ( + "context" + "encoding/binary" + "fmt" + "hash/fnv" + "math" + "sync" + "sync/atomic" + + "github.com/google/cel-go/common/functions" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +// Async extension function support. +// +// CEL supports `types.Unknown` as a first-class value, and concurrent (async) function execution +// in CEL invokes a stub function which checks for the presence of an existing result which matches +// the function call and call arguments, or which records the 'unexecuted' function and call arguments +// for concurrent execution in a later phase if the result is `types.Unknown` and indicates the +// expression ids of the functions necessary to advance the execution. +// +// This call pattern is repeated iteratively until there are either no more functions to call or no +// progress is made toward resolving the unknowns. + +// AsyncObserver provides callbacks for monitoring the lifecycle of asynchronous function calls. +// +// Implementations must be safe for concurrent use: OnCallStarted is invoked from the evaluator +// goroutine when a call is launched, while OnCallFinished is invoked from the call's own goroutine +// when it completes. The two callbacks therefore run on different goroutines, and OnCallFinished +// callbacks for distinct calls may run concurrently with each other. +type AsyncObserver interface { + // OnCallStarted is called when an asynchronous function is first launched. + OnCallStarted(callID int64, function, overload string, args []ref.Val) + // OnCallFinished is called when an asynchronous function completes. + OnCallFinished(callID int64, function, overload string, res ref.Val) +} + +// AsyncCall describes a pending or completed asynchronous function call. +type AsyncCall interface { + // CallID returns the unique identifier for this async call invocation. + CallID() int64 + // Function returns the name of the function being called. + Function() string + // Overload returns the specific overload ID being invoked. + Overload() string +} + +// evalAsyncFunc is the planned Interpretable for an asynchronous function call. +type evalAsyncFunc struct { + id int64 + function string + overload string + args []InterpretableV2 + impl functions.AsyncOp +} + +// ID implements the Interpretable interface method. +func (fn *evalAsyncFunc) ID() int64 { + return fn.id +} + +// Function returns the name of the function being invoked. +func (fn *evalAsyncFunc) Function() string { + return fn.function +} + +// OverloadID returns the overload id of the function being invoked. +func (fn *evalAsyncFunc) OverloadID() string { + return fn.overload +} + +// Args returns the argument Interpretables for the function call. +func (fn *evalAsyncFunc) Args() []InterpretableV2 { + return fn.args +} + +// Eval implements the Interpretable interface method. +func (fn *evalAsyncFunc) Eval(vars Activation) ref.Val { + return fn.Exec(AsFrame(vars)) +} + +// Exec implements the InterpretableV2 interface method. +func (fn *evalAsyncFunc) Exec(frame *ExecutionFrame) ref.Val { + argVals := make([]ref.Val, len(fn.args)) + var unk *types.Unknown + for i, arg := range fn.args { + argVals[i] = arg.Exec(frame) + if types.IsError(argVals[i]) { + return argVals[i] + } + unk, _ = types.MaybeMergeUnknowns(argVals[i], unk) + } + if unk != nil { + return unk + } + result := frame.ComputeResult(fn.ID(), fn.Function(), fn.OverloadID(), fn.impl, argVals) + return types.LabelErrNode(fn.id, result) +} + +// asyncCallStateTracker manages async call states across re-evaluations of a single program. +type asyncCallStateTracker struct { + mu sync.RWMutex + // calls buckets call states by a composite hash of (node id, overload, string/int/double/uint/bool args). + // A single AST node id may host many concurrently-live calls when it is evaluated inside a + // comprehension (once per element with different arguments), so each bucket may hold more + // than one state. The exact match within a bucket is resolved via asyncCallState.matches, + // which applies CEL's full equality semantics to the arguments. + calls map[uint64][]*asyncCallState + callsByID map[int64]*asyncCallState + nextCallID atomic.Int64 +} + +func newAsyncCallStateTracker() *asyncCallStateTracker { + return &asyncCallStateTracker{ + calls: make(map[uint64][]*asyncCallState), + callsByID: make(map[int64]*asyncCallState), + } +} + +var ( + hashZeroMarker = []byte{0} + hashStringMarker = []byte{'s'} + hashBoolTrueMarker = []byte{'b', 1} + hashBoolFalseMarker = []byte{'b', 0} + hashNumberMarker = []byte{'n'} + hashDefaultMarker = []byte{'x'} +) + +// hashCall computes the composite bucket key for an async call. +// +// Only string, int, double, uint, and bool argument values contribute to the hash. More complex types +// rely on a richer notion of equivalence (e.g. unordered maps, proto equality, custom types) +// that a byte-level hash cannot capture safely, so they are intentionally excluded from the key +// and are instead disambiguated within the bucket by asyncCallState.matches. +func hashCall(id int64, overload string, args []ref.Val) uint64 { + h := fnv.New64a() + var idBuf [8]byte + binary.LittleEndian.PutUint64(idBuf[:], uint64(id)) + h.Write(idBuf[:]) + h.Write([]byte(overload)) + h.Write(hashZeroMarker) + for _, arg := range args { + switch v := arg.(type) { + case types.String: + h.Write(hashStringMarker) + h.Write([]byte(string(v))) + case types.Bool: + if bool(v) { + h.Write(hashBoolTrueMarker) + } else { + h.Write(hashBoolFalseMarker) + } + case types.Int: + h.Write(hashNumberMarker) + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], math.Float64bits(float64(v))) + h.Write(buf[:]) + case types.Uint: + h.Write(hashNumberMarker) + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], math.Float64bits(float64(v))) + h.Write(buf[:]) + case types.Double: + h.Write(hashNumberMarker) + if math.IsNaN(float64(v)) { + h.Write([]byte("NaN")) + h.Write(hashZeroMarker) + continue + } + // Normalize -0.0 to 0.0. Go will treat -0.0 as 0.0 at compile time, + // but the function math.Copysign(0.0, -1.0) can be used to test the -0.0 case. + if v == types.Double(0.0) && math.Signbit(float64(v)) { + v = types.Double(0.0) + } + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], math.Float64bits(float64(v))) + h.Write(buf[:]) + default: + // Value intentionally omitted; bucket membership falls back to matches. + h.Write(hashDefaultMarker) + } + // Separator to avoid cross-argument collisions, e.g. ("a", "bc") vs ("ab", "c"). + h.Write(hashZeroMarker) + } + return h.Sum64() +} + +// findInBucket returns the call state in the bucket matching the same node id and call identity, +// or nil if no match is present. +func findInBucket(bucket []*asyncCallState, id int64, function, overload string, args []ref.Val) *asyncCallState { + for _, acs := range bucket { + if acs.matches(id, function, overload, args) { + return acs + } + } + return nil +} + +// getOrCreate returns the existing call state for the (node id, args) tuple, or registers and +// returns a new one. A newly registered call is assigned a unique callID and counted as pending. +func (t *asyncCallStateTracker) getOrCreate(id int64, function, overload string, argVals []ref.Val, impl functions.AsyncOp, gate *asyncGate) *asyncCallState { + key := hashCall(id, overload, argVals) + + t.mu.RLock() + acs := findInBucket(t.calls[key], id, function, overload, argVals) + t.mu.RUnlock() + if acs != nil { + return acs + } + + t.mu.Lock() + defer t.mu.Unlock() + // Check again in case it was created while waiting for the lock. + if acs := findInBucket(t.calls[key], id, function, overload, argVals); acs != nil { + return acs + } + + // Assign a new unique call ID for this async call. + acs = newAsyncCallState(id, function, overload, argVals, impl) + callID := t.nextCallID.Add(1) + acs.callID = callID + acs.gate = gate + t.calls[key] = append(t.calls[key], acs) + t.callsByID[callID] = acs + return acs +} + +func (t *asyncCallStateTracker) getByID(callID int64) *asyncCallState { + t.mu.RLock() + defer t.mu.RUnlock() + return t.callsByID[callID] +} + +func newAsyncCallState(id int64, function, overload string, argVals []ref.Val, impl functions.AsyncOp) *asyncCallState { + return &asyncCallState{ + id: id, + function: function, + overload: overload, + argVals: argVals, + impl: impl, + } +} + +// asyncCallState tracks the result of a single async function call across multiple re-evaluations. +type asyncCallState struct { + id int64 // AST expression node ID where the call is defined. + callID int64 // Unique incremental tracking ID assigned to this call. + function string + overload string + argVals []ref.Val + impl functions.AsyncOp + + mu sync.RWMutex + started bool + result ref.Val + + gate *asyncGate +} + +// CallID returns the unique identifier for this async call invocation. +func (acs *asyncCallState) CallID() int64 { + return acs.callID +} + +// Function returns the name of the function being called. +func (acs *asyncCallState) Function() string { + return acs.function +} + +// Overload returns the specific overload ID being invoked. +func (acs *asyncCallState) Overload() string { + return acs.overload +} + +// ResultOrUnknown returns the cached result if the call has completed, an Unknown +// with the call ID if pending, or nil if the call has not been started. +func (acs *asyncCallState) ResultOrUnknown() ref.Val { + if acs == nil { + return nil + } + acs.mu.RLock() + defer acs.mu.RUnlock() + if acs.result == nil && acs.started { + return types.NewUnknown(acs.callID, nil) + } + return acs.result +} + +// SetResult sets the completed result for an asynchronous function call. +func (acs *asyncCallState) SetResult(res ref.Val) { + if acs == nil { + return + } + acs.mu.Lock() + defer acs.mu.Unlock() + acs.result = res +} + +// launch returns a call's cached result, or starts the call (subject to the launch limiter) and +// returns an Unknown referencing its callID while the result is pending. +// +// Admission control: when a concurrency semaphore is configured, a launch slot is reserved with a +// non-blocking send. If no slot is free the call is left unstarted and an Unknown is returned; the +// call is retried on a later re-evaluation pass once an in-flight call completes and frees a slot. +// The reservation is non-blocking on purpose — the evaluator runs on a single goroutine, and +// blocking it here while completing calls block on an undrained completion channel would deadlock. +// The slot is held by the launched goroutine and released when it exits, so the number of live +// async goroutines is bounded by the semaphore capacity. +func (t *asyncCallStateTracker) launch(ctx context.Context, acs *asyncCallState, observer AsyncObserver) ref.Val { + if res := acs.ResultOrUnknown(); res != nil { + return res + } + gate := acs.gate + if !gate.TryAcquire() { + return types.NewUnknown(acs.callID, nil) + } + acs.mu.Lock() + if acs.started || acs.result != nil { + // Defensive: the evaluator is single-threaded so this should not happen, but if it does, + // return the reserved slot rather than leak it. + acs.mu.Unlock() + gate.Release() + return types.NewUnknown(acs.callID, nil) + } + acs.started = true + acs.mu.Unlock() + + if observer != nil { + observer.OnCallStarted(acs.callID, acs.function, acs.overload, acs.argVals) + } + go func() { + defer func() { + if observer != nil { + observer.OnCallFinished(acs.callID, acs.function, acs.overload, acs.ResultOrUnknown()) + } + gate.Complete(ctx, acs.callID) + }() + + ch := acs.impl(ctx, acs.argVals...) + // Early terminate with a CEL error when an implementation returns an empty channel. + if ch == nil { + acs.SetResult(types.NewErrFromString( + fmt.Sprintf("function %s returned an empty channel", acs.function))) + return + } + // Wait for the async computation to finish or for the context to be cancelled. + select { + case r, ok := <-ch: + if !ok { + acs.SetResult(types.NewErrFromString( + fmt.Sprintf("function %s returned an empty channel", acs.function))) + return + } + acs.SetResult(r) + case <-ctx.Done(): + // Evaluation context cancelled before the async operation completed. + acs.SetResult(types.WrapErr(context.Cause(ctx))) + } + }() + return types.NewUnknown(acs.callID, nil) +} + +// matches reports whether two call states refer to the same function, overload, and arguments. +func (acs *asyncCallState) matches(id int64, function, overload string, args []ref.Val) bool { + if acs == nil { + return false + } + if acs.id != id || acs.function != function || acs.overload != overload { + return false + } + if len(acs.argVals) != len(args) { + return false + } + for i, v := range acs.argVals { + otherV := args[i] + if types.Equal(v, otherV) == types.True { + continue + } + if n, ok := v.(types.Double); ok { + // Treat NaN as equivalent for the sake of function dispatch equality. + if otherN, ok := otherV.(types.Double); ok && math.IsNaN(float64(n)) && math.IsNaN(float64(otherN)) { + continue + } + } + return false + } + return true +} + +// trackerShrinkThreshold is the entry count above which a released tracker's maps are reallocated +// rather than cleared in place, so the pool does not retain a large backing array indefinitely. +const trackerShrinkThreshold = 1024 + +// asyncCallStateTrackerPool provides a synchronized pool of asyncCallStateTrackers. +type asyncCallTrackerPool struct { + sync.Pool +} + +func (pool *asyncCallTrackerPool) create() *asyncCallStateTracker { + return pool.Get().(*asyncCallStateTracker) +} + +func (pool *asyncCallTrackerPool) release(tracker *asyncCallStateTracker) { + if tracker == nil { + return + } + tracker.mu.Lock() + // Clearing with delete reuses the backing arrays, which is ideal for the common case but pins + // a large allocation in the pool after a wide fan-out (e.g. an async call over a big list). + // Past a threshold, reallocate so the high-water-mark memory is released to the GC instead of + // being retained by the pooled tracker. + if len(tracker.calls) > trackerShrinkThreshold || len(tracker.callsByID) > trackerShrinkThreshold { + tracker.calls = make(map[uint64][]*asyncCallState) + tracker.callsByID = make(map[int64]*asyncCallState) + } else { + for k := range tracker.calls { + delete(tracker.calls, k) + } + for k := range tracker.callsByID { + delete(tracker.callsByID, k) + } + } + tracker.nextCallID.Store(0) + tracker.mu.Unlock() + pool.Pool.Put(tracker) +} + +func newAsyncCallTrackerPool() *asyncCallTrackerPool { + return &asyncCallTrackerPool{ + Pool: sync.Pool{ + New: func() any { + return newAsyncCallStateTracker() + }, + }, + } +} + +var asyncCallStateTrackerPool = newAsyncCallTrackerPool() + +// asyncGate coordinates async call admission control and completion signaling. +type asyncGate struct { + semaphore chan struct{} + completions chan<- int64 + activeCalls atomic.Int32 +} + +func newAsyncGate(maxConcurrency int, completions chan<- int64) *asyncGate { + var sem chan struct{} + if maxConcurrency > 0 { + sem = make(chan struct{}, maxConcurrency) + } + return &asyncGate{ + semaphore: sem, + completions: completions, + } +} + +// TryAcquire attempts to acquire a concurrency slot and increments the active calls count. +func (g *asyncGate) TryAcquire() bool { + if g == nil { + return true + } + if g.semaphore != nil { + select { + case g.semaphore <- struct{}{}: + default: + return false + } + } + g.activeCalls.Add(1) + return true +} + +// Release releases a concurrency slot and decrements the active calls count (used for defensive recovery). +func (g *asyncGate) Release() { + if g == nil { + return + } + if g.semaphore != nil { + select { + case <-g.semaphore: + default: + } + } + g.activeCalls.Add(-1) +} + +// Complete releases a concurrency slot and notifies completions. +func (g *asyncGate) Complete(ctx context.Context, callID int64) { + if g == nil { + return + } + g.Release() + + if g.completions != nil { + // Prioritize context cancellation to prevent racy completion signals. + if ctx.Err() != nil { + return + } + select { + case g.completions <- callID: + case <-ctx.Done(): + } + } +} + +// ActiveCalls returns the number of active asynchronous calls. +func (g *asyncGate) ActiveCalls() int { + if g == nil { + return 0 + } + return int(g.activeCalls.Load()) +} diff --git a/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go b/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go index 41ca5cd219..bbaca52269 100644 --- a/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go +++ b/vendor/github.com/google/cel-go/interpreter/attribute_patterns.go @@ -255,6 +255,9 @@ func (fac *partialAttributeFactory) matchesUnknownPatterns( patterns := vars.UnknownAttributePatterns() candidateIndices := map[int]struct{}{} for _, variable := range variableNames { + if holder, ok := vars.(localVariableHolder); ok && holder.IsLocalVariable(variable) { + continue + } for i, pat := range patterns { if pat.VariableMatches(variable) { if len(qualifiers) == 0 { diff --git a/vendor/github.com/google/cel-go/interpreter/frame.go b/vendor/github.com/google/cel-go/interpreter/frame.go index 3cf2d6b6b5..20ab313c8f 100644 --- a/vendor/github.com/google/cel-go/interpreter/frame.go +++ b/vendor/github.com/google/cel-go/interpreter/frame.go @@ -21,6 +21,8 @@ import ( "sync" "sync/atomic" + "github.com/google/cel-go/common/functions" + "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" ) @@ -52,6 +54,15 @@ type evalContext struct { // cancel cancels the context when the evaluation is finished. cancel context.CancelFunc + + // asyncCalls tracks the state of async call invocations across re-evaluations. + asyncCalls *asyncCallStateTracker + + // gate coordinates async call admission control and completion signaling. + gate *asyncGate + + // observer for monitoring async calls. + observer AsyncObserver } // ExecutionFrame provides the context for a single evaluation of an expression. @@ -93,6 +104,8 @@ func (f *ExecutionFrame) SetContext(ctx context.Context, interruptCheckFrequency } f.ctx = evalContextPool.Get().(*evalContext) f.ctx.ctx, f.ctx.cancel = context.WithCancel(ctx) + f.ctx.asyncCalls = asyncCallStateTrackerPool.create() + f.ctx.gate = &asyncGate{} f.ctx.interrupt = ctx.Done() f.ctx.interruptCheckFrequency = interruptCheckFrequency f.ctx.interruptCheckCount.Store(0) @@ -108,6 +121,10 @@ func (f *ExecutionFrame) Close() { f.ctx.cancel = nil } f.ctx.ctx = nil + f.ctx.gate = nil + asyncCallStateTrackerPool.release(f.ctx.asyncCalls) + f.ctx.asyncCalls = nil + f.ctx.observer = nil f.ctx.interrupt = nil f.ctx.state = nil f.ctx.costs = nil @@ -118,17 +135,19 @@ func (f *ExecutionFrame) Close() { } f.ctx = nil f.parent = nil - switch a := f.Activation.(type) { - case *hierarchicalActivation: - if child, ok := a.child.(*inputActivation); ok { - activationInput.release(child) + if f.Activation != nil { + switch a := f.Activation.(type) { + case *hierarchicalActivation: + if child, ok := a.child.(*inputActivation); ok { + activationInput.release(child) + } + activationStack.release(a) + case *inputActivation: + activationInput.release(a) } - activationStack.release(a) - case *inputActivation: - activationInput.release(a) + f.Activation = nil + frameStack.Put(f) } - f.Activation = nil - frameStack.Put(f) } // Push pushes the given activation onto the activation stack and returns the new frame. @@ -177,6 +196,20 @@ func (f *ExecutionFrame) Unwrap() Activation { return f.Activation } +// IsLocalVariable reports whether the variable name is locally bound in the frame. +func (f *ExecutionFrame) IsLocalVariable(name string) bool { + if holder, ok := f.Activation.(localVariableHolder); ok { + if holder.IsLocalVariable(name) { + return true + } + } + // Search parent scopes + if f.parent != nil { + return f.parent.IsLocalVariable(name) + } + return false +} + // CheckInterrupt returns whether the evaluation has been interrupted. func (f *ExecutionFrame) CheckInterrupt() bool { if f.ctx == nil { @@ -198,6 +231,91 @@ func (f *ExecutionFrame) CheckInterrupt() bool { return false } +// ComputeResult tracks and computes the result of the given asynchronous function. +// +// The first invocation for a given (node id, args) tuple registers the call state and returns an +// Unknown which references the call's unique callID. Subsequent invocations return the cached +// result once the call has completed. Launching background execution is deferred to post-execution +// dispatch via DispatchPendingAsyncCalls. +func (f *ExecutionFrame) ComputeResult(id int64, function, overload string, impl functions.AsyncOp, argVals []ref.Val) ref.Val { + if f.ctx == nil || f.ctx.asyncCalls == nil { + return types.NewErrWithNodeID(id, "asynchronous function calls require concurrent evaluation and cannot be resolved by a synchronous Eval") + } + t := f.ctx.asyncCalls + acs := t.getOrCreate(id, function, overload, argVals, impl, f.ctx.gate) + if res := acs.ResultOrUnknown(); res != nil { + return res + } + return types.NewUnknown(acs.callID, nil) +} + +// DispatchPendingAsyncCalls launches pending asynchronous calls for the specified required call IDs. +func (f *ExecutionFrame) DispatchPendingAsyncCalls(callIDs []int64) { + if f.ctx == nil || f.ctx.asyncCalls == nil { + return + } + t := f.ctx.asyncCalls + for _, callID := range callIDs { + if acs := t.getByID(callID); acs != nil { + t.launch(f.ctx.ctx, acs, f.ctx.observer) + } + } +} + +// ActiveAsyncCalls returns the number of async function calls that have been launched +// but whose completions have not yet been drained. +func (f *ExecutionFrame) ActiveAsyncCalls() int { + if f.ctx == nil || f.ctx.gate == nil { + return 0 + } + return f.ctx.gate.ActiveCalls() +} + +// AsyncCall returns the state of an async call by its callID, or nil if not found. +func (f *ExecutionFrame) AsyncCall(callID int64) AsyncCall { + if f.ctx == nil || f.ctx.asyncCalls == nil { + return nil + } + acs := f.ctx.asyncCalls.getByID(callID) + if acs == nil { + return nil + } + return acs +} + +// SetCompletions configures a channel to receive callIDs when asynchronous evaluations finish. +func (f *ExecutionFrame) SetCompletions(ch chan<- int64) error { + if f.ctx == nil { + return errors.New("asynchronous evaluation options require the execution frame to have a context configured") + } + f.ctx.gate.completions = ch + return nil +} + +// SetAsyncObserver sets the observer for monitoring asynchronous function calls. +func (f *ExecutionFrame) SetAsyncObserver(observer AsyncObserver) error { + if f.ctx == nil { + return errors.New("asynchronous evaluation options require the execution frame to have a context configured") + } + f.ctx.observer = observer + return nil +} + +// SetAsyncMaxConcurrency sets the maximum concurrency for asynchronous function calls. +// +// A non-positive value indicates that concurrency is unbounded. +func (f *ExecutionFrame) SetAsyncMaxConcurrency(n int) error { + if f.ctx == nil { + return errors.New("asynchronous evaluation options require the execution frame to have a context configured") + } + if n > 0 { + f.ctx.gate.semaphore = make(chan struct{}, n) + } else { + f.ctx.gate.semaphore = nil + } + return nil +} + // frameStack provides a synchronized pool of ExecutionFrames. var frameStack = &sync.Pool{ New: func() any { diff --git a/vendor/github.com/google/cel-go/interpreter/interpretable.go b/vendor/github.com/google/cel-go/interpreter/interpretable.go index 5b94e23e6c..906c4f805f 100644 --- a/vendor/github.com/google/cel-go/interpreter/interpretable.go +++ b/vendor/github.com/google/cel-go/interpreter/interpretable.go @@ -438,13 +438,19 @@ func (eq *evalEq) ID() int64 { // Exec implements the InterpretableV2 interface method. func (eq *evalEq) Exec(frame *ExecutionFrame) ref.Val { lVal := eq.lhs.Exec(frame) - rVal := eq.rhs.Exec(frame) - if types.IsUnknownOrError(lVal) { + if types.IsError(lVal) { return lVal } - if types.IsUnknownOrError(rVal) { + rVal := eq.rhs.Exec(frame) + if types.IsError(rVal) { return rVal } + var unk *types.Unknown + unk, _ = types.MaybeMergeUnknowns(lVal, unk) + unk, _ = types.MaybeMergeUnknowns(rVal, unk) + if unk != nil { + return unk + } return types.Equal(lVal, rVal) } @@ -482,13 +488,19 @@ func (ne *evalNe) ID() int64 { // Exec implements the InterpretableV2 interface method. func (ne *evalNe) Exec(frame *ExecutionFrame) ref.Val { lVal := ne.lhs.Exec(frame) - rVal := ne.rhs.Exec(frame) - if types.IsUnknownOrError(lVal) { + if types.IsError(lVal) { return lVal } - if types.IsUnknownOrError(rVal) { + rVal := ne.rhs.Exec(frame) + if types.IsError(rVal) { return rVal } + var unk *types.Unknown + unk, _ = types.MaybeMergeUnknowns(lVal, unk) + unk, _ = types.MaybeMergeUnknowns(rVal, unk) + if unk != nil { + return unk + } return types.Bool(types.Equal(lVal, rVal) != types.True) } @@ -624,15 +636,20 @@ func (bin *evalBinary) ID() int64 { // Exec implements the InterpretableV2 interface method. func (bin *evalBinary) Exec(frame *ExecutionFrame) ref.Val { lVal := bin.lhs.Exec(frame) - rVal := bin.rhs.Exec(frame) - // Early return if any argument to the function is unknown or error. strict := !bin.nonStrict + if strict && types.IsError(lVal) { + return lVal + } + rVal := bin.rhs.Exec(frame) + if strict && types.IsError(rVal) { + return rVal + } if strict { - if types.IsUnknownOrError(lVal) { - return lVal - } - if types.IsUnknownOrError(rVal) { - return rVal + var unk *types.Unknown + unk, _ = types.MaybeMergeUnknowns(lVal, unk) + unk, _ = types.MaybeMergeUnknowns(rVal, unk) + if unk != nil { + return unk } } // If the implementation is bound and the argument value has the right traits required to @@ -697,14 +714,20 @@ func (fn *evalVarArgs) ID() int64 { // Exec implements the InterpretableV2 interface method. func (fn *evalVarArgs) Exec(frame *ExecutionFrame) ref.Val { argVals := make([]ref.Val, len(fn.args)) - // Early return if any argument to the function is unknown or error. strict := !fn.nonStrict + var unk *types.Unknown for i, arg := range fn.args { argVals[i] = arg.Exec(frame) - if strict && types.IsUnknownOrError(argVals[i]) { - return argVals[i] + if strict { + if types.IsError(argVals[i]) { + return argVals[i] + } + unk, _ = types.MaybeMergeUnknowns(argVals[i], unk) } } + if strict && unk != nil { + return unk + } // If the implementation is bound and the argument value has the right traits required to // invoke it, then call the implementation. arg0 := argVals[0] @@ -755,24 +778,32 @@ func (l *evalList) ID() int64 { // Exec implements the InterpretableV2 interface method. func (l *evalList) Exec(frame *ExecutionFrame) ref.Val { elemVals := make([]ref.Val, 0, len(l.elems)) - // If any argument is unknown or error early terminate. + var unk *types.Unknown for i, elem := range l.elems { elemVal := elem.Exec(frame) - if types.IsUnknownOrError(elemVal) { + if types.IsError(elemVal) { return elemVal } + unk, _ = types.MaybeMergeUnknowns(elemVal, unk) if l.hasOptionals && l.optionals[i] { - optVal, ok := elemVal.(*types.Optional) - if !ok { - return types.LabelErrNode(l.id, invalidOptionalElementInit(elemVal)) - } - if !optVal.HasValue() { - continue + if types.IsUnknown(elemVal) { + // skip optional checks for unknown values as they aren't fully resolved yet. + } else { + optVal, ok := elemVal.(*types.Optional) + if !ok { + return types.LabelErrNode(l.id, invalidOptionalElementInit(elemVal)) + } + if !optVal.HasValue() { + continue + } + elemVal = optVal.GetValue() } - elemVal = optVal.GetValue() } elemVals = append(elemVals, elemVal) } + if unk != nil { + return unk + } return types.NewRefValList(l.adapter, elemVals) } @@ -806,17 +837,21 @@ func (m *evalMap) ID() int64 { // Exec implements the InterpretableV2 interface method. func (m *evalMap) Exec(frame *ExecutionFrame) ref.Val { entries := make(map[ref.Val]ref.Val, len(m.keys)) - // If any argument is unknown or error early terminate. + var unk *types.Unknown for i, key := range m.keys { keyVal := key.Exec(frame) - if types.IsUnknownOrError(keyVal) { + if types.IsError(keyVal) { return keyVal } + unk, _ = types.MaybeMergeUnknowns(keyVal, unk) + valVal := m.vals[i].Exec(frame) - if types.IsUnknownOrError(valVal) { + if types.IsError(valVal) { return valVal } - if m.hasOptionals && m.optionals[i] { + unk, _ = types.MaybeMergeUnknowns(valVal, unk) + + if m.hasOptionals && m.optionals[i] && !types.IsUnknown(valVal) { optVal, ok := valVal.(*types.Optional) if !ok { return types.LabelErrNode(m.id, invalidOptionalEntryInit(keyVal, valVal)) @@ -829,6 +864,9 @@ func (m *evalMap) Exec(frame *ExecutionFrame) ref.Val { } entries[keyVal] = valVal } + if unk != nil { + return unk + } return types.NewRefValMap(m.adapter, entries) } @@ -875,13 +913,14 @@ func (o *evalObj) ID() int64 { // Exec implements the InterpretableV2 interface method. func (o *evalObj) Exec(frame *ExecutionFrame) ref.Val { fieldVals := make(map[string]ref.Val, len(o.fields)) - // If any argument is unknown or error early terminate. + var unk *types.Unknown for i, field := range o.fields { val := o.vals[i].Exec(frame) - if types.IsUnknownOrError(val) { + if types.IsError(val) { return val } - if o.hasOptionals && o.optionals[i] { + unk, _ = types.MaybeMergeUnknowns(val, unk) + if o.hasOptionals && o.optionals[i] && !types.IsUnknown(val) { optVal, ok := val.(*types.Optional) if !ok { return types.LabelErrNode(o.id, invalidOptionalEntryInit(field, val)) @@ -894,6 +933,9 @@ func (o *evalObj) Exec(frame *ExecutionFrame) ref.Val { } fieldVals[field] = val } + if unk != nil { + return unk + } return types.LabelErrNode(o.id, o.provider.NewValue(o.typeName, fieldVals)) } @@ -1602,6 +1644,26 @@ func (f *folder) Unwrap() Activation { return f.frame.parent } +// IsLocalVariable reports whether the variable name is locally bound by the folder scope. +func (f *folder) IsLocalVariable(name string) bool { + if name == f.accuVar { + return true + } + if !f.computeResult && (name == f.iterVar || name == f.iterVar2) { + return true + } + parent := f.Parent() + if parent == nil { + return false + } + if varHolder, ok := parent.(localVariableHolder); ok { + if varHolder.IsLocalVariable(name) { + return true + } + } + return false +} + // UnknownAttributePatterns implements the PartialActivation interface returning the unknown patterns // if they were provided to the input activation, or an empty set if the proxied activation is not partial. func (f *folder) UnknownAttributePatterns() []*AttributePattern { diff --git a/vendor/github.com/google/cel-go/interpreter/interpreter.go b/vendor/github.com/google/cel-go/interpreter/interpreter.go index 38174aff69..ef13ab9226 100644 --- a/vendor/github.com/google/cel-go/interpreter/interpreter.go +++ b/vendor/github.com/google/cel-go/interpreter/interpreter.go @@ -113,6 +113,12 @@ type activationWrapper interface { Unwrap() Activation } +// localVariableHolder identifies an Activation scope that holds local variables and supports testing +// whether a variable name is local to this scope. +type localVariableHolder interface { + IsLocalVariable(name string) bool +} + // evalStateFactory holds a reference to a factory function that produces an EvalState instance. type evalStateFactory struct { factory func() EvalState @@ -121,6 +127,9 @@ type evalStateFactory struct { // InitState produces an EvalState instance and bundles it into the ExecutionFrame in a way which is // not visible to expression evaluation. func (et *evalStateFactory) InitState(frame *ExecutionFrame) (any, error) { + if frame.ctx != nil && frame.ctx.state != nil { + return frame.ctx.state, nil + } state := et.factory() if frame.ctx == nil { frame.ctx = evalContextPool.Get().(*evalContext) diff --git a/vendor/github.com/google/cel-go/interpreter/planner.go b/vendor/github.com/google/cel-go/interpreter/planner.go index 95fd34551c..396a9803fc 100644 --- a/vendor/github.com/google/cel-go/interpreter/planner.go +++ b/vendor/github.com/google/cel-go/interpreter/planner.go @@ -281,6 +281,10 @@ func (p *planBuilder) planCall(expr ast.Expr) (InterpretableV2, error) { if fnDef == nil { fnDef, _ = p.disp.FindOverload(fnName) } + // Async overloads are planned into an evalAsyncFunc regardless of arity. + if fnDef != nil && fnDef.Async != nil { + return p.planCallAsync(expr, fnName, oName, fnDef, args) + } switch argCount { case 0: return p.planCallZero(expr, fnName, oName, fnDef) @@ -303,6 +307,24 @@ func (p *planBuilder) planCall(expr ast.Expr) (InterpretableV2, error) { } } +// planCallAsync generates an asynchronous callable Interpretable. +func (p *planBuilder) planCallAsync(expr ast.Expr, + function string, + overload string, + impl *functions.Overload, + args []InterpretableV2) (InterpretableV2, error) { + if impl == nil || impl.Async == nil { + return nil, fmt.Errorf("no such overload: %s()", function) + } + return &evalAsyncFunc{ + id: expr.ID(), + function: function, + overload: overload, + args: args, + impl: impl.Async, + }, nil +} + // planCallZero generates a zero-arity callable Interpretable. func (p *planBuilder) planCallZero(expr ast.Expr, function string, diff --git a/vendor/github.com/google/cel-go/interpreter/runtimecost.go b/vendor/github.com/google/cel-go/interpreter/runtimecost.go index 68e4310106..81e4ef63c4 100644 --- a/vendor/github.com/google/cel-go/interpreter/runtimecost.go +++ b/vendor/github.com/google/cel-go/interpreter/runtimecost.go @@ -70,6 +70,9 @@ type costTrackerFactory struct { // InitState produces a CostTracker and bundles it into an Activation in a way which is not visible // to expression evaluation. func (ct *costTrackerFactory) InitState(frame *ExecutionFrame) (any, error) { + if frame.ctx != nil && frame.ctx.costs != nil { + return frame.ctx.costs, nil + } tracker, err := ct.factory() if err != nil { return nil, err @@ -257,7 +260,7 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re if tracker, found := c.overloadTrackers[call.OverloadID()]; found { callCost := tracker(args, result) if callCost != nil { - cost += *callCost + cost = safeAdd(cost, *callCost) return cost } } @@ -265,7 +268,7 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re if c.Estimator != nil { callCost := c.Estimator.CallCost(call.Function(), call.OverloadID(), args, result) if callCost != nil { - cost += *callCost + cost = safeAdd(cost, *callCost) return cost } } @@ -274,13 +277,13 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re switch call.OverloadID() { // O(n) functions case overloads.StartsWithString, overloads.EndsWithString: - cost += uint64(math.Ceil(float64(actualSize(args[1])) * common.StringTraversalCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(actualSize(args[1]))*common.StringTraversalCostFactor))) case overloads.StringToBytes, overloads.BytesToString, overloads.ExtQuoteString, overloads.ExtFormatString: - cost += uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(actualSize(args[0]))*common.StringTraversalCostFactor))) case overloads.InList: // If a list is composed entirely of constant values this is O(1), but we don't account for that here. // We just assume all list containment checks are O(n). - cost += actualSize(args[1]) + cost = safeAdd(cost, actualSize(args[1])) // O(min(m, n)) functions case overloads.LessString, overloads.GreaterString, overloads.LessEqualsString, overloads.GreaterEqualsString, overloads.LessBytes, overloads.GreaterBytes, overloads.LessEqualsBytes, overloads.GreaterEqualsBytes, @@ -290,15 +293,12 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re // of 1. lhsSize := actualSize(args[0]) rhsSize := actualSize(args[1]) - minSize := lhsSize - if rhsSize < minSize { - minSize = rhsSize - } - cost += uint64(math.Ceil(float64(minSize) * common.StringTraversalCostFactor)) + minSize := min(rhsSize, lhsSize) + cost = safeAdd(cost, uint64(math.Ceil(float64(minSize)*common.StringTraversalCostFactor))) // O(m+n) functions case overloads.AddString, overloads.AddBytes: // In the worst case scenario, we would need to reallocate a new backing store and copy both operands over. - cost += uint64(math.Ceil(float64(actualSize(args[0])+actualSize(args[1])) * common.StringTraversalCostFactor)) + cost = safeAdd(cost, uint64(math.Ceil(float64(actualSize(args[0])+actualSize(args[1]))*common.StringTraversalCostFactor))) // O(nm) functions case overloads.Matches, overloads.MatchesString: // https://swtch.com/~rsc/regexp/regexp1.html applies to RE2 implementation supported by CEL @@ -311,11 +311,11 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re // 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(actualSize(args[1])) * common.RegexStringLengthCostFactor)) - cost += strCost * regexCost + cost = safeAdd(cost, strCost*regexCost) case overloads.ContainsString: strCost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor)) substrCost := uint64(math.Ceil(float64(actualSize(args[1])) * common.StringTraversalCostFactor)) - cost += strCost * substrCost + cost = safeAdd(cost, strCost*substrCost) default: // The following operations are assumed to have O(1) complexity. @@ -325,7 +325,7 @@ func (c *CostTracker) costCall(call InterpretableCall, args []ref.Val, result re // - Computing the size of strings, byte sequences, lists and maps. // - Logical operations and all operators on fixed width scalars (comparisons, equality) // - Any functions that don't have a declared cost either here or in provided ActualCostEstimator. - cost++ + cost = safeAdd(cost, 1) } return cost @@ -396,3 +396,21 @@ argloop: } return result, true } + +func safeAdd(x, y uint64, rest ...uint64) uint64 { + if y > 0 && x > math.MaxUint64-y { + return math.MaxUint64 + } + next := x + y + if len(rest) == 0 { + return next + } + return safeAdd(next, rest[0], rest[1:]...) +} + +func safeMul(x, y uint64) uint64 { + if y != 0 && x > math.MaxUint64/y { + return math.MaxUint64 + } + return x * y +} diff --git a/vendor/github.com/google/cel-go/parser/helper.go b/vendor/github.com/google/cel-go/parser/helper.go index f960be20ed..84bef80d5f 100644 --- a/vendor/github.com/google/cel-go/parser/helper.go +++ b/vendor/github.com/google/cel-go/parser/helper.go @@ -45,6 +45,10 @@ func (p *parserHelper) getSourceInfo() *ast.SourceInfo { return p.sourceInfo } +func (p *parserHelper) expressionCount() int64 { + return p.nextID - 1 +} + func (p *parserHelper) newLiteral(ctx any, value ref.Val) ast.Expr { return p.exprFactory.NewLiteral(p.newID(ctx), value) } diff --git a/vendor/github.com/google/cel-go/parser/options.go b/vendor/github.com/google/cel-go/parser/options.go index 4eb30f83e0..281021f122 100644 --- a/vendor/github.com/google/cel-go/parser/options.go +++ b/vendor/github.com/google/cel-go/parser/options.go @@ -22,6 +22,7 @@ type options struct { errorRecoveryTokenLookaheadLimit int errorRecoveryLimit int expressionSizeCodePointLimit int + maxExpressionNodeCount int macros map[string]Macro populateMacroCalls bool enableOptionalSyntax bool @@ -97,6 +98,18 @@ func ExpressionSizeCodePointLimit(expressionSizeCodePointLimit int) Option { } } +// MaxExpressionNodeCount limits the maximum number of expression nodes that may be emitted by the parser, +// including nodes created by macro expansion. +func MaxExpressionNodeCount(limit int) Option { + return func(opts *options) error { + if limit < -1 { + return fmt.Errorf("max expression node count must be greater than or equal to -1: %d", limit) + } + opts.maxExpressionNodeCount = limit + return nil + } +} + // Macros adds the given macros to the parser. func Macros(macros ...Macro) Option { return func(opts *options) error { diff --git a/vendor/github.com/google/cel-go/parser/parser.go b/vendor/github.com/google/cel-go/parser/parser.go index d1567b5ff0..338233543d 100644 --- a/vendor/github.com/google/cel-go/parser/parser.go +++ b/vendor/github.com/google/cel-go/parser/parser.go @@ -72,6 +72,12 @@ func NewParser(opts ...Option) (*Parser, error) { if p.expressionSizeCodePointLimit == -1 { p.expressionSizeCodePointLimit = int((^uint(0)) >> 1) } + if p.maxExpressionNodeCount == 0 { + p.maxExpressionNodeCount = 100_000 + } + if p.maxExpressionNodeCount == -1 { + p.maxExpressionNodeCount = int((^uint(0)) >> 1) + } // Bool is false by default, so populateMacroCalls will be false by default return p, nil } @@ -102,6 +108,7 @@ func (p *Parser) Parse(source common.Source) (*ast.AST, *common.Errors) { helper: newParserHelper(source, fac), macros: p.macros, maxRecursionDepth: p.maxRecursionDepth, + maxExpressionNodeCount: p.maxExpressionNodeCount, errorReportingLimit: p.errorReportingLimit, errorRecoveryLimit: p.errorRecoveryLimit, errorRecoveryLookaheadTokenLimit: p.errorRecoveryTokenLookaheadLimit, @@ -319,6 +326,7 @@ type parser struct { recursionDepth int errorReports int maxRecursionDepth int + maxExpressionNodeCount int errorReportingLimit int errorRecoveryLimit int errorRecoveryLookaheadTokenLimit int @@ -964,11 +972,21 @@ func (p *parser) expandMacro(exprID int64, function string, target ast.Expr, arg return nil, false } } + if int(p.helper.expressionCount()) > p.maxExpressionNodeCount { + loc := p.helper.getLocation(exprID) + p.helper.deleteID(exprID) + return p.reportError(loc, "expression count exceeds limit of %d while expanding macro '%s'", p.maxExpressionNodeCount, function), true + } eh := exprHelperPool.Get().(*exprHelper) defer exprHelperPool.Put(eh) eh.parserHelper = p.helper eh.id = exprID expr, err := macro.Expander()(eh, target, args) + if int(p.helper.expressionCount()) > p.maxExpressionNodeCount { + loc := p.helper.getLocation(exprID) + p.helper.deleteID(exprID) + return p.reportError(loc, "expression count exceeds limit of %d while expanding macro '%s'", p.maxExpressionNodeCount, function), true + } // An error indicates that the macro was matched, but the arguments were not well-formed. if err != nil { loc := err.Location diff --git a/vendor/github.com/tektoncd/triggers/test/e2e-common.sh b/vendor/github.com/tektoncd/triggers/test/e2e-common.sh index 8187fd4382..1f88824729 100644 --- a/vendor/github.com/tektoncd/triggers/test/e2e-common.sh +++ b/vendor/github.com/tektoncd/triggers/test/e2e-common.sh @@ -54,7 +54,7 @@ function install_triggers_crd() { echo ">> Deploying Tekton Triggers" rel=$(mktemp) release=$(mktemp) - ko resolve -f config/ > "${rel}" || fail_test "Tekton Triggers build failed" + ko resolve ${KO_FLAGS:-} -f config/ > "${rel}" || fail_test "Tekton Triggers build failed" if [ "${SKIP_SECURITY_CTX}" == "true" ]; then yq 'del(.spec.template.spec.containers[]?.securityContext.runAsUser, .spec.template.spec.containers[]?.securityContext.runAsGroup)' "${rel}" > "${release}" @@ -66,7 +66,7 @@ function install_triggers_crd() { # Wait for the Interceptors CRD to be available before adding the core-interceptors kubectl wait --for=condition=Established --timeout=30s crds/clusterinterceptors.triggers.tekton.dev - ko resolve -f config/interceptors > "${rel}" || fail_test "Core interceptors build failed" + ko resolve ${KO_FLAGS:-} -f config/interceptors > "${rel}" || fail_test "Core interceptors build failed" if [ "${SKIP_SECURITY_CTX}" == "true" ]; then kubectl patch configmap config-defaults-triggers -n tekton-pipelines --type='merge' -p='{"data":{"default-run-as-user":"","default-fs-group":"", "default-run-as-group":""}}' diff --git a/vendor/modules.txt b/vendor/modules.txt index 0cc67d2146..ccfdb8b3c7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -229,9 +229,10 @@ github.com/golang/protobuf/ptypes/struct # github.com/google/btree v1.1.3 ## explicit; go 1.18 github.com/google/btree -# github.com/google/cel-go v0.29.2 +# github.com/google/cel-go v0.30.0 ## explicit; go 1.23.0 github.com/google/cel-go/cel +github.com/google/cel-go/cel/async github.com/google/cel-go/checker github.com/google/cel-go/checker/decls github.com/google/cel-go/common @@ -633,8 +634,8 @@ github.com/tektoncd/pipeline/test/parse # github.com/tektoncd/plumbing v0.0.0-20250430145243-3b7cd59879c1 ## explicit; go 1.23 github.com/tektoncd/plumbing/scripts -# github.com/tektoncd/triggers v0.36.0 -## explicit; go 1.25.7 +# github.com/tektoncd/triggers v0.37.0 +## explicit; go 1.26.4 github.com/tektoncd/triggers/pkg/apis/config github.com/tektoncd/triggers/pkg/apis/triggers github.com/tektoncd/triggers/pkg/apis/triggers/contexts