Skip to content

Commit 7b41fd0

Browse files
pohlyk8s-publishing-bot
authored andcommitted
dra api: implement semver attribute value type
This adds support for semantic version comparison to the CEL support in the "named resources" structured parameter model. For example, it can be used to check that an instance supports a certain API level. To minimize the risk, the new "semver" type is only defined in the CEL environment for DRA expressions, not in the base library. See kubernetes/kubernetes#123664 for a PR which adds it to the base library. Validation of semver strings is done with the regular expression from semver.org. The actual evaluation at runtime then uses semver/v4. Kubernetes-commit: 42ee56f093133402ed860d4c5f54b049041386c9
1 parent c7efc8e commit 7b41fd0

File tree

6 files changed

+569
-25
lines changed

6 files changed

+569
-25
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module k8s.io/dynamic-resource-allocation
55
go 1.22.0
66

77
require (
8+
github.com/blang/semver/v4 v4.0.0
89
github.com/go-logr/logr v1.4.1
910
github.com/google/cel-go v0.17.8
1011
github.com/google/go-cmp v0.6.0

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/g
66
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
77
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
88
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
9+
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
910
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
1011
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
1112
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=

structured/namedresources/cel/compile.go

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"reflect"
2323

24+
"github.com/blang/semver/v4"
2425
"github.com/google/cel-go/cel"
2526
"github.com/google/cel-go/common/types"
2627
"github.com/google/cel-go/common/types/traits"
@@ -105,43 +106,54 @@ var valueTypes = map[string]struct {
105106
celType *cel.Type
106107
// get returns nil if the attribute doesn't have the type, otherwise
107108
// the value of that type.
108-
get func(attr resourceapi.NamedResourcesAttribute) any
109+
get func(attr resourceapi.NamedResourcesAttribute) (any, error)
109110
}{
110-
"quantity": {apiservercel.QuantityType, func(attr resourceapi.NamedResourcesAttribute) any {
111+
"quantity": {apiservercel.QuantityType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
111112
if attr.QuantityValue == nil {
112-
return nil
113+
return nil, nil
113114
}
114-
return apiservercel.Quantity{Quantity: attr.QuantityValue}
115+
return apiservercel.Quantity{Quantity: attr.QuantityValue}, nil
115116
}},
116-
"bool": {cel.BoolType, func(attr resourceapi.NamedResourcesAttribute) any {
117+
"bool": {cel.BoolType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
117118
if attr.BoolValue == nil {
118-
return nil
119+
return nil, nil
119120
}
120-
return *attr.BoolValue
121+
return *attr.BoolValue, nil
121122
}},
122-
"int": {cel.IntType, func(attr resourceapi.NamedResourcesAttribute) any {
123+
"int": {cel.IntType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
123124
if attr.IntValue == nil {
124-
return nil
125+
return nil, nil
125126
}
126-
return *attr.IntValue
127+
return *attr.IntValue, nil
127128
}},
128-
"intslice": {types.NewListType(cel.IntType), func(attr resourceapi.NamedResourcesAttribute) any {
129+
"intslice": {types.NewListType(cel.IntType), func(attr resourceapi.NamedResourcesAttribute) (any, error) {
129130
if attr.IntSliceValue == nil {
130-
return nil
131+
return nil, nil
131132
}
132-
return attr.IntSliceValue.Ints
133+
return attr.IntSliceValue.Ints, nil
133134
}},
134-
"string": {cel.StringType, func(attr resourceapi.NamedResourcesAttribute) any {
135+
"string": {cel.StringType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
135136
if attr.StringValue == nil {
136-
return nil
137+
return nil, nil
137138
}
138-
return *attr.StringValue
139+
return *attr.StringValue, nil
139140
}},
140-
"stringslice": {types.NewListType(cel.StringType), func(attr resourceapi.NamedResourcesAttribute) any {
141+
"stringslice": {types.NewListType(cel.StringType), func(attr resourceapi.NamedResourcesAttribute) (any, error) {
141142
if attr.StringSliceValue == nil {
142-
return nil
143+
return nil, nil
143144
}
144-
return attr.StringSliceValue.Strings
145+
return attr.StringSliceValue.Strings, nil
146+
}},
147+
"version": {SemverType, func(attr resourceapi.NamedResourcesAttribute) (any, error) {
148+
if attr.VersionValue == nil {
149+
return nil, nil
150+
}
151+
v, err := semver.Parse(*attr.VersionValue)
152+
if err != nil {
153+
return nil, fmt.Errorf("parse semantic version: %v", err)
154+
}
155+
156+
return Semver{Version: v}, nil
145157
}},
146158
}
147159

@@ -150,7 +162,11 @@ var boolType = reflect.TypeOf(true)
150162
func (c CompilationResult) Evaluate(ctx context.Context, attributes []resourceapi.NamedResourcesAttribute) (bool, error) {
151163
variables := make(map[string]any, len(valueTypes))
152164
for name, valueType := range valueTypes {
153-
variables[attributesVarPrefix+name] = buildValueMapper(c.Environment.CELTypeAdapter(), attributes, valueType.get)
165+
m, err := buildValueMapper(c.Environment.CELTypeAdapter(), attributes, valueType.get)
166+
if err != nil {
167+
return false, fmt.Errorf("extract attributes with type %s: %v", name, err)
168+
}
169+
variables[attributesVarPrefix+name] = m
154170
}
155171
result, _, err := c.Program.ContextEval(ctx, variables)
156172
if err != nil {
@@ -172,7 +188,9 @@ func mustBuildEnv() *environment.EnvSet {
172188
versioned := []environment.VersionedOptions{
173189
{
174190
IntroducedVersion: version.MajorMinor(1, 30),
175-
EnvOptions: buildVersionedAttributes(),
191+
EnvOptions: append(buildVersionedAttributes(),
192+
SemverLib(),
193+
),
176194
},
177195
}
178196
envset, err := envset.Extend(versioned...)
@@ -190,17 +208,21 @@ func buildVersionedAttributes() []cel.EnvOption {
190208
return options
191209
}
192210

193-
func buildValueMapper(adapter types.Adapter, attributes []resourceapi.NamedResourcesAttribute, get func(resourceapi.NamedResourcesAttribute) any) traits.Mapper {
211+
func buildValueMapper(adapter types.Adapter, attributes []resourceapi.NamedResourcesAttribute, get func(resourceapi.NamedResourcesAttribute) (any, error)) (traits.Mapper, error) {
194212
// This implementation constructs a map and then let's cel handle the
195213
// lookup and iteration. This is done for the sake of simplicity.
196214
// Whether it's faster than writing a custom mapper depends on
197215
// real-world attribute sets and CEL expressions and would have to be
198216
// benchmarked.
199217
valueMap := make(map[string]any)
200-
for _, attribute := range attributes {
201-
if value := get(attribute); value != nil {
218+
for name, attribute := range attributes {
219+
value, err := get(attribute)
220+
if err != nil {
221+
return nil, fmt.Errorf("attribute %q: %v", name, err)
222+
}
223+
if value != nil {
202224
valueMap[attribute.Name] = value
203225
}
204226
}
205-
return types.NewStringInterfaceMap(adapter, valueMap)
227+
return types.NewStringInterfaceMap(adapter, valueMap), nil
206228
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cel
18+
19+
import (
20+
"fmt"
21+
"reflect"
22+
23+
"github.com/blang/semver/v4"
24+
"github.com/google/cel-go/cel"
25+
"github.com/google/cel-go/common/types"
26+
"github.com/google/cel-go/common/types/ref"
27+
)
28+
29+
var (
30+
SemverType = cel.ObjectType("kubernetes.Semver")
31+
)
32+
33+
// Semver provdes a CEL representation of a [semver.Version].
34+
type Semver struct {
35+
semver.Version
36+
}
37+
38+
func (v Semver) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
39+
if reflect.TypeOf(v.Version).AssignableTo(typeDesc) {
40+
return v.Version, nil
41+
}
42+
if reflect.TypeOf("").AssignableTo(typeDesc) {
43+
return v.Version.String(), nil
44+
}
45+
return nil, fmt.Errorf("type conversion error from 'Semver' to '%v'", typeDesc)
46+
}
47+
48+
func (v Semver) ConvertToType(typeVal ref.Type) ref.Val {
49+
switch typeVal {
50+
case SemverType:
51+
return v
52+
case types.TypeType:
53+
return SemverType
54+
default:
55+
return types.NewErr("type conversion error from '%s' to '%s'", SemverType, typeVal)
56+
}
57+
}
58+
59+
func (v Semver) Equal(other ref.Val) ref.Val {
60+
otherDur, ok := other.(Semver)
61+
if !ok {
62+
return types.MaybeNoSuchOverloadErr(other)
63+
}
64+
return types.Bool(v.Version.EQ(otherDur.Version))
65+
}
66+
67+
func (v Semver) Type() ref.Type {
68+
return SemverType
69+
}
70+
71+
func (v Semver) Value() interface{} {
72+
return v.Version
73+
}

0 commit comments

Comments
 (0)