mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-08-19 04:06:35 +00:00
add cost budget, runtime cost estimator and metrics (#964)
Post / images (amd64) (push) Failing after 6m56s
Post / images (arm64) (push) Failing after 6m41s
Post / image manifest (push) Has been skipped
Post / trigger clusteradm e2e (push) Has been skipped
Post / coverage (push) Failing after 27m27s
Scorecard supply-chain security / Scorecard analysis (push) Failing after 4m7s
Close stale issues and PRs / stale (push) Successful in 34s
Post / images (amd64) (push) Failing after 6m56s
Post / images (arm64) (push) Failing after 6m41s
Post / image manifest (push) Has been skipped
Post / trigger clusteradm e2e (push) Has been skipped
Post / coverage (push) Failing after 27m27s
Scorecard supply-chain security / Scorecard analysis (push) Failing after 4m7s
Close stale issues and PRs / stale (push) Successful in 34s
Signed-off-by: Qing Hao <qhao@redhat.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+129
-26
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
|
||||
+20
-1
@@ -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
|
||||
|
||||
+94
@@ -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
|
||||
}
|
||||
+30
-9
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user