Skip to content

Commit c6fc1de

Browse files
committed
use kubernetes cel library
Signed-off-by: Qing Hao <qhao@redhat.com>
1 parent 5e95094 commit c6fc1de

File tree

1 file changed

+100
-92
lines changed
  • enhancements/sig-architecture/136-placement-cel-selector

1 file changed

+100
-92
lines changed

enhancements/sig-architecture/136-placement-cel-selector/README.md

+100-92
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ type ClusterCelSelector struct {
7070
A ManagedClusterLib will be added.
7171
- Variable `managedCluster` will be added.
7272
- Function `scores` will be added.
73-
- Function `versionIsGreaterThan`, `versionIsLessThan` will be added.
74-
- Function `quantityIsGreaterThan`, `quantityIsLessThan` will be added.
73+
- Function `parseJSON` will be added.
74+
- Kubernetes semver library is included.
75+
- Kubernetes quantity library is included.
7576

7677
```go
7778
// ManagedClusterLib defines the CEL library for ManagedCluster evaluation.
@@ -96,72 +97,59 @@ A ManagedClusterLib will be added.
9697
// - name: string - The name of the score
9798
// - value: int - The numeric score value
9899
// - quantity: number|string - The quantity value, represented as:
99-
// * number: for pure decimal values (e.g., 3)
100-
// * string: for values with units or decimal places (e.g., "300Mi", "1.5Gi")
100+
// - number: for pure decimal values (e.g., 3)
101+
// - string: for values with units or decimal places (e.g., "300Mi", "1.5Gi")
101102
//
102103
// Examples:
103104
//
104-
// managedCluster.scores("cpu-memory") // returns [{name: "cpu", value: 3, quantity: 3}, {name: "memory", value: 4, quantity: "300Mi"}]
105+
// managedCluster.scores("cpu-memory") // returns [{name: "cpu", value: 3, quantity: 3"}, {name: "memory", value: 4, quantity: "300Mi"}]
105106
//
106-
// Version Comparisons:
107+
// parseJSON
107108
//
108-
// versionIsGreaterThan
109+
// Parses a JSON string into a CEL-compatible map or list.
109110
//
110-
// Returns true if the first version string is greater than the second version string.
111-
// The version must follow Semantic Versioning specification (http://semver.org/).
112-
// It can be with or without 'v' prefix (eg, "1.14.3" or "v1.14.3").
111+
// parseJSON(<string>) <dyn>
113112
//
114-
// versionIsGreaterThan(<string>, <string>) <bool>
113+
// Takes a single string argument, attempts to parse it as JSON, and returns the resulting
114+
// data structure as a CEL-compatible value. If the input is not a valid JSON string, it returns an error.
115115
//
116116
// Examples:
117117
//
118-
// versionIsGreaterThan("1.25.0", "1.24.0") // returns true
119-
// versionIsGreaterThan("1.24.0", "1.25.0") // returns false
118+
// parseJSON("{\"key\": \"value\"}") // returns a map with key-value pairs
120119
//
121-
// versionIsLessThan
120+
// Semver Library:
122121
//
123-
// Returns true if the first version string is less than the second version string.
124-
// The version must follow Semantic Versioning specification (http://semver.org/).
125-
// It can be with or without 'v' prefix (eg, "1.14.3" or "v1.14.3").
126-
//
127-
// versionIsLessThan(<string>, <string>) <bool>
122+
// Semver provides a CEL function library extension for [semver.Version].
123+
// Upstream enhancement to support v is a prefix https://github.com/kubernetes/kubernetes/pull/130648.
128124
//
129125
// Examples:
130126
//
131-
// versionIsLessThan("1.24.0", "1.25.0") // returns true
132-
// versionIsLessThan("1.25.0", "1.24.0") // returns false
133-
//
134-
// Quantity Comparisons:
135-
//
136-
// quantityIsGreaterThan
127+
// semver("1.2.3").isLessThan(semver("1.2.4")) // returns true
128+
// semver("1.2.3").isGreaterThan(semver("1.2.4")) // returns false
137129
//
138-
// Returns true if the first quantity string is greater than the second quantity string.
130+
// Quantity Library:
139131
//
140-
// quantityIsGreaterThan(<string>, <string>) <bool>
132+
// Quantity provides a CEL function library extension of Kubernetes resource.
141133
//
142134
// Examples:
143135
//
144-
// quantityIsGreaterThan("2Gi", "1Gi") // returns true
145-
// quantityIsGreaterThan("1Gi", "2Gi") // returns false
146-
// quantityIsGreaterThan("1000Mi", "1Gi") // returns false
147-
//
148-
// quantityIsLessThan
149-
//
150-
// Returns true if the first quantity string is less than the second quantity string.
151-
//
152-
// quantityIsLessThan(<string>, <string>) <bool>
153-
//
154-
// Examples:
155-
//
156-
// quantityIsLessThan("1Gi", "2Gi") // returns true
157-
// quantityIsLessThan("2Gi", "1Gi") // returns false
158-
// quantityIsLessThan("1000Mi", "1Gi") // returns true
136+
// quantity("100Mi").isLessThan(quantity("200Mi")) // returns true
137+
// quantity("100Mi").isGreaterThan(quantity("200Mi")) // returns false
159138

160139
type ManagedClusterLib struct{}
161140

162141
// CompileOptions implements cel.Library interface to provide compile-time options.
163142
func (ManagedClusterLib) CompileOptions() []cel.EnvOption {
164143
return []cel.EnvOption{
144+
// Add the extended strings library
145+
ext.Strings(),
146+
147+
// Add the kubernetes semver library
148+
library.SemverLib(),
149+
150+
// Add the kubernetes quantity library
151+
library.Quantity(),
152+
165153
// The input types may either be instances of `proto.Message` or `ref.Type`.
166154
// Here we use func ConvertManagedCluster() to convert ManagedCluster to a Map.
167155
cel.Variable("managedCluster", cel.MapType(cel.StringType, cel.DynType)),
@@ -174,36 +162,12 @@ func (ManagedClusterLib) CompileOptions() []cel.EnvOption {
174162
cel.FunctionBinding(clusterScores)),
175163
),
176164

177-
cel.Function("versionIsGreaterThan",
178-
cel.MemberOverload(
179-
"version_is_greater_than",
180-
[]*cel.Type{cel.StringType, cel.StringType},
181-
cel.BoolType,
182-
cel.FunctionBinding(versionIsGreaterThan)),
183-
),
184-
185-
cel.Function("versionIsLessThan",
186-
cel.MemberOverload(
187-
"version_is_less_than",
188-
[]*cel.Type{cel.StringType, cel.StringType},
189-
cel.BoolType,
190-
cel.FunctionBinding(versionIsLessThan)),
191-
),
192-
193-
cel.Function("quantityIsGreaterThan",
194-
cel.MemberOverload(
195-
"quantity_is_greater_than",
196-
[]*cel.Type{cel.StringType, cel.StringType},
197-
cel.BoolType,
198-
cel.FunctionBinding(quantityIsGreaterThan)),
199-
),
200-
201-
cel.Function("quantityIsLessThan",
202-
cel.MemberOverload(
203-
"quantity_is_less_than",
204-
[]*cel.Type{cel.StringType, cel.StringType},
205-
cel.BoolType,
206-
cel.FunctionBinding(quantityIsLessThan)),
165+
cel.Function("parseJSON",
166+
cel.MemberOverload("parse_json_string",
167+
[]*cel.Type{cel.StringType},
168+
cel.DynType,
169+
cel.FunctionBinding(l.parseJSON),
170+
),
207171
),
208172
}
209173
}
@@ -223,6 +187,15 @@ func NewEvaluator() (*Evaluator, error) {
223187
}
224188
```
225189

190+
### Metrics for time spent evaluating CEL at runtime.
191+
- TBD
192+
193+
### Cost budget
194+
- TBD
195+
196+
### CEL validation error message
197+
- TBD
198+
226199
### Examples
227200

228201
1. The user can select clusters by Kubernetes version listed in `managedCluster.Status.version.kubernetes`.
@@ -234,14 +207,11 @@ metadata:
234207
name: placement1
235208
namespace: default
236209
spec:
237-
numberOfClusters: 3
238-
clusterSets:
239-
- prod
240210
predicates:
241211
- requiredClusterSelector:
242212
celSelector:
243213
celExpressions:
244-
- managedCluster.Status.version.kubernetes == "v1.30.0"
214+
- managedCluster.status.version.kubernetes == "v1.30.0"
245215
```
246216
2. The user can use CEL [Standard macros](https://github.com/google/cel-spec/blob/master/doc/langdef.md#macros) and [Standard functions](https://github.com/google/cel-spec/blob/master/doc/langdef.md#standard-definitions) on the `managedCluster` fields.
247217

@@ -254,9 +224,6 @@ metadata:
254224
name: placement1
255225
namespace: default
256226
spec:
257-
numberOfClusters: 3
258-
clusterSets:
259-
- prod
260227
predicates:
261228
- requiredClusterSelector:
262229
celSelector:
@@ -273,9 +240,6 @@ metadata:
273240
name: placement1
274241
namespace: default
275242
spec:
276-
numberOfClusters: 3
277-
clusterSets:
278-
- prod
279243
predicates:
280244
- requiredClusterSelector:
281245
celSelector:
@@ -284,7 +248,7 @@ spec:
284248
```
285249

286250

287-
3. The user can use CEL customized functions `versionIsGreaterThan` and `versionIsLessThan` to select clusters by version comparison. For example, select clusters whose kubernetes version > v1.13.0.
251+
3. The user can use Kubernetes semver library functions `isLessThan` and `isGreaterThan` to select clusters by version comparison. For example, select clusters whose kubernetes version > v1.13.0.
288252

289253

290254
```yaml
@@ -294,18 +258,17 @@ metadata:
294258
name: placement1
295259
namespace: default
296260
spec:
297-
numberOfClusters: 3
298-
clusterSets:
299-
- prod
300261
predicates:
301262
- requiredClusterSelector:
302263
celSelector:
303264
celExpressions:
304-
- managedCluster.Status.version.kubernetes.versionIsGreaterThan("v1.30.0")
265+
# Upstream enhancement to support v is a prefix https://github.com/kubernetes/kubernetes/pull/130648.
266+
- semver(managedCluster.status.version.kubernetes, true).isGreaterThan(semver("v1.30.0", true))
305267
```
306268

307269

308-
4. The user can use CEL customized function `score` to select clusters by `AddonPlacementScore`. For example, select clusters whose cpuAvailable score < 2.
270+
4. The user can use CEL customized function `scores` to select clusters by `AddonPlacementScore`. And use Kubernetes quantity library function `isLessThan` and `isGreaterThan` to compare quantities.
271+
For example, select clusters whose cpuAvailable quantity > 4 and memAvailable quantity > 100Mi.
309272

310273

311274
```yaml
@@ -315,17 +278,62 @@ metadata:
315278
name: placement1
316279
namespace: default
317280
spec:
318-
numberOfClusters: 3
319-
clusterSets:
320-
- prod
321281
predicates:
322282
- requiredClusterSelector:
323283
celSelector:
324284
celExpressions:
325285
- managedCluster.scores("default").filter(s, s.name == 'cpuAvailable').all(e, e.quantity > 4)
326-
- managedCluster.scores("default").filter(s, s.name == 'memAvailable').all(e, e.quantity.quantityIsGreaterThan("100Mi"))
286+
- managedCluster.scores("default").filter(s, s.name == 'memAvailable').all(e, quantity(e.quantity).isGreaterThan(quantity("100Mi")))
327287
```
328288

289+
5. The user can use CEL to prase properties with format like, a comma separated string or a json format string.
290+
291+
```yaml
292+
apiVersion: multicluster.x-k8s.io/v1alpha1
293+
kind: ClusterProfile
294+
metadata:
295+
name: bravelion
296+
namespace: ...
297+
...
298+
status:
299+
properties:
300+
- name: sku.node.k8s.io
301+
value: g6.xlarge,Standard_NC48ads_H100,m3-ultramem-32
302+
- name: sku.gpu.kubernetes-fleet.io
303+
value: |
304+
{
305+
"H100": [
306+
{
307+
"Standard_NC48ads_H100_v4": 10
308+
}
309+
{
310+
"Standard_NC96ads_H100_v4": 2
311+
}
312+
]
313+
"A100": [
314+
{
315+
"Standard_NC48ads_A100_v4": 50
316+
}
317+
{
318+
"Standard_NC96ads_A100_v4": 20
319+
}
320+
]
321+
}
322+
...
323+
```
324+
325+
```yaml
326+
apiVersion: cluster.open-cluster-management.io/v1beta1
327+
kind: Placement
328+
329+
spec:
330+
predicates:
331+
- requiredClusterSelector:
332+
celSelector:
333+
celExpressions:
334+
- managedCluster.status.properties.exists(c, c.name == "sku.node.k8s.io" && c.value.split(",").exists(e, e == "g6.xlarge"))
335+
- managedCluster.status.properties.exists(c, c.name == "sku.gpu.kubernetes-fleet.io" && c.value.parseJSON().H100.exists(e, e.Standard_NC96ads_H100_v4 == 2))
336+
```
329337

330338
### Test Plan
331339

0 commit comments

Comments
 (0)