From d2affe304847aa0bef3f81fa622d0b9c70a7f975 Mon Sep 17 00:00:00 2001 From: Joe Betz Date: Thu, 25 Jul 2024 16:33:18 -0400 Subject: [PATCH] add a type for each CEL library, register all types --- .../pkg/cel/environment/base_test.go | 25 ++++++++++ .../k8s.io/apiserver/pkg/cel/library/authz.go | 21 +++++++++ .../k8s.io/apiserver/pkg/cel/library/cidr.go | 8 ++++ .../apiserver/pkg/cel/library/format.go | 9 ++++ .../k8s.io/apiserver/pkg/cel/library/ip.go | 8 ++++ .../apiserver/pkg/cel/library/libraries.go | 46 +++++++++++++++++++ .../cel/library/library_compatibility_test.go | 41 ++++++++++++++--- .../k8s.io/apiserver/pkg/cel/library/lists.go | 8 ++++ .../apiserver/pkg/cel/library/quantity.go | 8 ++++ .../k8s.io/apiserver/pkg/cel/library/regex.go | 8 ++++ .../k8s.io/apiserver/pkg/cel/library/urls.go | 8 ++++ 11 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go diff --git a/staging/src/k8s.io/apiserver/pkg/cel/environment/base_test.go b/staging/src/k8s.io/apiserver/pkg/cel/environment/base_test.go index a21891115cf..d893f650c96 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/environment/base_test.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/environment/base_test.go @@ -18,11 +18,14 @@ package environment import ( "sort" + "strings" "testing" "github.com/google/cel-go/cel" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/version" + "k8s.io/apiserver/pkg/cel/library" ) // BenchmarkLoadBaseEnv is expected to be very fast, because a @@ -112,6 +115,28 @@ func TestLibraryCoverage(t *testing.T) { } } +func TestKnownLibraries(t *testing.T) { + known := sets.New[string]() + used := sets.New[string]() + + for _, lib := range library.KnownLibraries() { + known.Insert(lib.LibraryName()) + } + for _, libName := range MustBaseEnvSet(version.MajorMinor(1, 0), true).storedExpressions.Libraries() { + if strings.HasPrefix(libName, "cel.lib") { // ignore core libs + continue + } + used.Insert(libName) + } + + unexpected := used.Difference(known) + + if len(unexpected) != 0 { + t.Errorf("Expected all libraries in the base environment to be included k8s.io/apiserver/pkg/cel/library's KnownLibraries, but found missing libraries: %v", unexpected) + } + +} + func librariesInVersions(t *testing.T, vops ...VersionedOptions) []string { env, err := cel.NewCustomEnv() if err != nil { diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go b/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go index 1fd489fc916..0acd5a542a1 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/authz.go @@ -235,6 +235,19 @@ func (*authz) LibraryName() string { return "k8s.authz" } +func (*authz) Types() []*cel.Type { + return []*cel.Type{ + AuthorizerType, + PathCheckType, + GroupCheckType, + ResourceCheckType, + DecisionType} +} + +func (*authz) declarations() map[string][]cel.FunctionOpt { + return authzLibraryDecls +} + var authzLibraryDecls = map[string][]cel.FunctionOpt{ "path": { cel.MemberOverload("authorizer_path", []*cel.Type{AuthorizerType, cel.StringType}, PathCheckType, @@ -327,6 +340,14 @@ func (*authzSelectors) LibraryName() string { return "k8s.authzSelectors" } +func (*authzSelectors) Types() []*cel.Type { + return []*cel.Type{ResourceCheckType} +} + +func (*authzSelectors) declarations() map[string][]cel.FunctionOpt { + return authzSelectorsLibraryDecls +} + var authzSelectorsLibraryDecls = map[string][]cel.FunctionOpt{ "fieldSelector": { cel.MemberOverload("authorizer_fieldselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType, diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/cidr.go b/staging/src/k8s.io/apiserver/pkg/cel/library/cidr.go index c4259daed97..8686e6c17c0 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/cidr.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/cidr.go @@ -112,6 +112,14 @@ func (*cidrs) LibraryName() string { return "net.cidr" } +func (*cidrs) declarations() map[string][]cel.FunctionOpt { + return cidrLibraryDecls +} + +func (*cidrs) Types() []*cel.Type { + return []*cel.Type{apiservercel.CIDRType, apiservercel.IPType} +} + var cidrLibraryDecls = map[string][]cel.FunctionOpt{ "cidr": { cel.Overload("string_to_cidr", []*cel.Type{cel.StringType}, apiservercel.CIDRType, diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/format.go b/staging/src/k8s.io/apiserver/pkg/cel/library/format.go index c051f33c006..f76c6a29d56 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/format.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/format.go @@ -25,6 +25,7 @@ import ( "github.com/google/cel-go/common/decls" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" + apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" "k8s.io/apimachinery/pkg/util/validation" apiservercel "k8s.io/apiserver/pkg/cel" @@ -93,6 +94,14 @@ func (*format) LibraryName() string { return "format" } +func (*format) Types() []*cel.Type { + return []*cel.Type{apiservercel.FormatType} +} + +func (*format) declarations() map[string][]cel.FunctionOpt { + return formatLibraryDecls +} + func ZeroArgumentFunctionBinding(binding func() ref.Val) decls.OverloadOpt { return func(o *decls.OverloadDecl) (*decls.OverloadDecl, error) { wrapped, err := decls.FunctionBinding(func(values ...ref.Val) ref.Val { return binding() })(o) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/ip.go b/staging/src/k8s.io/apiserver/pkg/cel/library/ip.go index cdfeb1daf2b..b957afe769b 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/ip.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/ip.go @@ -135,6 +135,14 @@ func (*ip) LibraryName() string { return "net.ip" } +func (*ip) declarations() map[string][]cel.FunctionOpt { + return ipLibraryDecls +} + +func (*ip) Types() []*cel.Type { + return []*cel.Type{apiservercel.IPType} +} + var ipLibraryDecls = map[string][]cel.FunctionOpt{ "ip": { cel.Overload("string_to_ip", []*cel.Type{cel.StringType}, apiservercel.IPType, diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go b/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go new file mode 100644 index 00000000000..75a9b0e998f --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/libraries.go @@ -0,0 +1,46 @@ +/* +Copyright 2023 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" + +// Library represents a CEL library used by kubernetes. +type Library interface { + // SingletonLibrary provides the library name and ensures the library can be safely registered into environments. + cel.SingletonLibrary + + // Types provides all custom types introduced by the library. + Types() []*cel.Type + + // declarations returns all function declarations provided by the library. + declarations() map[string][]cel.FunctionOpt +} + +// KnownLibraries returns all libraries used in Kubernetes. +func KnownLibraries() []Library { + return []Library{ + authzLib, + authzSelectorsLib, + listsLib, + regexLib, + urlsLib, + quantityLib, + ipLib, + cidrsLib, + formatLib, + } +} 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 81691d556f0..cedbcb04f68 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 @@ -17,19 +17,18 @@ limitations under the License. package library import ( - "testing" - "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/decls" + "github.com/google/cel-go/common/types" + "testing" "k8s.io/apimachinery/pkg/util/sets" ) func TestLibraryCompatibility(t *testing.T) { - var libs []map[string][]cel.FunctionOpt - libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls, quantityLibraryDecls, ipLibraryDecls, cidrLibraryDecls, formatLibraryDecls, authzSelectorsLibraryDecls) functionNames := sets.New[string]() - for _, lib := range libs { - for name := range lib { + for _, lib := range KnownLibraries() { + for name := range lib.declarations() { functionNames[name] = struct{}{} } } @@ -66,3 +65,33 @@ func TestLibraryCompatibility(t *testing.T) { t.Errorf("Expected all functions in the libraries to be assigned to a kubernetes release, but found the missing function names: %v", missing) } } + +func TestTypeRegistration(t *testing.T) { + for _, lib := range KnownLibraries() { + registeredTypes := sets.New[*cel.Type]() + usedTypes := sets.New[*cel.Type]() + // scan all registered functions + for _, fn := range lib.declarations() { + testFn, err := decls.NewFunction("test", fn...) + if err != nil { + t.Fatal(err) + } + for _, o := range testFn.OverloadDecls() { + for _, at := range o.ArgTypes() { + switch at.Kind() { + case types.OpaqueKind, types.StructKind: + usedTypes.Insert(at) + } + } + } + } + for _, t := range lib.Types() { + registeredTypes.Insert(t) + } + unregistered := usedTypes.Difference(registeredTypes) + if len(unregistered) != 0 { + t.Errorf("Expected types to be registered with the %s library Type() functions, but they were not: %v", lib.LibraryName(), unregistered) + } + } + +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/lists.go b/staging/src/k8s.io/apiserver/pkg/cel/library/lists.go index 327ec93d6e2..d56e72761d8 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/lists.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/lists.go @@ -99,6 +99,14 @@ func (*lists) LibraryName() string { return "k8s.lists" } +func (*lists) Types() []*cel.Type { + return []*cel.Type{} +} + +func (*lists) declarations() map[string][]cel.FunctionOpt { + return listsLibraryDecls +} + var paramA = cel.TypeParamType("A") // CEL typeParams can be used to constraint to a specific trait (e.g. traits.ComparableType) if the 1st operand is the type to constrain. diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go b/staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go index b4ac91c8a72..4d9f1ac3eab 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/quantity.go @@ -146,6 +146,14 @@ func (*quantity) LibraryName() string { return "k8s.quantity" } +func (*quantity) Types() []*cel.Type { + return []*cel.Type{apiservercel.QuantityType} +} + +func (*quantity) declarations() map[string][]cel.FunctionOpt { + return quantityLibraryDecls +} + var quantityLibraryDecls = map[string][]cel.FunctionOpt{ "quantity": { cel.Overload("string_to_quantity", []*cel.Type{cel.StringType}, apiservercel.QuantityType, cel.UnaryBinding((stringToQuantity))), diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/regex.go b/staging/src/k8s.io/apiserver/pkg/cel/library/regex.go index 147a40f9bd2..e8577a18517 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/regex.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/regex.go @@ -55,6 +55,14 @@ func (*regex) LibraryName() string { return "k8s.regex" } +func (*regex) Types() []*cel.Type { + return []*cel.Type{} +} + +func (*regex) declarations() map[string][]cel.FunctionOpt { + return regexLibraryDecls +} + var regexLibraryDecls = map[string][]cel.FunctionOpt{ "find": { cel.MemberOverload("string_find_string", []*cel.Type{cel.StringType, cel.StringType}, cel.StringType, diff --git a/staging/src/k8s.io/apiserver/pkg/cel/library/urls.go b/staging/src/k8s.io/apiserver/pkg/cel/library/urls.go index 8f4ba85af7c..058706d8e07 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/library/urls.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/library/urls.go @@ -116,6 +116,14 @@ func (*urls) LibraryName() string { return "k8s.urls" } +func (*urls) Types() []*cel.Type { + return []*cel.Type{apiservercel.URLType} +} + +func (*urls) declarations() map[string][]cel.FunctionOpt { + return urlLibraryDecls +} + var urlLibraryDecls = map[string][]cel.FunctionOpt{ "url": { cel.Overload("string_to_url", []*cel.Type{cel.StringType}, apiservercel.URLType,