From df87f528d7d7479298361edf0149bd889174cf40 Mon Sep 17 00:00:00 2001 From: Qing Hao Date: Wed, 30 Apr 2025 16:15:22 +0800 Subject: [PATCH] add cost budget, runtime cost estimator and metrics (#964) Signed-off-by: Qing Hao --- go.mod | 2 +- go.sum | 4 +- pkg/placement/controllers/metrics/metrics.go | 11 +- .../scheduling/scheduling_controller.go | 2 +- pkg/placement/helpers/cel.go | 155 +++++++++++++++--- pkg/placement/helpers/cel_test.go | 44 ++++- pkg/placement/helpers/clusters.go | 8 +- pkg/placement/helpers/clusters_test.go | 2 +- pkg/placement/plugins/predicate/predicate.go | 2 +- vendor/modules.txt | 2 +- .../sdk-go/pkg/cel/common/values.go | 21 ++- .../sdk-go/pkg/cel/library/cost.go | 94 +++++++++++ .../pkg/cloudevents/clients/utils/utils.go | 39 ++++- 13 files changed, 331 insertions(+), 55 deletions(-) create mode 100644 vendor/open-cluster-management.io/sdk-go/pkg/cel/library/cost.go diff --git a/go.mod b/go.mod index d13e87fb8..f338d2e4c 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 open-cluster-management.io/addon-framework v0.12.1-0.20250407131028-9d436ffc2da7 open-cluster-management.io/api v0.16.2-0.20250425084048-6c5efe2ab15d - open-cluster-management.io/sdk-go v0.16.1-0.20250411154302-3a424961ead4 + open-cluster-management.io/sdk-go v0.16.1-0.20250428032116-875454003818 sigs.k8s.io/cluster-inventory-api v0.0.0-20240730014211-ef0154379848 sigs.k8s.io/controller-runtime v0.20.2 sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 diff --git a/go.sum b/go.sum index 095ed0adc..d3da39261 100644 --- a/go.sum +++ b/go.sum @@ -489,8 +489,8 @@ open-cluster-management.io/addon-framework v0.12.1-0.20250407131028-9d436ffc2da7 open-cluster-management.io/addon-framework v0.12.1-0.20250407131028-9d436ffc2da7/go.mod h1:7AEw1Sq9UEWpQGTU8zV1XPNkFRBYPbyBh8tfhISV++s= open-cluster-management.io/api v0.16.2-0.20250425084048-6c5efe2ab15d h1:uowhuu3pDvGLMU9if/xdRr1IO+DPGlBUqcqd0SpMjqY= open-cluster-management.io/api v0.16.2-0.20250425084048-6c5efe2ab15d/go.mod h1:/OeqXycNBZQoe3WG6ghuWsMgsKGuMZrK8ZpsU6gWL0Y= -open-cluster-management.io/sdk-go v0.16.1-0.20250411154302-3a424961ead4 h1:PT6kDaKjDi0EaQyNzIWzYAYeK4QpBHbm+/7VPrpJEkY= -open-cluster-management.io/sdk-go v0.16.1-0.20250411154302-3a424961ead4/go.mod h1:FtOYjn5dL8e9S1gzNb8cBNsFzHJ1F3cpmCo+qrltido= +open-cluster-management.io/sdk-go v0.16.1-0.20250428032116-875454003818 h1:b7HpdTpKPzLEoJ5UtrXCed1PjxaKOxEboJ+kG6FZudI= +open-cluster-management.io/sdk-go v0.16.1-0.20250428032116-875454003818/go.mod h1:n89YVVoi5zm3KVpOyVMmTdD4rGOVSsykUtu7Ol3do3M= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/cluster-inventory-api v0.0.0-20240730014211-ef0154379848 h1:WYPi2PdQyZwZkHG648v2jQl6deyCgyjJ0fkLYgUJ618= diff --git a/pkg/placement/controllers/metrics/metrics.go b/pkg/placement/controllers/metrics/metrics.go index d580a673c..1e9b6c5de 100644 --- a/pkg/placement/controllers/metrics/metrics.go +++ b/pkg/placement/controllers/metrics/metrics.go @@ -15,6 +15,7 @@ const ( SchedulingDurationKey = "scheduling_duration_seconds" BindDurationKey = "bind_duration_seconds" PluginDurationKey = "plugin_duration_seconds" + CelRuntimeDurationKey = "cel_runtime_duration_seconds" ) // Metric histograms for tracking various durations. @@ -43,8 +44,16 @@ var ( Buckets: k8smetrics.ExponentialBuckets(10e-7, 10, 10), }, []string{"name", "plugin_type", "plugin_name"}) + CelDuration = k8smetrics.NewHistogramVec(&k8smetrics.HistogramOpts{ + Subsystem: SchedulingSubsystem, + Name: CelRuntimeDurationKey, + StabilityLevel: k8smetrics.ALPHA, + Help: "How long in seconds CEL expressions validation runs for a placement.", + Buckets: k8smetrics.ExponentialBuckets(10e-7, 10, 10), + }, []string{"name"}) + metrics = []k8smetrics.Registerable{ - schedulingDuration, bindDuration, PluginDuration, + schedulingDuration, bindDuration, PluginDuration, CelDuration, } ) diff --git a/pkg/placement/controllers/scheduling/scheduling_controller.go b/pkg/placement/controllers/scheduling/scheduling_controller.go index 65782f9d7..77da3b356 100644 --- a/pkg/placement/controllers/scheduling/scheduling_controller.go +++ b/pkg/placement/controllers/scheduling/scheduling_controller.go @@ -785,7 +785,7 @@ func filterClustersBySelector( ) ([]clusterapiv1beta1.ClusterDecision, *framework.Status) { var matched []clusterapiv1beta1.ClusterDecision // set CEL env to nil since placement decision groups do not support CEL expressions. - clusterSelector, err := helpers.NewClusterSelector(selector, nil) + clusterSelector, err := helpers.NewClusterSelector(selector, nil, nil) if err != nil { status := framework.NewStatus("", framework.Misconfigured, err.Error()) return matched, status diff --git a/pkg/placement/helpers/cel.go b/pkg/placement/helpers/cel.go index 85ea33240..79cf3f6d5 100644 --- a/pkg/placement/helpers/cel.go +++ b/pkg/placement/helpers/cel.go @@ -2,8 +2,12 @@ package helpers import ( "context" + "math" + "time" "github.com/google/cel-go/cel" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + celconfig "k8s.io/apiserver/pkg/apis/cel" apiservercel "k8s.io/apiserver/pkg/cel" "k8s.io/klog/v2" @@ -11,8 +15,12 @@ import ( clusterapiv1 "open-cluster-management.io/api/cluster/v1" ocmcelcommon "open-cluster-management.io/sdk-go/pkg/cel/common" ocmcellibrary "open-cluster-management.io/sdk-go/pkg/cel/library" + + "open-cluster-management.io/ocm/pkg/placement/controllers/metrics" ) +var globalCostBudget = int64(celconfig.RuntimeCELCostBudget) + // CompilationResult represents the compilation result of a single CEL expression, // containing either a valid program or an error. type CompilationResult struct { @@ -23,9 +31,10 @@ type CompilationResult struct { // CELSelector handles CEL-based cluster selection by managing a set of CEL expressions // and their compilation results. type CELSelector struct { - env *cel.Env // CEL environment with registered libraries - celExpressions []string // Raw CEL expressions to evaluate - compilationResult []CompilationResult // Cached compilation results + env *cel.Env // CEL environment with registered libraries + metricsRecorder *metrics.ScheduleMetrics // Metrics recorder + celExpressions []string // Raw CEL expressions to evaluate + compilationResult []CompilationResult // Cached compilation results } // NewEnv creates a new CEL environment with managed cluster and JSON libraries. @@ -38,10 +47,18 @@ func NewEnv(scoreLister clusterlisterv1alpha1.AddOnPlacementScoreLister) (*cel.E return cel.NewEnv(envOpts...) } +// newEstimator creates a new cost estimator for CEL expressions. +func newEstimator() *ocmcelcommon.BaseEnvCostEstimator { + return &ocmcelcommon.BaseEnvCostEstimator{ + CostEstimator: &ocmcellibrary.CostEstimator{}, + } +} + // NewCELSelector creates a new CEL selector with the given environment and expressions. -func NewCELSelector(env *cel.Env, expressions []string) *CELSelector { +func NewCELSelector(env *cel.Env, expressions []string, metricsRecorder *metrics.ScheduleMetrics) *CELSelector { return &CELSelector{ env: env, + metricsRecorder: metricsRecorder, celExpressions: expressions, compilationResult: make([]CompilationResult, len(expressions)), } @@ -54,6 +71,7 @@ func (c *CELSelector) Compile() []CompilationResult { return c.compilationResult } + estimator := newEstimator() for i, expr := range c.celExpressions { ast, issues := c.env.Compile(expr) if issues != nil { @@ -64,7 +82,12 @@ func (c *CELSelector) Compile() []CompilationResult { continue } - prg, err := c.env.Program(ast) + prg, err := c.env.Program(ast, + cel.CostLimit(celconfig.PerCallLimit), + cel.CostTracking(estimator), + cel.InterruptCheckFrequency(celconfig.CheckFrequency), + ) + if err != nil { c.compilationResult[i].Error = &apiservercel.Error{ Type: apiservercel.ErrorTypeInvalid, @@ -79,40 +102,120 @@ func (c *CELSelector) Compile() []CompilationResult { } // Validate evaluates all compiled CEL expressions against a managed cluster. -// Returns true only if all expressions evaluate to true. -// Note: Compile() must be called before calling this method to ensure expressions are properly compiled. -// If Compile() has not been called, this method will return false. -func (c *CELSelector) Validate(ctx context.Context, cluster *clusterapiv1.ManagedCluster) bool { +// Returns (true, cost) if all expressions evaluate to true and within cost budget. +// Returns (false, cost) if validation fails. +func (c *CELSelector) Validate(ctx context.Context, cluster *clusterapiv1.ManagedCluster) (bool, int64) { logger := klog.FromContext(ctx) + + // Convert cluster to format required by CEL convertedCluster, err := ocmcelcommon.ConvertObjectToUnstructured(cluster) if err != nil { logger.Error(err, "Failed to convert cluster to unstructured format", "cluster", cluster.Name) - return false + return false, -1 } + startTime := time.Now() + ok, remainingBudget := c.evaluateAllExpressions(ctx, convertedCluster, globalCostBudget) + if c.metricsRecorder != nil { + metrics.CelDuration.WithLabelValues(metrics.SchedulingName).Observe(c.metricsRecorder.SinceInSeconds(startTime)) + } + cost := globalCostBudget - remainingBudget + return ok, cost +} + +// evaluateAllExpressions evaluates each CEL expression in sequence. +// Returns (true, remainingBudget) if all expressions succeed, otherwise (false, budget at failure). +func (c *CELSelector) evaluateAllExpressions(ctx context.Context, cluster *unstructured.Unstructured, budget int64) (bool, int64) { + logger := klog.FromContext(ctx) + remainingBudget := budget + for i, compiled := range c.compilationResult { - if !isValidProgram(compiled) { + // Validate program compilation + if !c.isProgramValid(compiled) { logger.Info("Validation failed: invalid compiled program", "rule", c.celExpressions[i]) - return false + return false, remainingBudget } - result, _, err := compiled.Program.Eval(map[string]interface{}{ - "managedCluster": convertedCluster.Object, - }) - - if err != nil { - logger.Error(err, "Evaluation failed", "rule", c.celExpressions[i]) - return false - } - - if value, ok := result.Value().(bool); !ok || !value { - return false + // Evaluate single expression + ok, newBudget := c.evaluateSingleExpression(ctx, compiled.Program, cluster, remainingBudget, i) + if !ok { + return false, newBudget } + remainingBudget = newBudget } - return true + + return true, remainingBudget } -// isValidProgram checks if a compilation result contains a valid program. -func isValidProgram(compiled CompilationResult) bool { +// isProgramValid checks if a compilation result contains a valid program +func (c *CELSelector) isProgramValid(compiled CompilationResult) bool { return compiled.Program != nil && compiled.Error == nil } + +// evaluateSingleExpression evaluates one CEL expression and handles its cost accounting. +// Returns (true, newBudget) if evaluation succeeds, otherwise (false, -1 or remaining budget). +func (c *CELSelector) evaluateSingleExpression( + ctx context.Context, + program cel.Program, + cluster *unstructured.Unstructured, + budget int64, + index int, +) (bool, int64) { + logger := klog.FromContext(ctx) + + // Evaluate the expression + evalResult, evalDetails, err := program.ContextEval(ctx, map[string]interface{}{ + "managedCluster": cluster.Object, + }) + + // Cost calculation + ok, rtCost := c.costCalculation(ctx, evalDetails, budget, index) + if !ok { + return false, -1 + } + + remainingBudget := budget - rtCost + // Handle evaluation error + if err != nil { + logger.Info("Expression evaluation failed", "rule", c.celExpressions[index], "cluster", cluster.GetName(), "err", err) + return false, remainingBudget + } + + // Check expression result + if value, ok := evalResult.Value().(bool); !ok || !value { + return false, remainingBudget + } + + return true, remainingBudget +} + +// costCalculation processes the cost details of an evaluation +func (c *CELSelector) costCalculation(ctx context.Context, evalDetails *cel.EvalDetails, budget int64, index int) (bool, int64) { + logger := klog.FromContext(ctx) + + // Check if cost details are available + if evalDetails == nil { + logger.Info("Runtime cost calculation failed: no evaluation details", + "rule", c.celExpressions[index]) + return false, -1 + } + + rtCost := evalDetails.ActualCost() + if rtCost == nil { + logger.Info("Runtime cost calculation failed: no cost information", + "rule", c.celExpressions[index]) + return false, -1 + } + + // Validate cost against budget + if *rtCost > math.MaxInt64 || int64(*rtCost) > budget { + logger.Info("Cost budget exceeded", + "rule", c.celExpressions[index], + "cost", *rtCost, + "budget", budget) + return false, -1 + } + + // Safe to convert since we checked for overflow + return true, int64(*rtCost) //nolint:gosec +} diff --git a/pkg/placement/helpers/cel_test.go b/pkg/placement/helpers/cel_test.go index e07c1ebc3..eba00cda1 100644 --- a/pkg/placement/helpers/cel_test.go +++ b/pkg/placement/helpers/cel_test.go @@ -20,6 +20,7 @@ func TestCELSelector(t *testing.T) { expressions []string cluster *clusterapiv1.ManagedCluster expectedMatch bool + expectedCost int64 expectCompileError bool }{ { @@ -33,6 +34,7 @@ func TestCELSelector(t *testing.T) { }, }, expectedMatch: true, + expectedCost: 5, }, { name: "valid expression no match", @@ -45,6 +47,7 @@ func TestCELSelector(t *testing.T) { }, }, expectedMatch: false, + expectedCost: 5, }, { name: "invalid expression", @@ -54,38 +57,59 @@ func TestCELSelector(t *testing.T) { cluster: &clusterapiv1.ManagedCluster{}, expectCompileError: true, expectedMatch: false, + expectedCost: 5, }, { name: "multiple expressions all match", expressions: []string{ `managedCluster.metadata.labels["env"] == "prod"`, - `managedCluster.metadata.labels["region"] == "us-east-1"`, + `semver(managedCluster.metadata.labels["version"]).isLessThan(semver("1.31.0"))`, }, cluster: &clusterapiv1.ManagedCluster{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ - "env": "prod", - "region": "us-east-1", + "env": "prod", + "version": "1.30.0", }, }, }, expectedMatch: true, + expectedCost: 12, }, { name: "multiple expressions one fails", expressions: []string{ `managedCluster.metadata.labels["env"] == "prod"`, - `managedCluster.metadata.labels["region"] == "us-east-1"`, + `semver(managedCluster.metadata.labels["version"]).isGreaterThan(semver("1.31.0"))`, }, cluster: &clusterapiv1.ManagedCluster{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ - "env": "prod", - "region": "us-west-1", + "env": "prod", + "version": "1.30.0", }, }, }, expectedMatch: false, + expectedCost: 12, + }, + { + name: "multiple expressions running out of cost budget", + expressions: []string{ + `managedCluster.metadata.labels["env"] == "prod"`, + `semver(managedCluster.metadata.labels["version"]).isLessThan(semver("1.31.0"))`, + `semver(managedCluster.metadata.labels["version"]).isLessThan(semver("1.31.0"))`, + }, + cluster: &clusterapiv1.ManagedCluster{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "env": "prod", + "version": "1.30.0", + }, + }, + }, + expectedMatch: false, + expectedCost: 16, }, { name: "nil cluster", @@ -95,6 +119,7 @@ func TestCELSelector(t *testing.T) { cluster: nil, expectedMatch: false, expectCompileError: false, + expectedCost: 3, }, { name: "empty expressions", @@ -102,12 +127,14 @@ func TestCELSelector(t *testing.T) { cluster: &clusterapiv1.ManagedCluster{}, expectedMatch: true, expectCompileError: false, + expectedCost: 0, }, } + globalCostBudget = 15 for _, test := range tests { t.Run(test.name, func(t *testing.T) { - selector := NewCELSelector(env, test.expressions) + selector := NewCELSelector(env, test.expressions, nil) results := selector.Compile() if test.expectCompileError { @@ -121,8 +148,9 @@ func TestCELSelector(t *testing.T) { assert.Nil(t, result.Error) } - match := selector.Validate(context.TODO(), test.cluster) + match, cost := selector.Validate(context.TODO(), test.cluster) assert.Equal(t, test.expectedMatch, match) + assert.Equal(t, test.expectedCost, cost) }) } } diff --git a/pkg/placement/helpers/clusters.go b/pkg/placement/helpers/clusters.go index 5f4247734..7694e83d8 100644 --- a/pkg/placement/helpers/clusters.go +++ b/pkg/placement/helpers/clusters.go @@ -9,6 +9,8 @@ import ( clusterapiv1 "open-cluster-management.io/api/cluster/v1" clusterapiv1beta1 "open-cluster-management.io/api/cluster/v1beta1" + + "open-cluster-management.io/ocm/pkg/placement/controllers/metrics" ) type ClusterSelector struct { @@ -17,7 +19,7 @@ type ClusterSelector struct { celSelector *CELSelector } -func NewClusterSelector(selector clusterapiv1beta1.ClusterSelector, env *cel.Env) (*ClusterSelector, error) { +func NewClusterSelector(selector clusterapiv1beta1.ClusterSelector, env *cel.Env, metricsRecorder *metrics.ScheduleMetrics) (*ClusterSelector, error) { // build label selector labelSelector, err := convertLabelSelector(&selector.LabelSelector) if err != nil { @@ -29,7 +31,7 @@ func NewClusterSelector(selector clusterapiv1beta1.ClusterSelector, env *cel.Env return nil, err } // build cel selector - celSelector := NewCELSelector(env, selector.CelSelector.CelExpressions) + celSelector := NewCELSelector(env, selector.CelSelector.CelExpressions, metricsRecorder) return &ClusterSelector{ labelSelector: labelSelector, claimSelector: claimSelector, @@ -63,7 +65,7 @@ func (c *ClusterSelector) Matches(ctx context.Context, cluster *clusterapiv1.Man // match with cel selector if exists if c.celSelector != nil { - if ok := c.celSelector.Validate(ctx, cluster); !ok { + if ok, _ := c.celSelector.Validate(ctx, cluster); !ok { return false } } diff --git a/pkg/placement/helpers/clusters_test.go b/pkg/placement/helpers/clusters_test.go index 37a844c07..7843a7b37 100644 --- a/pkg/placement/helpers/clusters_test.go +++ b/pkg/placement/helpers/clusters_test.go @@ -196,7 +196,7 @@ func TestMatches(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - clusterSelector, err := NewClusterSelector(c.clusterselector, env) + clusterSelector, err := NewClusterSelector(c.clusterselector, env, nil) if err != nil { t.Errorf("unexpected err: %v", err) } diff --git a/pkg/placement/plugins/predicate/predicate.go b/pkg/placement/plugins/predicate/predicate.go index 613c81b01..bf5a1e813 100644 --- a/pkg/placement/plugins/predicate/predicate.go +++ b/pkg/placement/plugins/predicate/predicate.go @@ -63,7 +63,7 @@ func (p *Predicate) Filter( // prebuild label/claim/cel selectors for each predicate clusterSelectors := []*helpers.ClusterSelector{} for _, predicate := range placement.Spec.Predicates { - clusterSelector, err := helpers.NewClusterSelector(predicate.RequiredClusterSelector, env) + clusterSelector, err := helpers.NewClusterSelector(predicate.RequiredClusterSelector, env, p.handle.MetricsRecorder()) if err != nil { return plugins.PluginFilterResult{}, framework.NewStatus( p.Name(), diff --git a/vendor/modules.txt b/vendor/modules.txt index df9ad41e4..4f0dad2a4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1782,7 +1782,7 @@ open-cluster-management.io/api/operator/v1 open-cluster-management.io/api/utils/work/v1/workapplier open-cluster-management.io/api/work/v1 open-cluster-management.io/api/work/v1alpha1 -# open-cluster-management.io/sdk-go v0.16.1-0.20250411154302-3a424961ead4 +# open-cluster-management.io/sdk-go v0.16.1-0.20250428032116-875454003818 ## explicit; go 1.23.6 open-cluster-management.io/sdk-go/pkg/apis/cluster/v1alpha1 open-cluster-management.io/sdk-go/pkg/apis/cluster/v1beta1 diff --git a/vendor/open-cluster-management.io/sdk-go/pkg/cel/common/values.go b/vendor/open-cluster-management.io/sdk-go/pkg/cel/common/values.go index 8e8d37d55..8481602b9 100644 --- a/vendor/open-cluster-management.io/sdk-go/pkg/cel/common/values.go +++ b/vendor/open-cluster-management.io/sdk-go/pkg/cel/common/values.go @@ -4,12 +4,15 @@ import ( "reflect" "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/ext" + "github.com/google/cel-go/interpreter" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/cel/library" ) +// BaseEnvOpts defines the base CEL environment options var BaseEnvOpts = []cel.EnvOption{ cel.OptionalTypes(), ext.Strings(), @@ -20,9 +23,25 @@ var BaseEnvOpts = []cel.EnvOption{ library.IP(), library.CIDR(), library.Format(), + library.SemverLib(), } -// ConvertObjectToUnstructured converts any object to an unstructured.Unstructured object. +// BaseEnvCostEstimator implements CEL's interpretable.ActualCostEstimator +type BaseEnvCostEstimator struct { + // Wraps a CEL cost estimator with additional functionality + CostEstimator interpreter.ActualCostEstimator +} + +// CallCost implements runtime cost estimation for CEL function calls +func (b *BaseEnvCostEstimator) CallCost(function, overloadId string, args []ref.Val, result ref.Val) *uint64 { + if b.CostEstimator != nil { + return b.CostEstimator.CallCost(function, overloadId, args, result) + } + k8sEstimator := &library.CostEstimator{} + return k8sEstimator.CallCost(function, overloadId, args, result) +} + +// ConvertObjectToUnstructured converts any object to an unstructured.Unstructured object func ConvertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) { if obj == nil || reflect.ValueOf(obj).IsNil() { return &unstructured.Unstructured{Object: nil}, nil diff --git a/vendor/open-cluster-management.io/sdk-go/pkg/cel/library/cost.go b/vendor/open-cluster-management.io/sdk-go/pkg/cel/library/cost.go new file mode 100644 index 000000000..d7b18e21f --- /dev/null +++ b/vendor/open-cluster-management.io/sdk-go/pkg/cel/library/cost.go @@ -0,0 +1,94 @@ +package library + +import ( + "math" + + "github.com/google/cel-go/common" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" +) + +// CostEstimator implements CEL's interpretable.ActualCostEstimator for runtime cost estimation +type CostEstimator struct{} + +func actualSize(value ref.Val) uint64 { + if sz, ok := value.(traits.Sizer); ok { + return uint64(sz.Size().(types.Int)) + } + return 1 +} + +// CallCost calculates the runtime cost for CEL function calls +func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, result ref.Val) *uint64 { + switch function { + case "scores": + // each scores returns a list + var totalCost uint64 = common.ListCreateBaseCost + if result != nil { + if lister, ok := result.(traits.Lister); ok { + // each item is a map + size := uint64(lister.Size().(types.Int)) + totalCost += size * (common.SelectAndIdentCost + common.MapCreateBaseCost) + } + } + return &totalCost + case "parseJSON": + var totalCost uint64 = common.MapCreateBaseCost + if len(args) >= 1 { + // Calculate the traversal cost of input string + inputSize := actualSize(args[0]) + traversalCost := uint64(math.Ceil(float64(inputSize) * common.StringTraversalCostFactor)) + // Recursively calculate the cost of result structure + totalCost = traversalCost + calculateStructCost(result) + } + return &totalCost + default: + return nil + } +} + +// calculateStructCost recursively calculates the cost of data structures +func calculateStructCost(val ref.Val) uint64 { + if val == nil { + return 0 + } + + switch v := val.(type) { + case traits.Mapper: + return calculateMapCost(v) + case traits.Lister: + return calculateListCost(v) + default: + return common.ConstCost + } +} + +// calculateMapCost computes cost for map structures +func calculateMapCost(v traits.Mapper) uint64 { + cost := uint64(common.MapCreateBaseCost) + + it := v.Iterator() + for it.HasNext() == types.True { + key := it.Next() + if value := v.Get(key); value != nil { + cost += calculateStructCost(value) + } + } + + return cost +} + +// calculateListCost computes cost for list structures +func calculateListCost(v traits.Lister) uint64 { + cost := uint64(common.ListCreateBaseCost) + + size := v.Size().(types.Int) + for i := types.Int(0); i < size; i++ { + if item := v.Get(types.Int(i)); item != nil { + cost += calculateStructCost(item) + } + } + + return cost +} diff --git a/vendor/open-cluster-management.io/sdk-go/pkg/cloudevents/clients/utils/utils.go b/vendor/open-cluster-management.io/sdk-go/pkg/cloudevents/clients/utils/utils.go index 90f99829e..71e9c574b 100644 --- a/vendor/open-cluster-management.io/sdk-go/pkg/cloudevents/clients/utils/utils.go +++ b/vendor/open-cluster-management.io/sdk-go/pkg/cloudevents/clients/utils/utils.go @@ -16,6 +16,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/tools/cache" "k8s.io/klog/v2" @@ -24,9 +25,25 @@ import ( "open-cluster-management.io/sdk-go/pkg/cloudevents/generic" ) -// Patch applies the patch to a resource with the patch type. -func Patch[T generic.ResourceObject](patchType types.PatchType, work T, patchData []byte) (resource T, err error) { - workData, err := json.Marshal(work) +// Patch applies the given patch to a `generic.ResourceObject` using the specified patch type. +// +// Parameters: +// - patchType: The type of patch to apply (JSONPatchType, MergePatchType and StrategicMergePatchType are supported). +// - original: The resource object to be patched. +// - patchData: The raw patch data. +// +// Returns: +// - The patched resource object. +// - An error if the patching fails at any step. +// +// Notes on StrategicMergePatch: +// - Strategic Merge Patch (SMP) is a Kubernetes-specific patch type. +// - It relies on **struct tags** (e.g., `patchStrategy` and `patchMergeKey`) defined in the Go types +// of Kubernetes API objects to determine how to merge lists and maps (e.g., merge by key instead of replacing). +// - SMP **only works** on known Kubernetes built-in API types (e.g., corev1.Pod) that have these metadata tags. +// - It will **fail or behave incorrectly** if used on CRDs or custom types that don’t have the necessary tags. +func Patch[T generic.ResourceObject](patchType types.PatchType, original T, patchData []byte) (resource T, err error) { + originalData, err := json.Marshal(original) if err != nil { return resource, err } @@ -39,13 +56,17 @@ func Patch[T generic.ResourceObject](patchType types.PatchType, work T, patchDat if err != nil { return resource, err } - patchedData, err = patchObj.Apply(workData) + patchedData, err = patchObj.Apply(originalData) if err != nil { return resource, err } - case types.MergePatchType: - patchedData, err = jsonpatch.MergePatch(workData, patchData) + patchedData, err = jsonpatch.MergePatch(originalData, patchData) + if err != nil { + return resource, err + } + case types.StrategicMergePatchType: + patchedData, err = strategicpatch.StrategicMergePatch(originalData, patchData, original) if err != nil { return resource, err } @@ -53,12 +74,12 @@ func Patch[T generic.ResourceObject](patchType types.PatchType, work T, patchDat return resource, fmt.Errorf("unsupported patch type: %s", patchType) } - patchedWork := new(T) - if err := json.Unmarshal(patchedData, patchedWork); err != nil { + patchedResource := new(T) + if err := json.Unmarshal(patchedData, patchedResource); err != nil { return resource, err } - return *patchedWork, nil + return *patchedResource, nil } // ListResourcesWithOptions retrieves the resources from store which matches the options.