diff --git a/pkg/placement/controllers/scheduling/scheduling_controller.go b/pkg/placement/controllers/scheduling/scheduling_controller.go index 01608dc8f..4a8a7b838 100644 --- a/pkg/placement/controllers/scheduling/scheduling_controller.go +++ b/pkg/placement/controllers/scheduling/scheduling_controller.go @@ -790,11 +790,24 @@ func filterClustersBySelector( return matched, status } + // cel evaluator + evaluator, err := helpers.NewEvaluator() + if err != nil { + panic(err) + } + // filter clusters by label selector for _, cluster := range clusters { if ok := clusterSelector.Matches(cluster.Labels, helpers.GetClusterClaims(cluster)); !ok { continue } + if ok, err := evaluator.Evaluate(cluster, selector.CelSelector.CelExpressions); !ok { + if err != nil { + status := framework.NewStatus("", framework.Misconfigured, err.Error()) + return []clusterapiv1beta1.ClusterDecision{}, status + } + continue + } if !clusterNames.Has(cluster.Name) { continue } diff --git a/pkg/placement/helpers/cel.go b/pkg/placement/helpers/cel.go new file mode 100644 index 000000000..fa42d553c --- /dev/null +++ b/pkg/placement/helpers/cel.go @@ -0,0 +1,356 @@ +package helpers + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/version" + clusterapiv1 "open-cluster-management.io/api/cluster/v1" + clusterapiv1alpha1 "open-cluster-management.io/api/cluster/v1alpha1" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" +) + +// ManagedClusterLib defines the CEL library for ManagedCluster evaluation. +// It provides functions and variables for evaluating ManagedCluster properties +// and their associated resources. +// +// Variables: +// +// managedCluster +// +// Provides access to ManagedCluster properties. +// +// Functions: +// +// scores +// +// Returns a list of AddOnPlacementScoreItem for a given cluster and AddOnPlacementScore resource name. +// +// scores(, ) +// +// The returned list contains maps with the following structure: +// - name: string - The name of the score +// - value: int - The numeric score value +// - quantity: number|string - The quantity value, represented as: +// * number: for pure decimal values (e.g., 3) +// * string: for values with units or decimal places (e.g., "300Mi", "1.5Gi") +// +// Examples: +// +// managedCluster.scores("cpu-memory") // returns [{name: "cpu", value: 3, quantity: 3"}, {name: "memory", value: 4, quantity: "300Mi"}] +// +// Version Comparisons: +// +// versionIsGreaterThan +// +// Returns true if the first version string is greater than the second version string. +// The version must follow Semantic Versioning specification (http://semver.org/). +// It can be with or without 'v' prefix (eg, "1.14.3" or "v1.14.3"). +// +// versionIsGreaterThan(, ) +// +// Examples: +// +// versionIsGreaterThan("1.25.0", "1.24.0") // returns true +// versionIsGreaterThan("1.24.0", "1.25.0") // returns false +// +// versionIsLessThan +// +// Returns true if the first version string is less than the second version string. +// The version must follow Semantic Versioning specification (http://semver.org/). +// It can be with or without 'v' prefix (eg, "1.14.3" or "v1.14.3"). +// +// versionIsLessThan(, ) +// +// Examples: +// +// versionIsLessThan("1.24.0", "1.25.0") // returns true +// versionIsLessThan("1.25.0", "1.24.0") // returns false +// +// Quantity Comparisons: +// +// quantityIsGreaterThan +// +// Returns true if the first quantity string is greater than the second quantity string. +// +// quantityIsGreaterThan(, ) +// +// Examples: +// +// quantityIsGreaterThan("2Gi", "1Gi") // returns true +// quantityIsGreaterThan("1Gi", "2Gi") // returns false +// quantityIsGreaterThan("1000Mi", "1Gi") // returns false +// +// quantityIsLessThan +// +// Returns true if the first quantity string is less than the second quantity string. +// +// quantityIsLessThan(, ) +// +// Examples: +// +// quantityIsLessThan("1Gi", "2Gi") // returns true +// quantityIsLessThan("2Gi", "1Gi") // returns false +// quantityIsLessThan("1000Mi", "1Gi") // returns true + +type ManagedClusterLib struct{} + +// CompileOptions implements cel.Library interface to provide compile-time options. +func (ManagedClusterLib) CompileOptions() []cel.EnvOption { + return []cel.EnvOption{ + // The input types may either be instances of `proto.Message` or `ref.Type`. + // Here we use func ConvertManagedCluster() to convert ManagedCluster to a Map. + cel.Variable("managedCluster", cel.MapType(cel.StringType, cel.DynType)), + + cel.Function("scores", + cel.MemberOverload( + "cluster_scores", + []*cel.Type{cel.DynType, cel.StringType}, + cel.ListType(cel.DynType), + cel.FunctionBinding(clusterScores)), + ), + + cel.Function("versionIsGreaterThan", + cel.MemberOverload( + "version_is_greater_than", + []*cel.Type{cel.StringType, cel.StringType}, + cel.BoolType, + cel.FunctionBinding(versionIsGreaterThan)), + ), + + cel.Function("versionIsLessThan", + cel.MemberOverload( + "version_is_less_than", + []*cel.Type{cel.StringType, cel.StringType}, + cel.BoolType, + cel.FunctionBinding(versionIsLessThan)), + ), + + cel.Function("quantityIsGreaterThan", + cel.MemberOverload( + "quantity_is_greater_than", + []*cel.Type{cel.StringType, cel.StringType}, + cel.BoolType, + cel.FunctionBinding(quantityIsGreaterThan)), + ), + + cel.Function("quantityIsLessThan", + cel.MemberOverload( + "quantity_is_less_than", + []*cel.Type{cel.StringType, cel.StringType}, + cel.BoolType, + cel.FunctionBinding(quantityIsLessThan)), + ), + } +} + +// ProgramOptions implements cel.Library interface to provide runtime options. +// You can use this to add custom functions or evaluators. +func (ManagedClusterLib) ProgramOptions() []cel.ProgramOption { + return nil +} + +// Evaluator is a reusable struct for CEL evaluation on ManagedCluster. +type Evaluator struct { + env *cel.Env +} + +// NewEvaluator creates a new CEL Evaluator for ManagedCluster objects. +func NewEvaluator() (*Evaluator, error) { + env, err := cel.NewEnv( + cel.Lib(ManagedClusterLib{}), // Add the ManagedClusterLib to the CEL environment + ) + if err != nil { + return nil, fmt.Errorf("failed to create CEL environment: %w", err) + } + return &Evaluator{env: env}, nil +} + +// Evaluate evaluates a CEL expression against a ManagedCluster. +func (e *Evaluator) Evaluate(cluster *clusterapiv1.ManagedCluster, expressions []string) (bool, error) { + convertedCluster := convertManagedCluster(cluster) + + for _, expr := range expressions { + ast, iss := e.env.Compile(expr) + if iss.Err() != nil { + return false, fmt.Errorf("failed to compile CEL expression '%s': %w", expr, iss.Err()) + } + + prg, _ := e.env.Program(ast) + result, _, err := prg.Eval(map[string]interface{}{ + "managedCluster": convertedCluster, + }) + if err != nil { + return false, fmt.Errorf("CEL evaluation error: %w", err) + } + + if value, ok := result.Value().(bool); !ok || !value { + return false, nil + } + } + + return true, nil +} + +// convertManagedCluster converts a ManagedCluster object to a CEL-compatible format. +func convertManagedCluster(cluster *clusterapiv1.ManagedCluster) map[string]interface{} { + convertedClusterClaims := []map[string]interface{}{} + for _, claim := range cluster.Status.ClusterClaims { + convertedClusterClaims = append(convertedClusterClaims, map[string]interface{}{ + "name": claim.Name, + "value": claim.Value, + }) + } + + convertedVersion := map[string]interface{}{ + "kubernetes": cluster.Status.Version.Kubernetes, + } + + // TODO: more fields to add + return map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": cluster.Name, + "labels": cluster.Labels, + "annotations": cluster.Annotations, + }, + "status": map[string]interface{}{ + "clusterClaims": convertedClusterClaims, + "version": convertedVersion, + }, + } +} + +// ScoreToCel converts an AddOnPlacementScoreItem to a CEL-compatible map structure. +// For quantities that are pure integers (e.g., 3), it uses numeric values. +// For quantities with units or decimals (e.g., "300Mi", "1.5Gi"), it uses string representation. +func ScoreToCel(name string, value int32, q resource.Quantity) ref.Val { + var quantityValue interface{} + if q.Format == resource.DecimalSI && q.MilliValue()%1000 == 0 { + quantityValue = q.Value() + } else { + quantityValue = q.String() + } + + return types.NewStringInterfaceMap(types.DefaultTypeAdapter, map[string]interface{}{ + "name": name, + "value": value, + "quantity": quantityValue, + }) +} + +// clusterScores implements the CEL function scores(cluster, scoreName) that returns +// a list of AddOnPlacementScores for the given cluster and score resource name. +// Each score in the returned list contains: +// - name: the score identifier +// - value: the numeric score value +// - quantity: the resource quantity (as number or string) +func clusterScores(args ...ref.Val) ref.Val { + cluster := args[0].(traits.Mapper) + metadata, _ := cluster.Find(types.String("metadata")) + clusterName, _ := metadata.(traits.Mapper).Find(types.String("name")) + scoreName := args[1] + fmt.Sprintf("%s, %s", clusterName, scoreName) + + // TODO: Replace with actual score lookup using clusterName and scoreName + scores := []clusterapiv1alpha1.AddOnPlacementScoreItem{ + { + Name: "cpu", + Value: 3, + Quantity: resource.MustParse("3"), + }, + { + Name: "memory", + Value: 4, + Quantity: resource.MustParse("300Mi"), + }, + } + + celScores := make([]ref.Val, len(scores)) + for i, score := range scores { + celScores[i] = ScoreToCel(score.Name, score.Value, score.Quantity) + } + + return types.NewDynamicList(types.DefaultTypeAdapter, celScores) +} + +// compareVersions is a helper function that compares two version strings +func compareVersions(v1Str, v2Str ref.Val, op string) ref.Val { + if v1Str == nil || v2Str == nil { + return types.NewErr("%s: requires exactly two arguments", op) + } + + // Convert arguments to strings + v1, ok1 := v1Str.Value().(string) + v2, ok2 := v2Str.Value().(string) + if !ok1 || !ok2 { + return types.NewErr("%s: both arguments must be strings", op) + } + + // Parse first version + v1Parsed, err := version.ParseSemantic(v1) + if err != nil { + return types.NewErr("%s: invalid first version: %v", op, err) + } + + // Compare versions + cmp, err := v1Parsed.Compare(v2) + if err != nil { + return types.NewErr("%s: comparison failed: %v", op, err) + } + + if op == "versionIsGreaterThan" { + return types.Bool(cmp > 0) + } + return types.Bool(cmp < 0) +} + +func versionIsGreaterThan(args ...ref.Val) ref.Val { + return compareVersions(args[0], args[1], "versionIsGreaterThan") +} + +func versionIsLessThan(args ...ref.Val) ref.Val { + return compareVersions(args[0], args[1], "versionIsLessThan") +} + +// compareQuantities is a helper function that compares two quantity strings +func compareQuantities(q1Str, q2Str ref.Val, op string) ref.Val { + if q1Str == nil || q2Str == nil { + return types.NewErr("%s: requires exactly two arguments", op) + } + + // Convert arguments to strings + q1, ok1 := q1Str.Value().(string) + q2, ok2 := q2Str.Value().(string) + if !ok1 || !ok2 { + return types.NewErr("%s: both arguments must be strings", op) + } + + // Parse quantities + q1Val, err := resource.ParseQuantity(q1) + if err != nil { + return types.NewErr("%s: invalid first quantity: %v", op, err) + } + + q2Val, err := resource.ParseQuantity(q2) + if err != nil { + return types.NewErr("%s: invalid second quantity: %v", op, err) + } + + cmp := q1Val.Cmp(q2Val) + if op == "quantityIsGreaterThan" { + return types.Bool(cmp > 0) + } + return types.Bool(cmp < 0) +} + +func quantityIsGreaterThan(args ...ref.Val) ref.Val { + return compareQuantities(args[0], args[1], "quantityIsGreaterThan") +} + +func quantityIsLessThan(args ...ref.Val) ref.Val { + return compareQuantities(args[0], args[1], "quantityIsLessThan") +} diff --git a/pkg/placement/helpers/cel_test.go b/pkg/placement/helpers/cel_test.go new file mode 100644 index 000000000..4d2ddb17b --- /dev/null +++ b/pkg/placement/helpers/cel_test.go @@ -0,0 +1,173 @@ +package helpers + +import ( + "errors" + "strings" + "testing" + + clusterapiv1 "open-cluster-management.io/api/cluster/v1" + clusterapiv1beta1 "open-cluster-management.io/api/cluster/v1beta1" + testinghelpers "open-cluster-management.io/ocm/pkg/placement/helpers/testing" +) + +func TestCelEvaluate(t *testing.T) { + cases := []struct { + name string + clusterselector clusterapiv1beta1.ClusterSelector + cluster *clusterapiv1.ManagedCluster + expectedMatch bool + expectedErr error + }{ + { + name: "match with label", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{`managedCluster.metadata.labels["version"].matches('^1\\.(14|15)\\.\\d+$')`}, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").WithLabel("version", "1.14.3").Build(), + expectedMatch: true, + }, + { + name: "not match with label", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{`managedCluster.metadata.labels["version"].matches('^1\\.(14|15)\\.\\d+$')`}, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").WithLabel("version", "1.16.3").Build(), + expectedMatch: false, + }, + { + name: "invalid labels expression", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{`managedCluster.metadata.labels["version"].matchess('^1\\.(14|15)\\.\\d+$')`}, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").WithLabel("version", "1.14.3").Build(), + expectedMatch: false, + expectedErr: errors.New("undeclared reference to 'matchess'"), + }, + { + name: "match with claim", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{`managedCluster.status.clusterClaims.exists(c, c.name == "version" && c.value.matches('^1\\.(14|15)\\.\\d+$'))`}, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").WithClaim("version", "1.14.3").Build(), + expectedMatch: true, + }, + { + name: "not match with claim", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{`managedCluster.status.clusterClaims.exists(c, c.name == "version" && c.value.matches('^1\\.(14|15)\\.\\d+$'))`}, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").WithClaim("version", "1.16.3").Build(), + expectedMatch: false, + }, + { + name: "invalid claims expression", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{`managedCluster.status.clusterClaims.exists(c, c.name == "version" && c.value.matchessssss('^1\\.(14|15)\\.\\d+$'))`}, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").WithClaim("version", "1.14.3").Build(), + expectedMatch: false, + expectedErr: errors.New("undeclared reference to 'matchessssss'"), + }, + { + name: "match with both label and claim", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{ + `managedCluster.metadata.labels["cloud"] == "Amazon"`, + `managedCluster.status.clusterClaims.exists(c, c.name == "region" && c.value == "us-east-1")`, + }, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").WithLabel("cloud", "Amazon").WithClaim("region", "us-east-1").Build(), + expectedMatch: true, + }, + { + name: "not match with both label and claim", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{ + `managedCluster.metadata.labels["cloud"] == "Amazon"`, + `managedCluster.status.clusterClaims.exists(c, c.name == "region" && c.value == "us-east-1"`, + }, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").WithLabel("region", "us-east-1").WithClaim("cloud", "Amazon").Build(), + expectedMatch: false, + expectedErr: errors.New("no such key: cloud"), + }, + { + name: "match with version", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{`managedCluster.status.version.kubernetes.versionIsGreaterThan('v1.29.0')`}, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").Withk8sVersion("v1.30.6").Build(), + expectedMatch: true, + }, + { + name: "not match with version", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{`managedCluster.status.version.kubernetes.versionIsLessThan('v1.29.0')`}, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").Withk8sVersion("v1.30.6").Build(), + expectedMatch: false, + }, + { + name: "match with score quantities", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{ + `managedCluster.scores("test-score").filter(s, s.name == 'cpu').all(e, e.quantity == 3)`, + `managedCluster.scores("test-score").filter(s, s.name == 'memory').all(e, e.quantity.quantityIsGreaterThan('200Mi'))`, + }, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").Build(), + expectedMatch: true, + }, + { + name: "not match with score quantities", + clusterselector: clusterapiv1beta1.ClusterSelector{ + CelSelector: clusterapiv1beta1.ClusterCelSelector{ + CelExpressions: []string{ + `managedCluster.scores("test-score").filter(s, s.name == 'cpu').all(e, e.quantity == 4)`, + `managedCluster.scores("test-score").filter(s, s.name == 'memory').all(e, e.quantity.quantityIsLessThan('200Mi'))`, + }, + }, + }, + cluster: testinghelpers.NewManagedCluster("test").Build(), + expectedMatch: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + evaluator, err := NewEvaluator() + if err != nil { + t.Errorf("unexpected err: %v", err) + } + result, err := evaluator.Evaluate(c.cluster, c.clusterselector.CelSelector.CelExpressions) + if c.expectedMatch != result { + t.Errorf("expected match to be %v but get : %v", c.expectedMatch, result) + } + if err != nil && !strings.Contains(err.Error(), c.expectedErr.Error()) { + t.Errorf("unexpected err %v", err) + } + }) + } +} diff --git a/pkg/placement/helpers/testing/builders.go b/pkg/placement/helpers/testing/builders.go index fc0ae61ee..3916bd816 100644 --- a/pkg/placement/helpers/testing/builders.go +++ b/pkg/placement/helpers/testing/builders.go @@ -309,6 +309,11 @@ func (b *ManagedClusterBuilder) WithDeletionTimestamp() *ManagedClusterBuilder { return b } +func (b *ManagedClusterBuilder) Withk8sVersion(version string) *ManagedClusterBuilder { + b.cluster.Status.Version.Kubernetes = version + return b +} + func (b *ManagedClusterBuilder) Build() *clusterapiv1.ManagedCluster { return b.cluster } diff --git a/vendor/open-cluster-management.io/api/cluster/v1alpha1/types_addonplacementscore.go b/vendor/open-cluster-management.io/api/cluster/v1alpha1/types_addonplacementscore.go index 1ce73580b..8b09fd3e9 100644 --- a/vendor/open-cluster-management.io/api/cluster/v1alpha1/types_addonplacementscore.go +++ b/vendor/open-cluster-management.io/api/cluster/v1alpha1/types_addonplacementscore.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -58,6 +59,8 @@ type AddOnPlacementScoreItem struct { // +kubebuilder:validation:Maximum:=100 // +required Value int32 `json:"value"` + + Quantity resource.Quantity `json:"quantity"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/vendor/open-cluster-management.io/api/cluster/v1beta1/0000_02_clusters.open-cluster-management.io_placements.crd.yaml b/vendor/open-cluster-management.io/api/cluster/v1beta1/0000_02_clusters.open-cluster-management.io_placements.crd.yaml index e2289658e..c9a440804 100644 --- a/vendor/open-cluster-management.io/api/cluster/v1beta1/0000_02_clusters.open-cluster-management.io_placements.crd.yaml +++ b/vendor/open-cluster-management.io/api/cluster/v1beta1/0000_02_clusters.open-cluster-management.io_placements.crd.yaml @@ -126,6 +126,13 @@ spec: description: LabelSelector to select clusters subset by label. properties: + celSelector: + properties: + celExpressions: + items: + type: string + type: array + type: object claimSelector: description: ClaimSelector represents a selector of ManagedClusters by clusterClaims in status @@ -256,6 +263,13 @@ spec: 3) If a ManagedCluster (not selected previously) starts to match the selector, it will either be selected or at least has a chance to be selected (when NumberOfClusters is specified); properties: + celSelector: + properties: + celExpressions: + items: + type: string + type: array + type: object claimSelector: description: ClaimSelector represents a selector of ManagedClusters by clusterClaims in status diff --git a/vendor/open-cluster-management.io/api/cluster/v1beta1/types_placement.go b/vendor/open-cluster-management.io/api/cluster/v1beta1/types_placement.go index 637ca5a4b..0d0c55d0f 100644 --- a/vendor/open-cluster-management.io/api/cluster/v1beta1/types_placement.go +++ b/vendor/open-cluster-management.io/api/cluster/v1beta1/types_placement.go @@ -173,6 +173,16 @@ type ClusterSelector struct { // ClaimSelector represents a selector of ManagedClusters by clusterClaims in status // +optional ClaimSelector ClusterClaimSelector `json:"claimSelector,omitempty"` + + // CelSelector represents a selector of ManagedClusters by CEL expressions on ManagedCluster fields + // +optional + CelSelector ClusterCelSelector `json:"celSelector,omitempty"` +} + +// ClusterCelSelector is a list of CEL expressions. The expressions are ANDed. +type ClusterCelSelector struct { + // +optional + CelExpressions []string `json:"celExpressions,omitempty"` } // ClusterClaimSelector is a claim query over a set of ManagedClusters. An empty cluster claim diff --git a/vendor/open-cluster-management.io/api/cluster/v1beta1/zz_generated.deepcopy.go b/vendor/open-cluster-management.io/api/cluster/v1beta1/zz_generated.deepcopy.go index cf2c2f1f0..0ca251ff8 100644 --- a/vendor/open-cluster-management.io/api/cluster/v1beta1/zz_generated.deepcopy.go +++ b/vendor/open-cluster-management.io/api/cluster/v1beta1/zz_generated.deepcopy.go @@ -26,6 +26,27 @@ func (in *AddOnScore) DeepCopy() *AddOnScore { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterCelSelector) DeepCopyInto(out *ClusterCelSelector) { + *out = *in + if in.CelExpressions != nil { + in, out := &in.CelExpressions, &out.CelExpressions + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterCelSelector. +func (in *ClusterCelSelector) DeepCopy() *ClusterCelSelector { + if in == nil { + return nil + } + out := new(ClusterCelSelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterClaimSelector) DeepCopyInto(out *ClusterClaimSelector) { *out = *in @@ -87,6 +108,7 @@ func (in *ClusterSelector) DeepCopyInto(out *ClusterSelector) { *out = *in in.LabelSelector.DeepCopyInto(&out.LabelSelector) in.ClaimSelector.DeepCopyInto(&out.ClaimSelector) + in.CelSelector.DeepCopyInto(&out.CelSelector) return }