Skip to content

Commit 69467d3

Browse files
authored
Merge pull request kubernetes#130648 from jpbetz/semver-tolerant
Enable Semver CEL library, add normalization support
2 parents d70bfc8 + 2d810dd commit 69467d3

File tree

7 files changed

+320
-25
lines changed

7 files changed

+320
-25
lines changed

staging/src/k8s.io/apiserver/pkg/cel/environment/base.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ var baseOptsWithoutStrictCost = []VersionedOptions{
176176
ext.TwoVarComprehensions(),
177177
},
178178
},
179+
// Semver
180+
{
181+
IntroducedVersion: version.MajorMinor(1, 33),
182+
EnvOptions: []cel.EnvOption{
183+
library.SemverLib(library.SemverVersion(1)),
184+
},
185+
},
179186
}
180187

181188
var (

staging/src/k8s.io/apiserver/pkg/cel/library/cost.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ package library
1818

1919
import (
2020
"fmt"
21+
"math"
22+
2123
"github.com/google/cel-go/checker"
2224
"github.com/google/cel-go/common"
2325
"github.com/google/cel-go/common/ast"
2426
"github.com/google/cel-go/common/types"
2527
"github.com/google/cel-go/common/types/ref"
2628
"github.com/google/cel-go/common/types/traits"
27-
"math"
2829

2930
"k8s.io/apiserver/pkg/cel"
3031
)
@@ -202,7 +203,7 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
202203

203204
return &cost
204205
}
205-
case "quantity", "isQuantity":
206+
case "quantity", "isQuantity", "semver", "isSemver":
206207
if len(args) >= 1 {
207208
cost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor))
208209
return &cost
@@ -236,7 +237,7 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
236237
// Simply dictionary lookup
237238
cost := uint64(1)
238239
return &cost
239-
case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub":
240+
case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub", "major", "minor", "patch":
240241
cost := uint64(1)
241242
return &cost
242243
case "getScheme", "getHostname", "getHost", "getPort", "getEscapedPath", "getQuery":
@@ -486,7 +487,7 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
486487

487488
return &checker.CallEstimate{CostEstimate: ipCompCost}
488489
}
489-
case "quantity", "isQuantity":
490+
case "quantity", "isQuantity", "semver", "isSemver":
490491
if target != nil {
491492
sz := l.sizeEstimate(args[0])
492493
return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor)}
@@ -498,7 +499,7 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
498499
}
499500
case "format.named":
500501
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
501-
case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub":
502+
case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub", "major", "minor", "patch":
502503
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
503504
case "getScheme", "getHostname", "getHost", "getPort", "getEscapedPath", "getQuery":
504505
// url accessors

staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ package library
1919
import (
2020
"context"
2121
"fmt"
22-
"github.com/google/cel-go/common/types/ref"
2322
"testing"
2423

24+
"github.com/google/cel-go/common/types/ref"
25+
2526
"github.com/google/cel-go/cel"
2627
"github.com/google/cel-go/checker"
2728
"github.com/google/cel-go/common"
@@ -1110,6 +1111,86 @@ func TestSetsCost(t *testing.T) {
11101111
}
11111112
}
11121113

1114+
func TestSemverCost(t *testing.T) {
1115+
cases := []struct {
1116+
name string
1117+
expr string
1118+
expectEstimatedCost checker.CostEstimate
1119+
expectRuntimeCost uint64
1120+
}{
1121+
{
1122+
name: "semver",
1123+
expr: `semver("1.0.0")`,
1124+
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
1125+
expectRuntimeCost: 1,
1126+
},
1127+
{
1128+
name: "semver long input",
1129+
expr: `semver("1234.56789012345.67890123456789")`,
1130+
expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4},
1131+
expectRuntimeCost: 4,
1132+
},
1133+
{
1134+
name: "isSemver",
1135+
expr: `isSemver("1.0.0")`,
1136+
expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1},
1137+
expectRuntimeCost: 1,
1138+
},
1139+
{
1140+
name: "isSemver long input",
1141+
expr: `isSemver("1234.56789012345.67890123456789")`,
1142+
expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4},
1143+
expectRuntimeCost: 4,
1144+
},
1145+
// major(), minor(), patch()
1146+
{
1147+
name: "major",
1148+
expr: `semver("1.2.3").major()`,
1149+
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
1150+
expectRuntimeCost: 2,
1151+
},
1152+
{
1153+
name: "minor",
1154+
expr: `semver("1.2.3").minor()`,
1155+
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
1156+
expectRuntimeCost: 2,
1157+
},
1158+
{
1159+
name: "patch",
1160+
expr: `semver("1.2.3").patch()`,
1161+
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
1162+
expectRuntimeCost: 2,
1163+
},
1164+
// isLessThan
1165+
{
1166+
name: "isLessThan",
1167+
expr: `semver("1.0.0").isLessThan(semver("1.1.0"))`,
1168+
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
1169+
expectRuntimeCost: 3,
1170+
},
1171+
// isGreaterThan
1172+
{
1173+
name: "isGreaterThan",
1174+
expr: `semver("1.1.0").isGreaterThan(semver("1.0.0"))`,
1175+
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
1176+
expectRuntimeCost: 3,
1177+
},
1178+
// compareTo
1179+
{
1180+
name: "compareTo",
1181+
expr: `semver("1.0.0").compareTo(semver("1.2.3"))`,
1182+
expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3},
1183+
expectRuntimeCost: 3,
1184+
},
1185+
}
1186+
1187+
for _, tc := range cases {
1188+
t.Run(tc.name, func(t *testing.T) {
1189+
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
1190+
})
1191+
}
1192+
}
1193+
11131194
func TestTwoVariableComprehensionCost(t *testing.T) {
11141195
cases := []struct {
11151196
name string
@@ -1223,6 +1304,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
12231304
// Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes.
12241305
cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)),
12251306
ext.TwoVarComprehensions(),
1307+
SemverLib(SemverVersion(1)),
12261308
)
12271309
if err != nil {
12281310
t.Fatalf("%v", err)

staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@ limitations under the License.
1717
package library
1818

1919
import (
20+
"strings"
21+
"testing"
22+
2023
"github.com/google/cel-go/cel"
2124
"github.com/google/cel-go/common/decls"
2225
"github.com/google/cel-go/common/types"
23-
"strings"
24-
"testing"
2526

2627
"k8s.io/apimachinery/pkg/util/sets"
2728
)
@@ -56,6 +57,8 @@ func TestLibraryCompatibility(t *testing.T) {
5657
"fieldSelector", "labelSelector", "validate", "format.named", "isSemver", "major", "minor", "patch", "semver",
5758
// Kubernetes <1.32>:
5859
"jsonpatch.escapeKey",
60+
// Kubernetes <1.33>:
61+
"semver", "isSemver", "major", "minor", "patch",
5962
// Kubernetes <1.??>:
6063
)
6164

staging/src/k8s.io/apiserver/pkg/cel/library/semver_test.go

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ import (
3131
library "k8s.io/apiserver/pkg/cel/library"
3232
)
3333

34-
func testSemver(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) {
34+
func testSemver(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string, version uint32) {
3535
env, err := cel.NewEnv(
36-
library.SemverLib(),
36+
library.SemverLib(library.SemverVersion(version)),
3737
)
3838
if err != nil {
3939
t.Fatalf("%v", err)
@@ -114,6 +114,7 @@ func TestSemver(t *testing.T) {
114114
expectValue ref.Val
115115
expectedCompileErr []string
116116
expectedRuntimeErr string
117+
version uint32
117118
}{
118119
{
119120
name: "parse",
@@ -131,15 +132,104 @@ func TestSemver(t *testing.T) {
131132
expectValue: trueVal,
132133
},
133134
{
134-
name: "isSemver_false",
135-
expr: `isSemver("v1.0")`,
135+
name: "isSemver_empty_false",
136+
expr: `isSemver("")`,
136137
expectValue: falseVal,
137138
},
139+
{
140+
name: "isSemver_v_prefix_false",
141+
expr: `isSemver("v1.0.0")`,
142+
expectValue: falseVal,
143+
},
144+
{
145+
name: "isSemver_v_leading_whitespace_false",
146+
expr: `isSemver(" 1.0.0")`,
147+
expectValue: falseVal,
148+
},
149+
{
150+
name: "isSemver_v_contains_whitespace_false",
151+
expr: `isSemver("1. 0.0")`,
152+
expectValue: falseVal,
153+
},
154+
{
155+
name: "isSemver_v_trailing_whitespace_false",
156+
expr: `isSemver("1.0.0 ")`,
157+
expectValue: falseVal,
158+
},
159+
{
160+
name: "isSemver_leading_zeros_false",
161+
expr: `isSemver("01.01.01")`,
162+
expectValue: falseVal,
163+
},
164+
{
165+
name: "isSemver_major_only_false",
166+
expr: `isSemver("1")`,
167+
expectValue: falseVal,
168+
},
169+
{
170+
name: "isSemver_major_minor_only_false",
171+
expr: `isSemver("1.1")`,
172+
expectValue: falseVal,
173+
},
174+
{
175+
name: "isSemver_empty_normalize_false",
176+
expr: `isSemver("", true)`,
177+
expectValue: falseVal,
178+
version: 1,
179+
},
180+
{
181+
name: "isSemver_v_leading_whitespace_normalize_false",
182+
expr: `isSemver(" 1.0.0", true)`,
183+
expectValue: falseVal,
184+
version: 1,
185+
},
186+
{
187+
name: "isSemver_v_contains_whitespace_normalize_false",
188+
expr: `isSemver("1. 0.0", true)`,
189+
expectValue: falseVal,
190+
version: 1,
191+
},
192+
{
193+
name: "isSemver_v_trailing_whitespace_normalize_false",
194+
expr: `isSemver("1.0.0 ", true)`,
195+
expectValue: falseVal,
196+
version: 1,
197+
},
198+
{
199+
name: "isSemver_v_prefix_normalize_true",
200+
expr: `isSemver("v1.0.0", true)`,
201+
expectValue: trueVal,
202+
version: 1,
203+
},
204+
{
205+
name: "isSemver_leading_zeros_normalize_true",
206+
expr: `isSemver("01.01.01", true)`,
207+
expectValue: trueVal,
208+
version: 1,
209+
},
210+
{
211+
name: "isSemver_major_only_normalize_true",
212+
expr: `isSemver("1", true)`,
213+
expectValue: trueVal,
214+
version: 1,
215+
},
216+
{
217+
name: "isSemver_major_minor_only_normalize_true",
218+
expr: `isSemver("1.1", true)`,
219+
expectValue: trueVal,
220+
version: 1,
221+
},
138222
{
139223
name: "isSemver_noOverload",
140224
expr: `isSemver([1, 2, 3])`,
141225
expectedCompileErr: []string{"found no matching overload for 'isSemver' applied to.*"},
142226
},
227+
{
228+
name: "equality_normalize",
229+
expr: `semver("v01.01", true) == semver("1.1.0")`,
230+
expectValue: trueVal,
231+
version: 1,
232+
},
143233
{
144234
name: "equality_reflexivity",
145235
expr: `semver("1.2.3") == semver("1.2.3")`,
@@ -204,7 +294,7 @@ func TestSemver(t *testing.T) {
204294

205295
for _, c := range cases {
206296
t.Run(c.name, func(t *testing.T) {
207-
testSemver(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr)
297+
testSemver(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr, c.version)
208298
})
209299
}
210300
}

0 commit comments

Comments
 (0)