diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/cost.go b/staging/src/k8s.io/apiserver/pkg/cel/library/cost.go index 14b74dc6b02..500fd8b0986 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/cost.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/cost.go @@ -97,7 +97,7 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re cost += traversalCost(args[0]) // these O(n) operations all cost roughly the cost of a single traversal } return &cost - case "url", "lowerAscii", "upperAscii", "substring", "trim": + case "url", "lowerAscii", "upperAscii", "substring", "trim", "jsonpatch.escapeKey": if len(args) >= 1 { cost := uint64(math.Ceil(float64(actualSize(args[0])) * common.StringTraversalCostFactor)) return &cost @@ -294,7 +294,7 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch return &checker.CallEstimate{CostEstimate: l.sizeEstimate(*target).MultiplyByCostFactor(common.StringTraversalCostFactor)} } } - case "url": + case "url", "jsonpatch.escapeKey": if len(args) == 1 { sz := l.sizeEstimate(args[0]) return &checker.CallEstimate{CostEstimate: sz.MultiplyByCostFactor(common.StringTraversalCostFactor), ResultSize: &sz} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go index 4da1934f66d..a818c795570 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/cost_test.go @@ -629,6 +629,12 @@ func TestStringLibrary(t *testing.T) { expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, expectRuntimeCost: 2, }, + { + name: "jsonpatch.escapeKey", + expr: "jsonpatch.escapeKey('abc/def~ abc/def~')", + expectEsimatedCost: checker.CostEstimate{Min: 2, Max: 2}, + expectRuntimeCost: 2, + }, } for _, tc := range cases { @@ -1122,6 +1128,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate IP(), CIDR(), Format(), + JSONPatch(), cel.OptionalTypes(), // cel-go v0.17.7 introduced CostEstimatorOptions. // Previous the presence has a cost of 0 but cel fixed it to 1. We still set to 0 here to avoid breaking changes. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go b/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go new file mode 100644 index 00000000000..bdcb6d852b0 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/jsonpatch.go @@ -0,0 +1,89 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package library + +import ( + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "strings" +) + +// JSONPatch provides a CEL function library extension of JSONPatch functions. +// +// jsonpatch.escapeKey +// +// Escapes a string for use as a JSONPatch path key. +// +// jsonpatch.escapeKey() +// +// Examples: +// +// "/metadata/labels/" + jsonpatch.escapeKey('k8s.io/my~label') // returns "/metadata/labels/k8s.io~1my~0label" +func JSONPatch() cel.EnvOption { + return cel.Lib(jsonPatchLib) +} + +var jsonPatchLib = &jsonPatch{} + +type jsonPatch struct{} + +func (*jsonPatch) LibraryName() string { + return "kubernetes.jsonpatch" +} + +func (*jsonPatch) declarations() map[string][]cel.FunctionOpt { + return jsonPatchLibraryDecls +} + +func (*jsonPatch) Types() []*cel.Type { + return []*cel.Type{} +} + +var jsonPatchLibraryDecls = map[string][]cel.FunctionOpt{ + "jsonpatch.escapeKey": { + cel.Overload("string_jsonpatch_escapeKey_string", []*cel.Type{cel.StringType}, cel.StringType, + cel.UnaryBinding(escape)), + }, +} + +func (*jsonPatch) CompileOptions() []cel.EnvOption { + var options []cel.EnvOption + for name, overloads := range jsonPatchLibraryDecls { + options = append(options, cel.Function(name, overloads...)) + } + return options +} + +func (*jsonPatch) ProgramOptions() []cel.ProgramOption { + return []cel.ProgramOption{} +} + +var jsonPatchReplacer = strings.NewReplacer("/", "~1", "~", "~0") + +func escapeKey(k string) string { + return jsonPatchReplacer.Replace(k) +} + +func escape(arg ref.Val) ref.Val { + s, ok := arg.Value().(string) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + escaped := escapeKey(s) + return types.String(escaped) +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go b/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go index e3689e3e0eb..dc436973e5a 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go @@ -45,6 +45,7 @@ func KnownLibraries() []Library { cidrsLib, formatLib, semverLib, + jsonPatchLib, } } diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go b/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go index 50b5d22882e..fc56651061f 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/library_compatibility_test.go @@ -54,6 +54,8 @@ func TestLibraryCompatibility(t *testing.T) { "ip", "family", "isUnspecified", "isLoopback", "isLinkLocalMulticast", "isLinkLocalUnicast", "isGlobalUnicast", "ip.isCanonical", "isIP", "cidr", "containsIP", "containsCIDR", "masked", "prefixLength", "isCIDR", "string", // Kubernetes <1.31>: "fieldSelector", "labelSelector", "validate", "format.named", "isSemver", "major", "minor", "patch", "semver", + // Kubernetes <1.32>: + "jsonpatch.escapeKey", // Kubernetes <1.??>: )