mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 02:08:13 +00:00 
			
		
		
		
	Merge pull request #130648 from jpbetz/semver-tolerant
Enable Semver CEL library, add normalization support
This commit is contained in:
		| @@ -176,6 +176,13 @@ var baseOptsWithoutStrictCost = []VersionedOptions{ | |||||||
| 			ext.TwoVarComprehensions(), | 			ext.TwoVarComprehensions(), | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
|  | 	// Semver | ||||||
|  | 	{ | ||||||
|  | 		IntroducedVersion: version.MajorMinor(1, 33), | ||||||
|  | 		EnvOptions: []cel.EnvOption{ | ||||||
|  | 			library.SemverLib(library.SemverVersion(1)), | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
|   | |||||||
| @@ -18,13 +18,14 @@ package library | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"math" | ||||||
|  |  | ||||||
| 	"github.com/google/cel-go/checker" | 	"github.com/google/cel-go/checker" | ||||||
| 	"github.com/google/cel-go/common" | 	"github.com/google/cel-go/common" | ||||||
| 	"github.com/google/cel-go/common/ast" | 	"github.com/google/cel-go/common/ast" | ||||||
| 	"github.com/google/cel-go/common/types" | 	"github.com/google/cel-go/common/types" | ||||||
| 	"github.com/google/cel-go/common/types/ref" | 	"github.com/google/cel-go/common/types/ref" | ||||||
| 	"github.com/google/cel-go/common/types/traits" | 	"github.com/google/cel-go/common/types/traits" | ||||||
| 	"math" |  | ||||||
|  |  | ||||||
| 	"k8s.io/apiserver/pkg/cel" | 	"k8s.io/apiserver/pkg/cel" | ||||||
| ) | ) | ||||||
| @@ -202,7 +203,7 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re | |||||||
|  |  | ||||||
| 			return &cost | 			return &cost | ||||||
| 		} | 		} | ||||||
| 	case "quantity", "isQuantity": | 	case "quantity", "isQuantity", "semver", "isSemver": | ||||||
| 		if len(args) >= 1 { | 		if len(args) >= 1 { | ||||||
| 			cost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor)) | 			cost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor)) | ||||||
| 			return &cost | 			return &cost | ||||||
| @@ -236,7 +237,7 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re | |||||||
| 		// Simply dictionary lookup | 		// Simply dictionary lookup | ||||||
| 		cost := uint64(1) | 		cost := uint64(1) | ||||||
| 		return &cost | 		return &cost | ||||||
| 	case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub": | 	case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub", "major", "minor", "patch": | ||||||
| 		cost := uint64(1) | 		cost := uint64(1) | ||||||
| 		return &cost | 		return &cost | ||||||
| 	case "getScheme", "getHostname", "getHost", "getPort", "getEscapedPath", "getQuery": | 	case "getScheme", "getHostname", "getHost", "getPort", "getEscapedPath", "getQuery": | ||||||
| @@ -486,7 +487,7 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch | |||||||
|  |  | ||||||
| 			return &checker.CallEstimate{CostEstimate: ipCompCost} | 			return &checker.CallEstimate{CostEstimate: ipCompCost} | ||||||
| 		} | 		} | ||||||
| 	case "quantity", "isQuantity": | 	case "quantity", "isQuantity", "semver", "isSemver": | ||||||
| 		if target != nil { | 		if target != nil { | ||||||
| 			sz := l.sizeEstimate(args[0]) | 			sz := l.sizeEstimate(args[0]) | ||||||
| 			return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor)} | 			return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor)} | ||||||
| @@ -498,7 +499,7 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch | |||||||
| 		} | 		} | ||||||
| 	case "format.named": | 	case "format.named": | ||||||
| 		return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}} | 		return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}} | ||||||
| 	case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub": | 	case "sign", "asInteger", "isInteger", "asApproximateFloat", "isGreaterThan", "isLessThan", "compareTo", "add", "sub", "major", "minor", "patch": | ||||||
| 		return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}} | 		return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}} | ||||||
| 	case "getScheme", "getHostname", "getHost", "getPort", "getEscapedPath", "getQuery": | 	case "getScheme", "getHostname", "getHost", "getPort", "getEscapedPath", "getQuery": | ||||||
| 		// url accessors | 		// url accessors | ||||||
|   | |||||||
| @@ -19,9 +19,10 @@ package library | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"github.com/google/cel-go/common/types/ref" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/google/cel-go/common/types/ref" | ||||||
|  |  | ||||||
| 	"github.com/google/cel-go/cel" | 	"github.com/google/cel-go/cel" | ||||||
| 	"github.com/google/cel-go/checker" | 	"github.com/google/cel-go/checker" | ||||||
| 	"github.com/google/cel-go/common" | 	"github.com/google/cel-go/common" | ||||||
| @@ -1110,6 +1111,86 @@ func TestSetsCost(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestSemverCost(t *testing.T) { | ||||||
|  | 	cases := []struct { | ||||||
|  | 		name                string | ||||||
|  | 		expr                string | ||||||
|  | 		expectEstimatedCost checker.CostEstimate | ||||||
|  | 		expectRuntimeCost   uint64 | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:                "semver", | ||||||
|  | 			expr:                `semver("1.0.0")`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1}, | ||||||
|  | 			expectRuntimeCost:   1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:                "semver long input", | ||||||
|  | 			expr:                `semver("1234.56789012345.67890123456789")`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4}, | ||||||
|  | 			expectRuntimeCost:   4, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:                "isSemver", | ||||||
|  | 			expr:                `isSemver("1.0.0")`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 1, Max: 1}, | ||||||
|  | 			expectRuntimeCost:   1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:                "isSemver long input", | ||||||
|  | 			expr:                `isSemver("1234.56789012345.67890123456789")`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 4, Max: 4}, | ||||||
|  | 			expectRuntimeCost:   4, | ||||||
|  | 		}, | ||||||
|  | 		// major(), minor(), patch() | ||||||
|  | 		{ | ||||||
|  | 			name:                "major", | ||||||
|  | 			expr:                `semver("1.2.3").major()`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2}, | ||||||
|  | 			expectRuntimeCost:   2, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:                "minor", | ||||||
|  | 			expr:                `semver("1.2.3").minor()`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2}, | ||||||
|  | 			expectRuntimeCost:   2, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:                "patch", | ||||||
|  | 			expr:                `semver("1.2.3").patch()`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2}, | ||||||
|  | 			expectRuntimeCost:   2, | ||||||
|  | 		}, | ||||||
|  | 		// isLessThan | ||||||
|  | 		{ | ||||||
|  | 			name:                "isLessThan", | ||||||
|  | 			expr:                `semver("1.0.0").isLessThan(semver("1.1.0"))`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3}, | ||||||
|  | 			expectRuntimeCost:   3, | ||||||
|  | 		}, | ||||||
|  | 		// isGreaterThan | ||||||
|  | 		{ | ||||||
|  | 			name:                "isGreaterThan", | ||||||
|  | 			expr:                `semver("1.1.0").isGreaterThan(semver("1.0.0"))`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3}, | ||||||
|  | 			expectRuntimeCost:   3, | ||||||
|  | 		}, | ||||||
|  | 		// compareTo | ||||||
|  | 		{ | ||||||
|  | 			name:                "compareTo", | ||||||
|  | 			expr:                `semver("1.0.0").compareTo(semver("1.2.3"))`, | ||||||
|  | 			expectEstimatedCost: checker.CostEstimate{Min: 3, Max: 3}, | ||||||
|  | 			expectRuntimeCost:   3, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range cases { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestTwoVariableComprehensionCost(t *testing.T) { | func TestTwoVariableComprehensionCost(t *testing.T) { | ||||||
| 	cases := []struct { | 	cases := []struct { | ||||||
| 		name                string | 		name                string | ||||||
| @@ -1223,6 +1304,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate | |||||||
| 		// Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes. | 		// Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes. | ||||||
| 		cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)), | 		cel.CostEstimatorOptions(checker.PresenceTestHasCost(false)), | ||||||
| 		ext.TwoVarComprehensions(), | 		ext.TwoVarComprehensions(), | ||||||
|  | 		SemverLib(SemverVersion(1)), | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("%v", err) | 		t.Fatalf("%v", err) | ||||||
|   | |||||||
| @@ -17,11 +17,12 @@ limitations under the License. | |||||||
| package library | package library | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/google/cel-go/cel" | 	"github.com/google/cel-go/cel" | ||||||
| 	"github.com/google/cel-go/common/decls" | 	"github.com/google/cel-go/common/decls" | ||||||
| 	"github.com/google/cel-go/common/types" | 	"github.com/google/cel-go/common/types" | ||||||
| 	"strings" |  | ||||||
| 	"testing" |  | ||||||
|  |  | ||||||
| 	"k8s.io/apimachinery/pkg/util/sets" | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
| ) | ) | ||||||
| @@ -56,6 +57,8 @@ func TestLibraryCompatibility(t *testing.T) { | |||||||
| 		"fieldSelector", "labelSelector", "validate", "format.named", "isSemver", "major", "minor", "patch", "semver", | 		"fieldSelector", "labelSelector", "validate", "format.named", "isSemver", "major", "minor", "patch", "semver", | ||||||
| 		// Kubernetes <1.32>: | 		// Kubernetes <1.32>: | ||||||
| 		"jsonpatch.escapeKey", | 		"jsonpatch.escapeKey", | ||||||
|  | 		// Kubernetes <1.33>: | ||||||
|  | 		"semver", "isSemver", "major", "minor", "patch", | ||||||
| 		// Kubernetes <1.??>: | 		// Kubernetes <1.??>: | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,9 +31,9 @@ import ( | |||||||
| 	library "k8s.io/apiserver/pkg/cel/library" | 	library "k8s.io/apiserver/pkg/cel/library" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func testSemver(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) { | func testSemver(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string, version uint32) { | ||||||
| 	env, err := cel.NewEnv( | 	env, err := cel.NewEnv( | ||||||
| 		library.SemverLib(), | 		library.SemverLib(library.SemverVersion(version)), | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("%v", err) | 		t.Fatalf("%v", err) | ||||||
| @@ -114,6 +114,7 @@ func TestSemver(t *testing.T) { | |||||||
| 		expectValue        ref.Val | 		expectValue        ref.Val | ||||||
| 		expectedCompileErr []string | 		expectedCompileErr []string | ||||||
| 		expectedRuntimeErr string | 		expectedRuntimeErr string | ||||||
|  | 		version            uint32 | ||||||
| 	}{ | 	}{ | ||||||
| 		{ | 		{ | ||||||
| 			name:        "parse", | 			name:        "parse", | ||||||
| @@ -131,15 +132,104 @@ func TestSemver(t *testing.T) { | |||||||
| 			expectValue: trueVal, | 			expectValue: trueVal, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name:        "isSemver_false", | 			name:        "isSemver_empty_false", | ||||||
| 			expr:        `isSemver("v1.0")`, | 			expr:        `isSemver("")`, | ||||||
| 			expectValue: falseVal, | 			expectValue: falseVal, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_v_prefix_false", | ||||||
|  | 			expr:        `isSemver("v1.0.0")`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_v_leading_whitespace_false", | ||||||
|  | 			expr:        `isSemver(" 1.0.0")`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_v_contains_whitespace_false", | ||||||
|  | 			expr:        `isSemver("1. 0.0")`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_v_trailing_whitespace_false", | ||||||
|  | 			expr:        `isSemver("1.0.0 ")`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_leading_zeros_false", | ||||||
|  | 			expr:        `isSemver("01.01.01")`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_major_only_false", | ||||||
|  | 			expr:        `isSemver("1")`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_major_minor_only_false", | ||||||
|  | 			expr:        `isSemver("1.1")`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_empty_normalize_false", | ||||||
|  | 			expr:        `isSemver("", true)`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 			version:     1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_v_leading_whitespace_normalize_false", | ||||||
|  | 			expr:        `isSemver(" 1.0.0", true)`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 			version:     1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_v_contains_whitespace_normalize_false", | ||||||
|  | 			expr:        `isSemver("1. 0.0", true)`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 			version:     1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_v_trailing_whitespace_normalize_false", | ||||||
|  | 			expr:        `isSemver("1.0.0 ", true)`, | ||||||
|  | 			expectValue: falseVal, | ||||||
|  | 			version:     1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_v_prefix_normalize_true", | ||||||
|  | 			expr:        `isSemver("v1.0.0", true)`, | ||||||
|  | 			expectValue: trueVal, | ||||||
|  | 			version:     1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_leading_zeros_normalize_true", | ||||||
|  | 			expr:        `isSemver("01.01.01", true)`, | ||||||
|  | 			expectValue: trueVal, | ||||||
|  | 			version:     1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_major_only_normalize_true", | ||||||
|  | 			expr:        `isSemver("1", true)`, | ||||||
|  | 			expectValue: trueVal, | ||||||
|  | 			version:     1, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "isSemver_major_minor_only_normalize_true", | ||||||
|  | 			expr:        `isSemver("1.1", true)`, | ||||||
|  | 			expectValue: trueVal, | ||||||
|  | 			version:     1, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name:               "isSemver_noOverload", | 			name:               "isSemver_noOverload", | ||||||
| 			expr:               `isSemver([1, 2, 3])`, | 			expr:               `isSemver([1, 2, 3])`, | ||||||
| 			expectedCompileErr: []string{"found no matching overload for 'isSemver' applied to.*"}, | 			expectedCompileErr: []string{"found no matching overload for 'isSemver' applied to.*"}, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:        "equality_normalize", | ||||||
|  | 			expr:        `semver("v01.01", true) == semver("1.1.0")`, | ||||||
|  | 			expectValue: trueVal, | ||||||
|  | 			version:     1, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name:        "equality_reflexivity", | 			name:        "equality_reflexivity", | ||||||
| 			expr:        `semver("1.2.3") == semver("1.2.3")`, | 			expr:        `semver("1.2.3") == semver("1.2.3")`, | ||||||
| @@ -204,7 +294,7 @@ func TestSemver(t *testing.T) { | |||||||
|  |  | ||||||
| 	for _, c := range cases { | 	for _, c := range cases { | ||||||
| 		t.Run(c.name, func(t *testing.T) { | 		t.Run(c.name, func(t *testing.T) { | ||||||
| 			testSemver(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr) | 			testSemver(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr, c.version) | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,6 +17,10 @@ limitations under the License. | |||||||
| package library | package library | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"math" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/blang/semver/v4" | 	"github.com/blang/semver/v4" | ||||||
| 	"github.com/google/cel-go/cel" | 	"github.com/google/cel-go/cel" | ||||||
| 	"github.com/google/cel-go/common/types" | 	"github.com/google/cel-go/common/types" | ||||||
| @@ -31,8 +35,10 @@ import ( | |||||||
| // | // | ||||||
| // Converts a string to a semantic version or results in an error if the string is not a valid semantic version. Refer | // Converts a string to a semantic version or results in an error if the string is not a valid semantic version. Refer | ||||||
| // to semver.org documentation for information on accepted patterns. | // to semver.org documentation for information on accepted patterns. | ||||||
| // | // An optional "normalize" argument can be passed to enable normalization. Normalization removes any "v" prefix, adds a | ||||||
|  | // 0 minor and patch numbers to versions with only major or major.minor components specified, and removes any leading 0s. | ||||||
| //	semver(<string>) <Semver> | //	semver(<string>) <Semver> | ||||||
|  | //	semver(<string>, <bool>) <Semver> | ||||||
| // | // | ||||||
| // Examples: | // Examples: | ||||||
| // | // | ||||||
| @@ -41,19 +47,28 @@ import ( | |||||||
| //	semver('200K') // error | //	semver('200K') // error | ||||||
| //	semver('Three') // error | //	semver('Three') // error | ||||||
| //	semver('Mi') // error | //	semver('Mi') // error | ||||||
|  | //	semver('v1.0.0', true) // Applies normalization to remove the leading "v". Returns a Semver of "1.0.0". | ||||||
|  | //	semver('1.0', true) // Applies normalization to add the missing patch version. Returns a Semver of "1.0.0" | ||||||
|  | //	semver('01.01.01', true) // Applies normalization to remove leading zeros. Returns a Semver of "1.1.1" | ||||||
| // | // | ||||||
| // isSemver | // isSemver | ||||||
| // | // | ||||||
| // Returns true if a string is a valid Semver. isSemver returns true if and | // Returns true if a string is a valid Semver. isSemver returns true if and | ||||||
| // only if semver does not result in error. | // only if semver does not result in error. | ||||||
|  | // An optional "normalize" argument can be passed to enable normalization. Normalization removes any "v" prefix, adds a | ||||||
|  | // 0 minor and patch numbers to versions with only major or major.minor components specified, and removes any leading 0s. | ||||||
| // | // | ||||||
| //	isSemver( <string>) <bool> | //	isSemver( <string>) <bool> | ||||||
|  | //	isSemver( <string>, <bool>) <bool> | ||||||
| // | // | ||||||
| // Examples: | // Examples: | ||||||
| // | // | ||||||
| //	isSemver('1.0.0') // returns true | //	isSemver('1.0.0') // returns true | ||||||
| //	isSemver('v1.0') // returns true (tolerant parsing) |  | ||||||
| //	isSemver('hello') // returns false | //	isSemver('hello') // returns false | ||||||
|  | //  isSemver('v1.0')  // returns false (leading "v" is not allowed unless normalization is enabled) | ||||||
|  | //	isSemver('v1.0', true) // Applies normalization to remove leading "v". returns true | ||||||
|  | //	semver('1.0', true) // Applies normalization to add the missing patch version. Returns true | ||||||
|  | //	semver('01.01.01', true) // Applies normalization to remove leading zeros. Returns true | ||||||
| // | // | ||||||
| // Conversion to Scalars: | // Conversion to Scalars: | ||||||
| // | // | ||||||
| @@ -84,13 +99,29 @@ import ( | |||||||
| // semver("1.2.3").compareTo(semver("2.0.0")) // returns -1 | // semver("1.2.3").compareTo(semver("2.0.0")) // returns -1 | ||||||
| // semver("1.2.3").compareTo(semver("0.1.2")) // returns 1 | // semver("1.2.3").compareTo(semver("0.1.2")) // returns 1 | ||||||
|  |  | ||||||
| func SemverLib() cel.EnvOption { | func SemverLib(options ...SemverOption) cel.EnvOption { | ||||||
|  | 	semverLib := &semverLibType{} | ||||||
|  | 	for _, o := range options { | ||||||
|  | 		semverLib = o(semverLib) | ||||||
|  | 	} | ||||||
| 	return cel.Lib(semverLib) | 	return cel.Lib(semverLib) | ||||||
| } | } | ||||||
|  |  | ||||||
| var semverLib = &semverLibType{} | var semverLib = &semverLibType{version: math.MaxUint32} // include all versions | ||||||
|  |  | ||||||
| type semverLibType struct{} | type semverLibType struct { | ||||||
|  | 	version uint32 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // StringsOption is a functional interface for configuring the strings library. | ||||||
|  | type SemverOption func(*semverLibType) *semverLibType | ||||||
|  |  | ||||||
|  | func SemverVersion(version uint32) SemverOption { | ||||||
|  | 	return func(lib *semverLibType) *semverLibType { | ||||||
|  | 		lib.version = version | ||||||
|  | 		return lib | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func (*semverLibType) LibraryName() string { | func (*semverLibType) LibraryName() string { | ||||||
| 	return "kubernetes.Semver" | 	return "kubernetes.Semver" | ||||||
| @@ -100,8 +131,8 @@ func (*semverLibType) Types() []*cel.Type { | |||||||
| 	return []*cel.Type{apiservercel.SemverType} | 	return []*cel.Type{apiservercel.SemverType} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (*semverLibType) declarations() map[string][]cel.FunctionOpt { | func (lib *semverLibType) declarations() map[string][]cel.FunctionOpt { | ||||||
| 	return map[string][]cel.FunctionOpt{ | 	fnOpts := map[string][]cel.FunctionOpt{ | ||||||
| 		"semver": { | 		"semver": { | ||||||
| 			cel.Overload("string_to_semver", []*cel.Type{cel.StringType}, apiservercel.SemverType, cel.UnaryBinding((stringToSemver))), | 			cel.Overload("string_to_semver", []*cel.Type{cel.StringType}, apiservercel.SemverType, cel.UnaryBinding((stringToSemver))), | ||||||
| 		}, | 		}, | ||||||
| @@ -127,6 +158,11 @@ func (*semverLibType) declarations() map[string][]cel.FunctionOpt { | |||||||
| 			cel.MemberOverload("semver_patch", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverPatch)), | 			cel.MemberOverload("semver_patch", []*cel.Type{apiservercel.SemverType}, cel.IntType, cel.UnaryBinding(semverPatch)), | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  | 	if lib.version >= 1 { | ||||||
|  | 		fnOpts["semver"] = append(fnOpts["semver"], cel.Overload("string_bool_to_semver", []*cel.Type{cel.StringType, cel.BoolType}, apiservercel.SemverType, cel.BinaryBinding((stringToSemverNormalize)))) | ||||||
|  | 		fnOpts["isSemver"] = append(fnOpts["isSemver"], cel.Overload("is_semver_string_bool", []*cel.Type{cel.StringType, cel.BoolType}, cel.BoolType, cel.BinaryBinding(isSemverNormalize))) | ||||||
|  | 	} | ||||||
|  | 	return fnOpts | ||||||
| } | } | ||||||
|  |  | ||||||
| func (s *semverLibType) CompileOptions() []cel.EnvOption { | func (s *semverLibType) CompileOptions() []cel.EnvOption { | ||||||
| @@ -144,16 +180,29 @@ func (*semverLibType) ProgramOptions() []cel.ProgramOption { | |||||||
| } | } | ||||||
|  |  | ||||||
| func isSemver(arg ref.Val) ref.Val { | func isSemver(arg ref.Val) ref.Val { | ||||||
|  | 	return isSemverNormalize(arg, types.Bool(false)) | ||||||
|  | } | ||||||
|  | func isSemverNormalize(arg ref.Val, normalizeArg ref.Val) ref.Val { | ||||||
| 	str, ok := arg.Value().(string) | 	str, ok := arg.Value().(string) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return types.MaybeNoSuchOverloadErr(arg) | 		return types.MaybeNoSuchOverloadErr(arg) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	normalize, ok := normalizeArg.Value().(bool) | ||||||
|  | 	if !ok { | ||||||
|  | 		return types.MaybeNoSuchOverloadErr(arg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Using semver/v4 here is okay because this function isn't | 	// Using semver/v4 here is okay because this function isn't | ||||||
| 	// used to validate the Kubernetes API. In the CEL base library | 	// used to validate the Kubernetes API. In the CEL base library | ||||||
| 	// we would have to use the regular expression from | 	// we would have to use the regular expression from | ||||||
| 	// pkg/apis/resource/structured/namedresources/validation/validation.go. | 	// pkg/apis/resource/structured/namedresources/validation/validation.go. | ||||||
| 	_, err := semver.Parse(str) | 	var err error | ||||||
|  | 	if normalize { | ||||||
|  | 		_, err = normalizeAndParse(str) | ||||||
|  | 	} else { | ||||||
|  | 		_, err = semver.Parse(str) | ||||||
|  | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return types.Bool(false) | 		return types.Bool(false) | ||||||
| 	} | 	} | ||||||
| @@ -162,17 +211,31 @@ func isSemver(arg ref.Val) ref.Val { | |||||||
| } | } | ||||||
|  |  | ||||||
| func stringToSemver(arg ref.Val) ref.Val { | func stringToSemver(arg ref.Val) ref.Val { | ||||||
|  | 	return stringToSemverNormalize(arg, types.Bool(false)) | ||||||
|  | } | ||||||
|  | func stringToSemverNormalize(arg ref.Val, normalizeArg ref.Val) ref.Val { | ||||||
| 	str, ok := arg.Value().(string) | 	str, ok := arg.Value().(string) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return types.MaybeNoSuchOverloadErr(arg) | 		return types.MaybeNoSuchOverloadErr(arg) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	normalize, ok := normalizeArg.Value().(bool) | ||||||
|  | 	if !ok { | ||||||
|  | 		return types.MaybeNoSuchOverloadErr(arg) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Using semver/v4 here is okay because this function isn't | 	// Using semver/v4 here is okay because this function isn't | ||||||
| 	// used to validate the Kubernetes API. In the CEL base library | 	// used to validate the Kubernetes API. In the CEL base library | ||||||
| 	// we would have to use the regular expression from | 	// we would have to use the regular expression from | ||||||
| 	// pkg/apis/resource/structured/namedresources/validation/validation.go | 	// pkg/apis/resource/structured/namedresources/validation/validation.go | ||||||
| 	// first before parsing. | 	// first before parsing. | ||||||
| 	v, err := semver.Parse(str) | 	var err error | ||||||
|  | 	var v semver.Version | ||||||
|  | 	if normalize { | ||||||
|  | 		v, err = normalizeAndParse(str) | ||||||
|  | 	} else { | ||||||
|  | 		v, err = semver.Parse(str) | ||||||
|  | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return types.WrapErr(err) | 		return types.WrapErr(err) | ||||||
| 	} | 	} | ||||||
| @@ -245,3 +308,37 @@ func semverCompareTo(arg ref.Val, other ref.Val) ref.Val { | |||||||
|  |  | ||||||
| 	return types.Int(v.Compare(v2)) | 	return types.Int(v.Compare(v2)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // normalizeAndParse removes any "v" prefix,  adds a 0 minor and patch numbers to versions with | ||||||
|  | // only major or major.minor components specified, and removes any leading 0s. | ||||||
|  | // normalizeAndParse is based on semver.ParseTolerant but does not trim extra whitespace and is | ||||||
|  | // guaranteed to not change behavior in the future. | ||||||
|  | func normalizeAndParse(s string) (semver.Version, error) { | ||||||
|  | 	s = strings.TrimPrefix(s, "v") | ||||||
|  |  | ||||||
|  | 	// Split into major.minor.(patch+pr+meta) | ||||||
|  | 	parts := strings.SplitN(s, ".", 3) | ||||||
|  | 	// Remove leading zeros. | ||||||
|  | 	for i, p := range parts { | ||||||
|  | 		if len(p) > 1 { | ||||||
|  | 			p = strings.TrimLeft(p, "0") | ||||||
|  | 			if len(p) == 0 || !strings.ContainsAny(p[0:1], "0123456789") { | ||||||
|  | 				p = "0" + p | ||||||
|  | 			} | ||||||
|  | 			parts[i] = p | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Fill up shortened versions. | ||||||
|  | 	if len(parts) < 3 { | ||||||
|  | 		if strings.ContainsAny(parts[len(parts)-1], "+-") { | ||||||
|  | 			return semver.Version{}, errors.New("short version cannot contain PreRelease/Build meta data") | ||||||
|  | 		} | ||||||
|  | 		for len(parts) < 3 { | ||||||
|  | 			parts = append(parts, "0") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	s = strings.Join(parts, ".") | ||||||
|  |  | ||||||
|  | 	return semver.Parse(s) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -33,13 +33,14 @@ import ( | |||||||
| 	"github.com/google/cel-go/common/types/traits" | 	"github.com/google/cel-go/common/types/traits" | ||||||
| 	"github.com/google/cel-go/ext" | 	"github.com/google/cel-go/ext" | ||||||
|  |  | ||||||
|  | 	"k8s.io/utils/ptr" | ||||||
|  |  | ||||||
| 	resourceapi "k8s.io/api/resource/v1beta1" | 	resourceapi "k8s.io/api/resource/v1beta1" | ||||||
| 	"k8s.io/apimachinery/pkg/util/version" | 	"k8s.io/apimachinery/pkg/util/version" | ||||||
| 	celconfig "k8s.io/apiserver/pkg/apis/cel" | 	celconfig "k8s.io/apiserver/pkg/apis/cel" | ||||||
| 	apiservercel "k8s.io/apiserver/pkg/cel" | 	apiservercel "k8s.io/apiserver/pkg/cel" | ||||||
| 	"k8s.io/apiserver/pkg/cel/environment" | 	"k8s.io/apiserver/pkg/cel/environment" | ||||||
| 	"k8s.io/apiserver/pkg/cel/library" | 	"k8s.io/apiserver/pkg/cel/library" | ||||||
| 	"k8s.io/utils/ptr" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -297,8 +298,6 @@ func newCompiler() *compiler { | |||||||
| 			EnvOptions: []cel.EnvOption{ | 			EnvOptions: []cel.EnvOption{ | ||||||
| 				cel.Variable(deviceVar, deviceType.CelType()), | 				cel.Variable(deviceVar, deviceType.CelType()), | ||||||
|  |  | ||||||
| 				environment.UnversionedLib(library.SemverLib), |  | ||||||
|  |  | ||||||
| 				// https://pkg.go.dev/github.com/google/cel-go/ext#Bindings | 				// https://pkg.go.dev/github.com/google/cel-go/ext#Bindings | ||||||
| 				// | 				// | ||||||
| 				// This is useful to simplify attribute lookups because the | 				// This is useful to simplify attribute lookups because the | ||||||
| @@ -311,6 +310,22 @@ func newCompiler() *compiler { | |||||||
| 				deviceType, | 				deviceType, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			IntroducedVersion: version.MajorMinor(1, 31), | ||||||
|  | 			// This library has added to base environment of Kubernetes | ||||||
|  | 			// in 1.33 at version 1. It will continue to be available for | ||||||
|  | 			// use in this environment, but does not need to be included | ||||||
|  | 			// directly since it becomes available indirectly via the base | ||||||
|  | 			// environment shared across Kubernetes. | ||||||
|  | 			// In Kubernetes 1.34, version 1 feature of this library will | ||||||
|  | 			// become available, and will be rollback safe to 1.33. | ||||||
|  | 			// TODO: In Kubernetes 1.34: Add compile tests that demonstrate that | ||||||
|  | 			// `isSemver("v1.0.0", true)` and `semver("v1.0.0", true)` are supported. | ||||||
|  | 			RemovedVersion: version.MajorMinor(1, 33), | ||||||
|  | 			EnvOptions: []cel.EnvOption{ | ||||||
|  | 				library.SemverLib(library.SemverVersion(0)), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 	envset, err := envset.Extend(versioned...) | 	envset, err := envset.Extend(versioned...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot