Merge pull request #113314 from cici37/celIntegration

CEL validation in Admission chain

Kubernetes-commit: 595ea324113580ae61f4a15ab3e5b22303a195cf
This commit is contained in:
Kubernetes Publisher 2022-11-07 17:08:33 -08:00
commit aa0e1e5e62
26 changed files with 3049 additions and 1026 deletions

8
go.mod
View File

@ -42,9 +42,9 @@ require (
google.golang.org/protobuf v1.28.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/square/go-jose.v2 v2.2.2
k8s.io/api v0.0.0-20221108053747-3f61c95cab71
k8s.io/api v0.0.0-20221108053748-98c1aa6b3d0a
k8s.io/apimachinery v0.0.0-20221108052757-4fe4321a9d5e
k8s.io/client-go v0.0.0-20221108054908-3daf180aa6b1
k8s.io/client-go v0.0.0-20221108054913-4b1a9fdfb58c
k8s.io/component-base v0.0.0-20221108061007-abdc0eb56a1d
k8s.io/klog/v2 v2.80.1
k8s.io/kms v0.0.0-20221028080743-a9ba1c11c0c6
@ -122,9 +122,9 @@ require (
)
replace (
k8s.io/api => k8s.io/api v0.0.0-20221108053747-3f61c95cab71
k8s.io/api => k8s.io/api v0.0.0-20221108053748-98c1aa6b3d0a
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20221108052757-4fe4321a9d5e
k8s.io/client-go => k8s.io/client-go v0.0.0-20221108054908-3daf180aa6b1
k8s.io/client-go => k8s.io/client-go v0.0.0-20221108054913-4b1a9fdfb58c
k8s.io/component-base => k8s.io/component-base v0.0.0-20221108061007-abdc0eb56a1d
k8s.io/kms => k8s.io/kms v0.0.0-20221028080743-a9ba1c11c0c6
)

8
go.sum
View File

@ -985,12 +985,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.0.0-20221108053747-3f61c95cab71 h1:pyiHUA+hk/Uld88AATDzP+rOHuHoUvQVDosQmOGGT/w=
k8s.io/api v0.0.0-20221108053747-3f61c95cab71/go.mod h1:PSXY9/fSNyKgKHUU+O9scnZiW8m+V1znqk49oI6hAEY=
k8s.io/api v0.0.0-20221108053748-98c1aa6b3d0a h1:GaCla9HtNyi63kysI/cyeA4bv6wRkIyuiUeXpaTF+dw=
k8s.io/api v0.0.0-20221108053748-98c1aa6b3d0a/go.mod h1:PSXY9/fSNyKgKHUU+O9scnZiW8m+V1znqk49oI6hAEY=
k8s.io/apimachinery v0.0.0-20221108052757-4fe4321a9d5e h1:zX/AC2CNrYwngyVHVHcsCL36uUtC/3tiZOE6/gfojvc=
k8s.io/apimachinery v0.0.0-20221108052757-4fe4321a9d5e/go.mod h1:VXMmlsE7YRJ5vyAyWpkKIfFkEbDNpVs0ObpkuQf1WfM=
k8s.io/client-go v0.0.0-20221108054908-3daf180aa6b1 h1:0kFOheClgHsssAuquQ5SUM5vDTS8tSaSjv+9J1UXTnI=
k8s.io/client-go v0.0.0-20221108054908-3daf180aa6b1/go.mod h1:nLJ8OQ/Q/icQcfjnVrKZyCgc/CPXX7o8Hlqh70Oo6Jk=
k8s.io/client-go v0.0.0-20221108054913-4b1a9fdfb58c h1:73hBKeNTruSueGRemXkcxxOJ2OV58wJvfdo0+ZMkucA=
k8s.io/client-go v0.0.0-20221108054913-4b1a9fdfb58c/go.mod h1:Hnc/VjMc6mMK4isnk+uuot3Ymjl7evdMdssMoHIDeSc=
k8s.io/component-base v0.0.0-20221108061007-abdc0eb56a1d h1:IF51IULm49B+VT4PJOBd4z/SxtU0WVfInKrnykCG/t0=
k8s.io/component-base v0.0.0-20221108061007-abdc0eb56a1d/go.mod h1:yN3pjWt18ANoCwZlZZaB+9OdFe2a6rgTUE51kThAe3Q=
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=

View File

@ -19,6 +19,7 @@ package initializer
import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/component-base/featuregate"
@ -26,6 +27,7 @@ import (
type pluginInitializer struct {
externalClient kubernetes.Interface
dynamicClient dynamic.Interface
externalInformers informers.SharedInformerFactory
authorizer authorizer.Authorizer
featureGates featuregate.FeatureGate
@ -37,6 +39,7 @@ type pluginInitializer struct {
// during compilation when they update a level.
func New(
extClientset kubernetes.Interface,
dynamicClient dynamic.Interface,
extInformers informers.SharedInformerFactory,
authz authorizer.Authorizer,
featureGates featuregate.FeatureGate,
@ -44,6 +47,7 @@ func New(
) pluginInitializer {
return pluginInitializer{
externalClient: extClientset,
dynamicClient: dynamicClient,
externalInformers: extInformers,
authorizer: authz,
featureGates: featureGates,
@ -68,6 +72,10 @@ func (i pluginInitializer) Initialize(plugin admission.Interface) {
wants.SetExternalKubeClientSet(i.externalClient)
}
if wants, ok := plugin.(WantsDynamicClient); ok {
wants.SetDynamicClient(i.dynamicClient)
}
if wants, ok := plugin.(WantsExternalKubeInformerFactory); ok {
wants.SetExternalKubeInformerFactory(i.externalInformers)
}

View File

@ -32,7 +32,7 @@ import (
// TestWantsAuthorizer ensures that the authorizer is injected
// when the WantsAuthorizer interface is implemented by a plugin.
func TestWantsAuthorizer(t *testing.T) {
target := initializer.New(nil, nil, &TestAuthorizer{}, nil, nil)
target := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, nil)
wantAuthorizerAdmission := &WantAuthorizerAdmission{}
target.Initialize(wantAuthorizerAdmission)
if wantAuthorizerAdmission.auth == nil {
@ -44,7 +44,7 @@ func TestWantsAuthorizer(t *testing.T) {
// when the WantsExternalKubeClientSet interface is implemented by a plugin.
func TestWantsExternalKubeClientSet(t *testing.T) {
cs := &fake.Clientset{}
target := initializer.New(cs, nil, &TestAuthorizer{}, nil, nil)
target := initializer.New(cs, nil, nil, &TestAuthorizer{}, nil, nil)
wantExternalKubeClientSet := &WantExternalKubeClientSet{}
target.Initialize(wantExternalKubeClientSet)
if wantExternalKubeClientSet.cs != cs {
@ -57,7 +57,7 @@ func TestWantsExternalKubeClientSet(t *testing.T) {
func TestWantsExternalKubeInformerFactory(t *testing.T) {
cs := &fake.Clientset{}
sf := informers.NewSharedInformerFactory(cs, time.Duration(1)*time.Second)
target := initializer.New(cs, sf, &TestAuthorizer{}, nil, nil)
target := initializer.New(cs, nil, sf, &TestAuthorizer{}, nil, nil)
wantExternalKubeInformerFactory := &WantExternalKubeInformerFactory{}
target.Initialize(wantExternalKubeInformerFactory)
if wantExternalKubeInformerFactory.sf != sf {
@ -69,7 +69,7 @@ func TestWantsExternalKubeInformerFactory(t *testing.T) {
// when the WantsShutdownSignal interface is implemented by a plugin.
func TestWantsShutdownNotification(t *testing.T) {
stopCh := make(chan struct{})
target := initializer.New(nil, nil, &TestAuthorizer{}, nil, stopCh)
target := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, stopCh)
wantDrainedNotification := &WantDrainedNotification{}
target.Initialize(wantDrainedNotification)
if wantDrainedNotification.stopCh == nil {

View File

@ -17,9 +17,11 @@ limitations under the License.
package initializer
import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
quota "k8s.io/apiserver/pkg/quota/v1"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/component-base/featuregate"
@ -68,3 +70,14 @@ type WantsFeatures interface {
InspectFeatureGates(featuregate.FeatureGate)
admission.InitializationValidator
}
type WantsDynamicClient interface {
SetDynamicClient(dynamic.Interface)
admission.InitializationValidator
}
// WantsRESTMapper defines a function which sets RESTMapper for admission plugins that need it.
type WantsRESTMapper interface {
SetRESTMapper(meta.RESTMapper)
admission.InitializationValidator
}

View File

@ -21,9 +21,16 @@ import (
"errors"
"fmt"
"io"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apiserver/pkg/features"
"k8s.io/client-go/dynamic"
"k8s.io/component-base/featuregate"
"time"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
)
@ -38,7 +45,7 @@ import (
const (
// PluginName indicates the name of admission plug-in
PluginName = "CEL"
PluginName = "ValidatingAdmissionPolicy"
)
// Register registers a plugin
@ -54,28 +61,89 @@ func Register(plugins *admission.Plugins) {
type celAdmissionPlugin struct {
evaluator CELPolicyEvaluator
inspectedFeatureGates bool
enabled bool
// Injected Dependencies
informerFactory informers.SharedInformerFactory
client kubernetes.Interface
restMapper meta.RESTMapper
dynamicClient dynamic.Interface
stopCh <-chan struct{}
}
var _ WantsCELPolicyEvaluator = &celAdmissionPlugin{}
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{}
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
var _ admission.InitializationValidator = &celAdmissionPlugin{}
var _ admission.ValidationInterface = &celAdmissionPlugin{}
func NewPlugin() (*celAdmissionPlugin, error) {
func NewPlugin() (admission.Interface, error) {
result := &celAdmissionPlugin{}
return result, nil
}
func (c *celAdmissionPlugin) SetCELPolicyEvaluator(evaluator CELPolicyEvaluator) {
c.evaluator = evaluator
func (c *celAdmissionPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
c.informerFactory = f
}
// Once clientset and informer factory are provided, creates and starts the
// admission controller
func (c *celAdmissionPlugin) SetExternalKubeClientSet(client kubernetes.Interface) {
c.client = client
}
func (c *celAdmissionPlugin) SetRESTMapper(mapper meta.RESTMapper) {
c.restMapper = mapper
}
func (c *celAdmissionPlugin) SetDynamicClient(client dynamic.Interface) {
c.dynamicClient = client
}
func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
c.stopCh = stopCh
}
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
if featureGates.Enabled(features.CELValidatingAdmission) {
c.enabled = true
}
c.inspectedFeatureGates = true
}
// ValidateInitialization - once clientset and informer factory are provided, creates and starts the admission controller
func (c *celAdmissionPlugin) ValidateInitialization() error {
if c.evaluator != nil {
if !c.inspectedFeatureGates {
return fmt.Errorf("%s did not see feature gates", PluginName)
}
if !c.enabled {
return nil
}
if c.informerFactory == nil {
return errors.New("missing informer factory")
}
if c.client == nil {
return errors.New("missing kubernetes client")
}
if c.restMapper == nil {
return errors.New("missing rest mapper")
}
if c.dynamicClient == nil {
return errors.New("missing dynamic client")
}
if c.stopCh == nil {
return errors.New("missing stop channel")
}
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient)
if err := c.evaluator.ValidateInitialization(); err != nil {
return err
}
return errors.New("CELPolicyEvaluator not injected")
go c.evaluator.Run(c.stopCh)
return nil
}
////////////////////////////////////////////////////////////////////////////////
@ -91,13 +159,32 @@ func (c *celAdmissionPlugin) Validate(
a admission.Attributes,
o admission.ObjectInterfaces,
) (err error) {
if !c.enabled {
return nil
}
deadlined, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
// isPolicyResource determines if an admission.Attributes object is describing
// the admission of a ValidatingAdmissionPolicy or a ValidatingAdmissionPolicyBinding
if isPolicyResource(a) {
return
}
if !cache.WaitForNamedCacheSync("cel-admission-plugin", deadlined.Done(), c.evaluator.HasSynced) {
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
}
return c.evaluator.Validate(ctx, a, o)
}
func isPolicyResource(attr admission.Attributes) bool {
gvk := attr.GetResource()
if gvk.Group == "admissionregistration.k8s.io" {
if gvk.Resource == "validatingadmissionpolicies" || gvk.Resource == "validatingadmissionpolicybindings" {
return true
}
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,231 @@
/*
Copyright 2022 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 cel
import (
"sync"
"github.com/google/cel-go/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library"
)
const (
ObjectVarName = "object"
OldObjectVarName = "oldObject"
ParamsVarName = "params"
RequestVarName = "request"
checkFrequency = 100
)
type envs struct {
noParams *cel.Env
withParams *cel.Env
}
var (
initEnvsOnce sync.Once
initEnvs *envs
initEnvsErr error
)
func getEnvs() (*envs, error) {
initEnvsOnce.Do(func() {
base, err := buildBaseEnv()
if err != nil {
initEnvsErr = err
return
}
noParams, err := buildNoParamsEnv(base)
if err != nil {
initEnvsErr = err
return
}
withParams, err := buildWithParamsEnv(noParams)
if err != nil {
initEnvsErr = err
return
}
initEnvs = &envs{noParams: noParams, withParams: withParams}
})
return initEnvs, initEnvsErr
}
// This is a similar code as in k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go
// If any changes are made here, consider to make the same changes there as well.
func buildBaseEnv() (*cel.Env, error) {
var opts []cel.EnvOption
opts = append(opts, cel.HomogeneousAggregateLiterals())
// Validate function declarations once during base env initialization,
// so they don't need to be evaluated each time a CEL rule is compiled.
// This is a relatively expensive operation.
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
opts = append(opts, library.ExtensionLibs...)
return cel.NewEnv(opts...)
}
func buildNoParamsEnv(baseEnv *cel.Env) (*cel.Env, error) {
var propDecls []cel.EnvOption
reg := apiservercel.NewRegistry(baseEnv)
requestType := buildRequestType()
rt, err := apiservercel.NewRuleTypes(requestType.TypeName(), requestType, reg)
if err != nil {
return nil, err
}
if rt == nil {
return nil, nil
}
opts, err := rt.EnvOptions(baseEnv.TypeProvider())
if err != nil {
return nil, err
}
propDecls = append(propDecls, cel.Variable(ObjectVarName, cel.DynType))
propDecls = append(propDecls, cel.Variable(OldObjectVarName, cel.DynType))
propDecls = append(propDecls, cel.Variable(RequestVarName, requestType.CelType()))
opts = append(opts, propDecls...)
env, err := baseEnv.Extend(opts...)
if err != nil {
return nil, err
}
return env, nil
}
func buildWithParamsEnv(noParams *cel.Env) (*cel.Env, error) {
return noParams.Extend(cel.Variable(ParamsVarName, cel.DynType))
}
// buildRequestType generates a DeclType for AdmissionRequest. This may be replaced with a utility that
// converts the native type definition to apiservercel.DeclType once such a utility becomes available.
// The 'uid' field is omitted since it is not needed for in-process admission review.
// The 'object' and 'oldObject' fields are omitted since they are exposed as root level CEL variables.
func buildRequestType() *apiservercel.DeclType {
field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
return apiservercel.NewDeclField(name, declType, required, nil, nil)
}
fields := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField {
result := make(map[string]*apiservercel.DeclField, len(fields))
for _, f := range fields {
result[f.Name] = f
}
return result
}
gvkType := apiservercel.NewObjectType("kubernetes.GroupVersionKind", fields(
field("group", apiservercel.StringType, true),
field("version", apiservercel.StringType, true),
field("kind", apiservercel.StringType, true),
))
gvrType := apiservercel.NewObjectType("kubernetes.GroupVersionResource", fields(
field("group", apiservercel.StringType, true),
field("version", apiservercel.StringType, true),
field("resource", apiservercel.StringType, true),
))
userInfoType := apiservercel.NewObjectType("kubernetes.UserInfo", fields(
field("username", apiservercel.StringType, false),
field("uid", apiservercel.StringType, false),
field("groups", apiservercel.NewListType(apiservercel.StringType, -1), false),
field("extra", apiservercel.NewMapType(apiservercel.StringType, apiservercel.NewListType(apiservercel.StringType, -1), -1), false),
))
return apiservercel.NewObjectType("kubernetes.AdmissionRequest", fields(
field("kind", gvkType, true),
field("resource", gvrType, true),
field("subResource", apiservercel.StringType, false),
field("requestKind", gvkType, true),
field("requestResource", gvrType, true),
field("requestSubResource", apiservercel.StringType, false),
field("name", apiservercel.StringType, true),
field("namespace", apiservercel.StringType, false),
field("operation", apiservercel.StringType, true),
field("userInfo", userInfoType, true),
field("dryRun", apiservercel.BoolType, false),
field("options", apiservercel.DynType, false),
))
}
// CompilationResult represents a compiled ValidatingAdmissionPolicy validation expression.
type CompilationResult struct {
Program cel.Program
Error *apiservercel.Error
}
// CompileValidatingPolicyExpression returns a compiled vaalidating policy CEL expression.
func CompileValidatingPolicyExpression(validationExpression string, hasParams bool) CompilationResult {
var env *cel.Env
envs, err := getEnvs()
if err != nil {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInternal,
Detail: "compiler initialization failed: " + err.Error(),
},
}
}
if hasParams {
env = envs.withParams
} else {
env = envs.noParams
}
ast, issues := env.Compile(validationExpression)
if issues != nil {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: "compilation failed: " + issues.String(),
},
}
}
if ast.OutputType() != cel.BoolType {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: "cel expression must evaluate to a bool",
},
}
}
_, err = cel.AstToCheckedExpr(ast)
if err != nil {
// should be impossible since env.Compile returned no issues
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInternal,
Detail: "unexpected compilation error: " + err.Error(),
},
}
}
prog, err := env.Program(ast,
cel.EvalOptions(cel.OptOptimize),
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),
cel.InterruptCheckFrequency(checkFrequency),
)
if err != nil {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: "program instantiation failed: " + err.Error(),
},
}
}
return CompilationResult{
Program: prog,
}
}

View File

@ -0,0 +1,125 @@
/*
Copyright 2022 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 cel
import (
"strings"
"testing"
)
func TestCompileValidatingPolicyExpression(t *testing.T) {
cases := []struct {
name string
expressions []string
hasParams bool
errorExpressions map[string]string
}{
{
name: "invalid syntax",
errorExpressions: map[string]string{
"1 < 'asdf'": "found no matching overload for '_<_' applied to '(int, string)",
"'asdf'.contains('x'": "Syntax error: missing ')' at",
},
},
{
name: "with params",
expressions: []string{"object.foo < params.x"},
hasParams: true,
},
{
name: "without params",
errorExpressions: map[string]string{"object.foo < params.x": "undeclared reference to 'params'"},
hasParams: false,
},
{
name: "oldObject comparison",
expressions: []string{"object.foo == oldObject.foo"},
},
{
name: "object null checks",
// since object and oldObject are CEL variable, has() cannot be used (it works only on fields),
// so we always populate it to allow for a null check in the case of CREATE, where oldObject is
// null, and DELETE, where object is null.
expressions: []string{"object == null || oldObject == null || object.foo == oldObject.foo"},
},
{
name: "invalid root var",
errorExpressions: map[string]string{"object.foo < invalid.x": "undeclared reference to 'invalid'"},
hasParams: false,
},
{
name: "function library",
// sanity check that functions of the various libraries are available
expressions: []string{
"object.spec.string.matches('[0-9]+')", // strings extension lib
"object.spec.string.findAll('[0-9]+').size() > 0", // kubernetes string lib
"object.spec.list.isSorted()", // kubernetes list lib
"url(object.spec.endpoint).getHostname() in ['ok1', 'ok2']", // kubernetes url lib
},
},
{
name: "valid request",
expressions: []string{
"request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'",
"request.resource.group == 'example.com' && request.resource.version == 'v1' && request.resource.resource == 'fake' && request.subResource == 'scale'",
"request.requestKind.group == 'example.com' && request.requestKind.version == 'v1' && request.requestKind.kind == 'Fake'",
"request.requestResource.group == 'example.com' && request.requestResource.version == 'v1' && request.requestResource.resource == 'fake' && request.requestSubResource == 'scale'",
"request.name == 'fake-name'",
"request.namespace == 'fake-namespace'",
"request.operation == 'CREATE'",
"request.userInfo.username == 'admin'",
"request.userInfo.uid == '014fbff9a07c'",
"request.userInfo.groups == ['system:authenticated', 'my-admin-group']",
"request.userInfo.extra == {'some-key': ['some-value1', 'some-value2']}",
"request.dryRun == false",
"request.options == {'whatever': 'you want'}",
},
},
{
name: "invalid request",
errorExpressions: map[string]string{
"request.foo1 == 'nope'": "undefined field 'foo1'",
"request.resource.foo2 == 'nope'": "undefined field 'foo2'",
"request.requestKind.foo3 == 'nope'": "undefined field 'foo3'",
"request.requestResource.foo4 == 'nope'": "undefined field 'foo4'",
"request.userInfo.foo5 == 'nope'": "undefined field 'foo5'",
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
for _, expr := range tc.expressions {
result := CompileValidatingPolicyExpression(expr, tc.hasParams)
if result.Error != nil {
t.Errorf("Unexpected error: %v", result.Error)
}
}
for expr, expectErr := range tc.errorExpressions {
result := CompileValidatingPolicyExpression(expr, tc.hasParams)
if result.Error == nil {
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
continue
}
if !strings.Contains(result.Error.Error(), expectErr) {
t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
}
continue
}
})
}
}

View File

@ -21,28 +21,35 @@ import (
"errors"
"fmt"
"sync"
"sync/atomic"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission/plugin/cel/matching"
"k8s.io/api/admissionregistration/v1alpha1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel/internal/generic"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
)
var _ CELPolicyEvaluator = &celAdmissionController{}
// celAdmissionController is the top-level controller for admission control using CEL
// it is responsible for watching policy definitions, bindings, and config param CRDs
type celAdmissionController struct {
// Context under which the controller runs
runningContext context.Context
policyDefinitionsController generic.Controller[PolicyDefinition]
policyBindingController generic.Controller[PolicyBinding]
policyDefinitionsController generic.Controller[*v1alpha1.ValidatingAdmissionPolicy]
policyBindingController generic.Controller[*v1alpha1.ValidatingAdmissionPolicyBinding]
// dynamicclient used to create informers to watch the param crd types
dynamicClient dynamic.Interface
@ -50,7 +57,7 @@ type celAdmissionController struct {
// Provided to the policy's Compile function as an injected dependency to
// assist with compiling its expressions to CEL
objectConverter ObjectConverter
validatorCompiler ValidatorCompiler
// Lock which protects:
// - definitionInfo
@ -61,21 +68,26 @@ type celAdmissionController struct {
mutex sync.RWMutex
// controller and metadata
paramsCRDControllers map[schema.GroupVersionKind]*paramInfo
paramsCRDControllers map[v1alpha1.ParamKind]*paramInfo
// Index for each definition namespace/name, contains all binding
// namespace/names known to exist for that definition
definitionInfo map[string]*definitionInfo
definitionInfo map[namespacedName]*definitionInfo
// Index for each bindings namespace/name. Contains compiled templates
// for the binding depending on the policy/param combination.
bindingInfos map[string]*bindingInfo
bindingInfos map[namespacedName]*bindingInfo
// Map from namespace/name of a definition to a set of namespace/name
// of bindings which depend on it.
// All keys must have at least one dependent binding
// All binding names MUST exist as a key bindingInfos
definitionsToBindings map[string]sets.String
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
}
// namespaceName is used as a key in definitionInfo and bindingInfos
type namespacedName struct {
namespace, name string
}
type definitionInfo struct {
@ -86,16 +98,16 @@ type definitionInfo struct {
// Last value seen by this controller to be used in policy enforcement
// May not be nil
lastReconciledValue PolicyDefinition
lastReconciledValue *v1alpha1.ValidatingAdmissionPolicy
}
type bindingInfo struct {
// Compiled CEL expression turned into an evaluator
evaluator EvaluatorFunc
// Compiled CEL expression turned into an validator
validator atomic.Pointer[Validator]
// Last value seen by this controller to be used in policy enforcement
// May not be nil
lastReconciledValue PolicyBinding
lastReconciledValue *v1alpha1.ValidatingAdmissionPolicyBinding
}
type paramInfo struct {
@ -106,31 +118,33 @@ type paramInfo struct {
stop func()
// Policy Definitions which refer to this param CRD
dependentDefinitions sets.String
dependentDefinitions sets.Set[namespacedName]
}
func NewAdmissionController(
// Informers
policyDefinitionsInformer cache.SharedIndexInformer,
policyBindingInformer cache.SharedIndexInformer,
// Injected Dependencies
objectConverter ObjectConverter,
informerFactory informers.SharedInformerFactory,
client kubernetes.Interface,
restMapper meta.RESTMapper,
dynamicClient dynamic.Interface,
) CELPolicyEvaluator {
matcher := matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)
validatorCompiler := &CELValidatorCompiler{
Matcher: matcher,
}
c := &celAdmissionController{
definitionInfo: make(map[string]*definitionInfo),
bindingInfos: make(map[string]*bindingInfo),
paramsCRDControllers: make(map[schema.GroupVersionKind]*paramInfo),
definitionsToBindings: make(map[string]sets.String),
definitionInfo: make(map[namespacedName]*definitionInfo),
bindingInfos: make(map[namespacedName]*bindingInfo),
paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo),
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
dynamicClient: dynamicClient,
objectConverter: objectConverter,
validatorCompiler: validatorCompiler,
restMapper: restMapper,
}
c.policyDefinitionsController = generic.NewController(
generic.NewInformer[PolicyDefinition](policyDefinitionsInformer),
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
c.reconcilePolicyDefinition,
generic.ControllerOptions{
Workers: 1,
@ -138,7 +152,8 @@ func NewAdmissionController(
},
)
c.policyBindingController = generic.NewController(
generic.NewInformer[PolicyBinding](policyBindingInformer),
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()),
c.reconcilePolicyBinding,
generic.ControllerOptions{
Workers: 1,
@ -187,30 +202,57 @@ func (c *celAdmissionController) Validate(
c.mutex.RLock()
defer c.mutex.RUnlock()
var allDecisions []PolicyDecisionWithMetadata = nil
var deniedDecisions []policyDecisionWithMetadata
addConfigError := func(err error, definition PolicyDefinition, binding PolicyBinding) {
wrappedError := fmt.Errorf("configuration error: %w", err)
switch p := definition.GetFailurePolicy(); p {
case Ignore:
klog.Info(wrappedError)
addConfigError := func(err error, definition *v1alpha1.ValidatingAdmissionPolicy, binding *v1alpha1.ValidatingAdmissionPolicyBinding) {
// we always default the FailurePolicy if it is unset and validate it in API level
var policy v1alpha1.FailurePolicyType
if definition.Spec.FailurePolicy == nil {
policy = v1alpha1.Fail
} else {
policy = *definition.Spec.FailurePolicy
}
// apply FailurePolicy specified in ValidatingAdmissionPolicy, the default would be Fail
switch policy {
case v1alpha1.Ignore:
// TODO: add metrics for ignored error here
return
case Fail:
allDecisions = append(allDecisions, PolicyDecisionWithMetadata{
PolicyDecision: PolicyDecision{
Kind: Deny,
Message: wrappedError.Error(),
case v1alpha1.Fail:
var message string
if binding == nil {
message = fmt.Errorf("failed to configure policy: %w", err).Error()
} else {
message = fmt.Errorf("failed to configure binding: %w", err).Error()
}
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
policyDecision: policyDecision{
kind: deny,
message: message,
},
Definition: definition,
Binding: binding,
definition: definition,
binding: binding,
})
default:
utilruntime.HandleError(fmt.Errorf("unrecognized failure policy: '%v'", p))
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
policyDecision: policyDecision{
kind: deny,
message: fmt.Errorf("unrecognized failure policy: '%v'", policy).Error(),
},
definition: definition,
binding: binding,
})
}
}
for definitionNamespacedName, definitionInfo := range c.definitionInfo {
definition := definitionInfo.lastReconciledValue
if !definition.Matches(a) {
matches, matchKind, err := c.validatorCompiler.DefinitionMatches(a, o, definition)
if err != nil {
// Configuration error.
addConfigError(err, definition, nil)
continue
}
if !matches {
// Policy definition does not match request
continue
} else if definitionInfo.configurationError != nil {
@ -221,8 +263,6 @@ func (c *celAdmissionController) Validate(
dependentBindings := c.definitionsToBindings[definitionNamespacedName]
if len(dependentBindings) == 0 {
// Definition has no known bindings yet.
addConfigError(errors.New("no bindings found"), definition, nil)
continue
}
@ -231,40 +271,61 @@ func (c *celAdmissionController) Validate(
// be a bindingInfo for it
bindingInfo := c.bindingInfos[namespacedBindingName]
binding := bindingInfo.lastReconciledValue
if !binding.Matches(a) {
matches, err := c.validatorCompiler.BindingMatches(a, o, binding)
if err != nil {
// Configuration error.
addConfigError(err, definition, binding)
continue
}
if !matches {
continue
}
var param *unstructured.Unstructured
// If definition has no paramsource, always provide nil params to
// evaluator. If binding specifies a params to use they are ignored.
// Done this way so you can configure params before definition is ready.
if paramSource := definition.GetParamSource(); paramSource != nil {
paramsNamespace, paramsName := binding.GetTargetParams()
// If definition has paramKind, paramRef is required in binding.
// If definition has no paramKind, paramRef set in binding will be ignored.
paramKind := definition.Spec.ParamKind
paramRef := binding.Spec.ParamRef
if paramKind != nil && paramRef != nil {
// Find the params referred by the binding by looking its name up
// in our informer for its CRD
paramInfo, ok := c.paramsCRDControllers[*paramSource]
paramInfo, ok := c.paramsCRDControllers[*paramKind]
if !ok {
addConfigError(fmt.Errorf("paramSource kind `%v` not known",
paramSource.String()), definition, binding)
addConfigError(fmt.Errorf("paramKind kind `%v` not known",
paramKind.String()), definition, binding)
continue
}
if len(paramsNamespace) == 0 {
param, err = paramInfo.controller.Informer().Get(paramsName)
// If the param informer for this admission policy has not yet
// had time to perform an initial listing, don't attempt to use
// it.
//!TOOD(alexzielenski): add a wait for a very short amount of
// time for the cache to sync
if !paramInfo.controller.HasSynced() {
addConfigError(fmt.Errorf("paramKind kind `%v` not yet synced to use for admission",
paramKind.String()), definition, binding)
continue
}
if len(paramRef.Namespace) == 0 {
param, err = paramInfo.controller.Informer().Get(paramRef.Name)
} else {
param, err = paramInfo.controller.Informer().Namespaced(paramsNamespace).Get(paramsName)
param, err = paramInfo.controller.Informer().Namespaced(paramRef.Namespace).Get(paramRef.Name)
}
if err != nil {
// Apply failure policy
addConfigError(err, definition, binding)
if k8serrors.IsNotFound(err) {
// Param doesnt exist yet?
// Maybe just have to wait a bit.
if k8serrors.IsInvalid(err) {
// Param mis-configured
// require to set paramRef.namespace for namespaced resource and unset paramRef.namespace for cluster scoped resource
continue
} else if k8serrors.IsNotFound(err) {
// Param not yet available. User may need to wait a bit
// before being able to use it for validation.
continue
}
@ -274,47 +335,69 @@ func (c *celAdmissionController) Validate(
}
}
if bindingInfo.evaluator == nil {
validator := bindingInfo.validator.Load()
if validator == nil {
// Compile policy definition using binding
bindingInfo.evaluator, err = definition.Compile(c.objectConverter, c.restMapper)
newValidator := c.validatorCompiler.Compile(definition)
validator = &newValidator
bindingInfo.validator.Store(validator)
}
decisions, err := (*validator).Validate(a, o, param, matchKind)
if err != nil {
// compilation error. Apply failure policy
wrappedError := fmt.Errorf("failed to compile CEL expression: %w", err)
// runtime error. Apply failure policy
wrappedError := fmt.Errorf("failed to evaluate CEL expression: %w", err)
addConfigError(wrappedError, definition, binding)
continue
}
c.bindingInfos[namespacedBindingName] = bindingInfo
}
decisions := bindingInfo.evaluator(a, param)
for _, decision := range decisions {
switch decision.Kind {
case Admit:
// Do nothing
case Deny:
allDecisions = append(allDecisions, PolicyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: decision,
switch decision.kind {
case admit:
// TODO: add metrics for ignored error here
case deny:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
definition: definition,
binding: binding,
policyDecision: decision,
})
default:
// unrecognized decision. ignore
return fmt.Errorf("unrecognized evaluation decision '%s' for ValidatingAdmissionPolicyBinding '%s' with ValidatingAdmissionPolicy '%s'",
decision.kind, binding.Name, definition.Name)
}
}
}
}
if len(allDecisions) > 0 {
return k8serrors.NewConflict(
a.GetResource().GroupResource(), a.GetName(),
&PolicyError{
Decisions: allDecisions,
})
if len(deniedDecisions) > 0 {
// TODO: refactor admission.NewForbidden so the name extraction is reusable but the code/reason is customizable
var message string
deniedDecision := deniedDecisions[0]
if deniedDecision.binding != nil {
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' with binding '%s' denied request: %s", deniedDecision.definition.Name, deniedDecision.binding.Name, deniedDecision.message)
} else {
message = fmt.Sprintf("ValidatingAdmissionPolicy '%s' denied request: %s", deniedDecision.definition.Name, deniedDecision.message)
}
err := admission.NewForbidden(a, errors.New(message)).(*k8serrors.StatusError)
reason := deniedDecision.reason
if len(reason) == 0 {
reason = metav1.StatusReasonInvalid
}
err.ErrStatus.Reason = reason
err.ErrStatus.Code = reasonToCode(reason)
err.ErrStatus.Details.Causes = append(err.ErrStatus.Details.Causes, metav1.StatusCause{Message: message})
return err
}
return nil
}
func (c *celAdmissionController) HasSynced() bool {
return c.policyBindingController.HasSynced() &&
c.policyDefinitionsController.HasSynced()
}
func (c *celAdmissionController) ValidateInitialization() error {
return c.validatorCompiler.ValidateInitialization()
}

View File

@ -21,6 +21,7 @@ import (
"fmt"
"time"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -30,28 +31,27 @@ import (
"k8s.io/client-go/tools/cache"
)
func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name string, definition PolicyDefinition) error {
func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name string, definition *v1alpha1.ValidatingAdmissionPolicy) error {
c.mutex.Lock()
defer c.mutex.Unlock()
// Namespace for policydefinition is empty. Leaving usage here for compatibility
// with future NamespacedPolicyDefinition
namespacedName := namespace + "/" + name
info, ok := c.definitionInfo[namespacedName]
// Namespace for policydefinition is empty.
nn := getNamespaceName(namespace, name)
info, ok := c.definitionInfo[nn]
if !ok {
info = &definitionInfo{}
c.definitionInfo[namespacedName] = info
c.definitionInfo[nn] = info
}
var paramSource *schema.GroupVersionKind
var paramSource *v1alpha1.ParamKind
if definition != nil {
paramSource = definition.GetParamSource()
paramSource = definition.Spec.ParamKind
}
// If param source has changed, remove definition as dependent of old params
// If there are no more dependents of old param, stop and clean up controller
if info.lastReconciledValue != nil && info.lastReconciledValue.GetParamSource() != nil {
oldParamSource := *info.lastReconciledValue.GetParamSource()
if info.lastReconciledValue != nil && info.lastReconciledValue.Spec.ParamKind != nil {
oldParamSource := *info.lastReconciledValue.Spec.ParamKind
// If we are:
// - switching from having a param to not having a param (includes deletion)
@ -59,7 +59,7 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
// we remove dependency on the controller.
if paramSource == nil || *paramSource != oldParamSource {
if oldParamInfo, ok := c.paramsCRDControllers[oldParamSource]; ok {
oldParamInfo.dependentDefinitions.Delete(namespacedName)
oldParamInfo.dependentDefinitions.Delete(nn)
if len(oldParamInfo.dependentDefinitions) == 0 {
oldParamInfo.stop()
delete(c.paramsCRDControllers, oldParamSource)
@ -70,14 +70,14 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
// Reset all previously compiled evaluators in case something relevant in
// definition has changed.
for key := range c.definitionsToBindings[namespacedName] {
for key := range c.definitionsToBindings[nn] {
bindingInfo := c.bindingInfos[key]
bindingInfo.evaluator = nil
bindingInfo.validator.Store(nil)
c.bindingInfos[key] = bindingInfo
}
if definition == nil {
delete(c.definitionInfo, namespacedName)
delete(c.definitionInfo, nn)
return nil
}
@ -91,12 +91,28 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
}
// find GVR for params
paramsGVR, err := c.restMapper.RESTMapping(paramSource.GroupKind(), paramSource.Version)
// Parse param source into a GVK
paramSourceGV, err := schema.ParseGroupVersion(paramSource.APIVersion)
if err != nil {
// Failed to resolve. Return error so we retry again (rate limited)
// Save a record of this definition with an evaluator that unconditionally
info.configurationError = fmt.Errorf("failed to parse apiVersion of paramKind '%v' with error: %w", paramSource.String(), err)
// Return nil, since this error cannot be resolved by waiting more time
return nil
}
paramsGVR, err := c.restMapper.RESTMapping(schema.GroupKind{
Group: paramSourceGV.Group,
Kind: paramSource.Kind,
}, paramSourceGV.Version)
if err != nil {
// Failed to resolve. Return error so we retry again (rate limited)
// Save a record of this definition with an evaluator that unconditionally
//
info.configurationError = fmt.Errorf("failed to find resource for param source: '%v'", paramSource.String())
info.configurationError = fmt.Errorf("failed to find resource referenced by paramKind: '%v'", paramSourceGV.WithKind(paramSource.Kind))
return info.configurationError
}
@ -126,7 +142,7 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
c.paramsCRDControllers[*paramSource] = &paramInfo{
controller: controller,
stop: instanceCancel,
dependentDefinitions: sets.NewString(namespacedName),
dependentDefinitions: sets.New(nn),
}
go informer.Informer().Run(instanceContext.Done())
@ -136,37 +152,37 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
return nil
}
func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string, binding PolicyBinding) error {
func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string, binding *v1alpha1.ValidatingAdmissionPolicyBinding) error {
c.mutex.Lock()
defer c.mutex.Unlock()
// Namespace for PolicyBinding is empty. In the future a namespaced binding
// may be added
// https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042
namespacedName := namespace + "/" + name
info, ok := c.bindingInfos[namespacedName]
nn := getNamespaceName(namespace, name)
info, ok := c.bindingInfos[nn]
if !ok {
info = &bindingInfo{}
c.bindingInfos[namespacedName] = info
c.bindingInfos[nn] = info
}
oldNamespacedDefinitionName := ""
var oldNamespacedDefinitionName namespacedName
if info.lastReconciledValue != nil {
oldefinitionNamespace, oldefinitionName := info.lastReconciledValue.GetTargetDefinition()
oldNamespacedDefinitionName = oldefinitionNamespace + "/" + oldefinitionName
// All validating policies are cluster-scoped so have empty namespace
oldNamespacedDefinitionName = getNamespaceName("", info.lastReconciledValue.Spec.PolicyName)
}
namespacedDefinitionName := ""
var namespacedDefinitionName namespacedName
if binding != nil {
newDefinitionNamespace, newDefinitionName := binding.GetTargetDefinition()
namespacedDefinitionName = newDefinitionNamespace + "/" + newDefinitionName
// All validating policies are cluster-scoped so have empty namespace
namespacedDefinitionName = getNamespaceName("", binding.Spec.PolicyName)
}
// Remove record of binding from old definition if the referred policy
// has changed
if oldNamespacedDefinitionName != namespacedDefinitionName {
if dependentBindings, ok := c.definitionsToBindings[oldNamespacedDefinitionName]; ok {
dependentBindings.Delete(namespacedName)
dependentBindings.Delete(nn)
// if there are no more dependent bindings, remove knowledge of the
// definition altogether
@ -177,19 +193,19 @@ func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string,
}
if binding == nil {
delete(c.bindingInfos, namespacedName)
delete(c.bindingInfos, nn)
return nil
}
// Add record of binding to new definition
if dependentBindings, ok := c.definitionsToBindings[namespacedDefinitionName]; ok {
dependentBindings.Insert(namespacedName)
dependentBindings.Insert(nn)
} else {
c.definitionsToBindings[namespacedDefinitionName] = sets.NewString(namespacedName)
c.definitionsToBindings[namespacedDefinitionName] = sets.New(nn)
}
// Remove compiled template for old binding
info.evaluator = nil
info.validator.Store(nil)
info.lastReconciledValue = binding
return nil
}
@ -201,3 +217,10 @@ func (c *celAdmissionController) reconcileParams(namespace, name string, params
// checker errors to the status of the resources.
return nil
}
func getNamespaceName(namespace, name string) namespacedName {
return namespacedName{
namespace: namespace,
name: name,
}
}

View File

@ -1,258 +0,0 @@
/*
Copyright 2022 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 cel
import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
)
////////////////////////////////////////////////////////////////////////////////
// Fake Policy Definitions
////////////////////////////////////////////////////////////////////////////////
type FakePolicyDefinition struct {
metav1.TypeMeta
metav1.ObjectMeta
// Function called when `Matches` is called
// If nil, a default function that always returns true is used
// Specified as a function pointer so that this type is still comparable
MatchFunc *func(admission.Attributes) bool `json:"-"`
// Func invoked for implementation of `Compile`
// Specified as a function pointer so that this type is still comparable
CompileFunc *func(converter ObjectConverter) (EvaluatorFunc, error) `json:"-"`
// GVK to return when ParamSource() is called
ParamSource *schema.GroupVersionKind `json:"paramSource"`
FailurePolicy FailurePolicy `json:"failurePolicy"`
}
var _ PolicyDefinition = &FakePolicyDefinition{}
func (f *FakePolicyDefinition) SetGroupVersionKind(kind schema.GroupVersionKind) {
f.TypeMeta.APIVersion = kind.GroupVersion().String()
f.TypeMeta.Kind = kind.Kind
}
func (f *FakePolicyDefinition) GroupVersionKind() schema.GroupVersionKind {
parsedGV, err := schema.ParseGroupVersion(f.TypeMeta.APIVersion)
if err != nil || f.TypeMeta.Kind == "" || parsedGV.Empty() {
return schema.GroupVersionKind{
Group: "admission.k8s.io",
Version: "v1alpha1",
Kind: "PolicyDefinition",
}
}
return schema.GroupVersionKind{
Group: parsedGV.Group,
Version: parsedGV.Version,
Kind: f.TypeMeta.Kind,
}
}
func (f *FakePolicyDefinition) GetObjectKind() schema.ObjectKind {
return f
}
func (f *FakePolicyDefinition) DeepCopyObject() runtime.Object {
copied := *f
f.ObjectMeta.DeepCopyInto(&copied.ObjectMeta)
return &copied
}
func (f *FakePolicyDefinition) GetName() string {
return f.ObjectMeta.Name
}
func (f *FakePolicyDefinition) GetNamespace() string {
return f.ObjectMeta.Namespace
}
func (f *FakePolicyDefinition) Matches(a admission.Attributes) bool {
if f.MatchFunc == nil || *f.MatchFunc == nil {
return true
}
return (*f.MatchFunc)(a)
}
func (f *FakePolicyDefinition) Compile(
converter ObjectConverter,
mapper meta.RESTMapper,
) (EvaluatorFunc, error) {
if f.CompileFunc == nil || *f.CompileFunc == nil {
panic("must provide a CompileFunc to policy definition")
}
return (*f.CompileFunc)(converter)
}
func (f *FakePolicyDefinition) GetParamSource() *schema.GroupVersionKind {
return f.ParamSource
}
func (f *FakePolicyDefinition) GetFailurePolicy() FailurePolicy {
return f.FailurePolicy
}
////////////////////////////////////////////////////////////////////////////////
// Fake Policy Binding
////////////////////////////////////////////////////////////////////////////////
type FakePolicyBinding struct {
metav1.TypeMeta
metav1.ObjectMeta
// Specified as a function pointer so that this type is still comparable
MatchFunc *func(admission.Attributes) bool `json:"-"`
Params string `json:"params"`
Policy string `json:"policy"`
}
var _ PolicyBinding = &FakePolicyBinding{}
func (f *FakePolicyBinding) SetGroupVersionKind(kind schema.GroupVersionKind) {
f.TypeMeta.APIVersion = kind.GroupVersion().String()
f.TypeMeta.Kind = kind.Kind
}
func (f *FakePolicyBinding) GroupVersionKind() schema.GroupVersionKind {
parsedGV, err := schema.ParseGroupVersion(f.TypeMeta.APIVersion)
if err != nil || f.TypeMeta.Kind == "" || parsedGV.Empty() {
return schema.GroupVersionKind{
Group: "admission.k8s.io",
Version: "v1alpha1",
Kind: "PolicyBinding",
}
}
return schema.GroupVersionKind{
Group: parsedGV.Group,
Version: parsedGV.Version,
Kind: f.TypeMeta.Kind,
}
}
func (f *FakePolicyBinding) GetObjectKind() schema.ObjectKind {
return f
}
func (f *FakePolicyBinding) DeepCopyObject() runtime.Object {
copied := *f
f.ObjectMeta.DeepCopyInto(&copied.ObjectMeta)
return &copied
}
func (f *FakePolicyBinding) Matches(a admission.Attributes) bool {
if f.MatchFunc == nil || *f.MatchFunc == nil {
return true
}
return (*f.MatchFunc)(a)
}
func (f *FakePolicyBinding) GetTargetDefinition() (namespace, name string) {
return f.Namespace, f.Policy
}
func (f *FakePolicyBinding) GetTargetParams() (namespace, name string) {
return f.Namespace, f.Params
}
/// List Types
type FakePolicyDefinitionList struct {
metav1.TypeMeta
metav1.ListMeta
Items []FakePolicyDefinition
}
func (f *FakePolicyDefinitionList) SetGroupVersionKind(kind schema.GroupVersionKind) {
f.TypeMeta.APIVersion = kind.GroupVersion().String()
f.TypeMeta.Kind = kind.Kind
}
func (f *FakePolicyDefinitionList) GroupVersionKind() schema.GroupVersionKind {
parsedGV, err := schema.ParseGroupVersion(f.TypeMeta.APIVersion)
if err != nil || f.TypeMeta.Kind == "" || parsedGV.Empty() {
return schema.GroupVersionKind{
Group: "admission.k8s.io",
Version: "v1alpha1",
Kind: "PolicyDefinitionList",
}
}
return schema.GroupVersionKind{
Group: parsedGV.Group,
Version: parsedGV.Version,
Kind: f.TypeMeta.Kind,
}
}
func (f *FakePolicyDefinitionList) GetObjectKind() schema.ObjectKind {
return f
}
func (f *FakePolicyDefinitionList) DeepCopyObject() runtime.Object {
copied := *f
f.ListMeta.DeepCopyInto(&copied.ListMeta)
copied.Items = make([]FakePolicyDefinition, len(f.Items))
copy(copied.Items, f.Items)
return &copied
}
type FakePolicyBindingList struct {
metav1.TypeMeta
metav1.ListMeta
Items []FakePolicyBinding
}
func (f *FakePolicyBindingList) SetGroupVersionKind(kind schema.GroupVersionKind) {
f.TypeMeta.APIVersion = kind.GroupVersion().String()
f.TypeMeta.Kind = kind.Kind
}
func (f *FakePolicyBindingList) GroupVersionKind() schema.GroupVersionKind {
parsedGV, err := schema.ParseGroupVersion(f.TypeMeta.APIVersion)
if err != nil || f.TypeMeta.Kind == "" || parsedGV.Empty() {
return schema.GroupVersionKind{
Group: "admission.k8s.io",
Version: "v1alpha1",
Kind: "PolicyBindingList",
}
}
return schema.GroupVersionKind{
Group: parsedGV.Group,
Version: parsedGV.Version,
Kind: f.TypeMeta.Kind,
}
}
func (f *FakePolicyBindingList) GetObjectKind() schema.ObjectKind {
return f
}
func (f *FakePolicyBindingList) DeepCopyObject() runtime.Object {
copied := *f
f.ListMeta.DeepCopyInto(&copied.ListMeta)
copied.Items = make([]FakePolicyBinding, len(f.Items))
copy(copied.Items, f.Items)
return &copied
}

View File

@ -18,38 +18,13 @@ package cel
import (
"context"
"k8s.io/apiserver/pkg/admission"
)
type CELPolicyEvaluator interface {
admission.InitializationValidator
Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error
HasSynced() bool
}
// NewPluginInitializer creates a plugin initializer which dependency injects a
// singleton cel admission controller into the plugins which desire it
func NewPluginInitializer(validator CELPolicyEvaluator) *PluginInitializer {
return &PluginInitializer{validator: validator}
}
// WantsCELPolicyEvaluator gives the ability to have the shared
// CEL Admission Controller dependency injected at initialization-time.
type WantsCELPolicyEvaluator interface {
SetCELPolicyEvaluator(CELPolicyEvaluator)
}
// PluginInitializer is used for initialization of the webhook admission plugin.
type PluginInitializer struct {
validator CELPolicyEvaluator
}
var _ admission.PluginInitializer = &PluginInitializer{}
// Initialize checks the initialization interfaces implemented by each plugin
// and provide the appropriate initialization data
func (i *PluginInitializer) Initialize(plugin admission.Interface) {
if wants, ok := plugin.(WantsCELPolicyEvaluator); ok {
wants.SetCELPolicyEvaluator(i.validator)
}
Run(stopCh <-chan struct{})
}

View File

@ -17,92 +17,34 @@ limitations under the License.
package cel
import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"github.com/google/cel-go/common/types/ref"
)
type FailurePolicy string
const (
Fail FailurePolicy = "Fail"
Ignore FailurePolicy = "Ignore"
)
// EvaluatorFunc represents the AND of one or more compiled CEL expression's
// evaluators `params` may be nil if definition does not specify a paramsource
type EvaluatorFunc func(a admission.Attributes, params *unstructured.Unstructured) []PolicyDecision
// ObjectConverter is Dependency Injected into the PolicyDefinition's `Compile`
// function to assist with converting types and values to/from CEL-typed values.
type ObjectConverter interface {
// DeclForResource looks up the openapi or JSONSchemaProps, structural schema, etc.
// and compiles it into something that can be used to turn objects into CEL
// values
DeclForResource(gvr schema.GroupVersionResource) (*cel.DeclType, error)
// ValueForObject takes a Kubernetes Object and uses the CEL DeclType
// to transform it into a CEL value.
// Object may be a typed native object or an unstructured object
ValueForObject(value runtime.Object, decl *cel.DeclType) (ref.Val, error)
// Validator defines the func used to validate the cel expressions
// matchKind provides the GroupVersionKind that the object should be
// validated by CEL expressions as.
type Validator interface {
Validate(a admission.Attributes, o admission.ObjectInterfaces, versionedParams runtime.Object, matchKind schema.GroupVersionKind) ([]policyDecision, error)
}
// PolicyDefinition is an interface for internal policy binding type.
// Implemented by mock/testing types, and to be implemented by the public API
// types once they have completed API review.
//
// The interface closely mirrors the format and functionality of the
// PolicyDefinition proposed in the KEP.
type PolicyDefinition interface {
runtime.Object
// ValidatorCompiler is Dependency Injected into the PolicyDefinition's `Compile`
// function to assist with converting types and values to/from CEL-typed values.
type ValidatorCompiler interface {
admission.InitializationValidator
// Matches says whether this policy definition matches the provided admission
// resource request
Matches(a admission.Attributes) bool
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error)
Compile(
// Definition is provided with a converter which may be used by the
// return evaluator function to convert objects into CEL-typed objects
objectConverter ObjectConverter,
// Injected RESTMapper to assist with compilation
mapper meta.RESTMapper,
) (EvaluatorFunc, error)
// GetParamSource returns the GVK for the CRD used as the source of
// parameters used in the evaluation of instances of this policy
// May return nil if there is no paramsource for this definition.
GetParamSource() *schema.GroupVersionKind
// GetFailurePolicy returns how an object should be treated during an
// admission when there is a configuration error preventing CEL evaluation
GetFailurePolicy() FailurePolicy
}
// PolicyBinding is an interface for internal policy binding type. Implemented
// by mock/testing types, and to be implemented by the public API types once
// they have completed API review.
//
// The interface closely mirrors the format and functionality of the
// PolicyBinding proposed in the KEP.
type PolicyBinding interface {
runtime.Object
// Matches says whether this policy binding matches the provided admission
// Matches says whether this policy definition matches the provided admission
// resource request
Matches(a admission.Attributes) bool
BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error)
// GetTargetDefinition returns the Namespace/Name of Policy Definition used
// by this binding.
GetTargetDefinition() (namespace, name string)
// GetTargetParams returns the Namespace/Name of instance of TargetDefinition's
// ParamSource to be provided to the CEL expressions of the definition during
// evaluation.
// If TargetDefinition has nil ParamSource, this is ignored.
GetTargetParams() (namespace, name string)
// Compile is used for the cel expression compilation
Compile(
policy *v1alpha1.ValidatingAdmissionPolicy,
) Validator
}

View File

@ -52,7 +52,7 @@ type ControllerOptions struct {
Workers uint
}
func (c controller[T]) Informer() Informer[T] {
func (c *controller[T]) Informer() Informer[T] {
return c.informer
}
@ -73,7 +73,7 @@ func NewController[T runtime.Object](
options: options,
informer: informer,
reconciler: reconciler,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), options.Name),
queue: nil,
}
}
@ -84,10 +84,18 @@ func (c *controller[T]) Run(ctx context.Context) error {
klog.Infof("starting %s", c.options.Name)
defer klog.Infof("stopping %s", c.options.Name)
c.queue = workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), c.options.Name)
// Forcefully shutdown workqueue. Drop any enqueued items.
// Important to do this in a `defer` at the start of `Run`.
// Otherwise, if there are any early returns without calling this, we
// would never shut down the workqueue
defer c.queue.ShutDown()
enqueue := func(obj interface{}) {
var key string
var err error
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
utilruntime.HandleError(err)
return
}
@ -185,7 +193,7 @@ func (c *controller[T]) HasSynced() bool {
func (c *controller[T]) runWorker() {
for {
obj, shutdown := c.queue.Get()
key, shutdown := c.queue.Get()
if shutdown {
return
}
@ -221,9 +229,9 @@ func (c *controller[T]) runWorker() {
// Finally, if no error occurs we Forget this item so it is allowed
// to be re-enqueued without a long rate limit
c.queue.Forget(obj)
klog.Infof("Successfully synced '%s'", key)
klog.V(4).Infof("syncAdmissionPolicy(%q)", key)
return nil
}(obj)
}(key)
if err != nil {
utilruntime.HandleError(err)

View File

@ -0,0 +1,192 @@
/*
Copyright 2022 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 matching
import (
"fmt"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/kubernetes"
listersv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
)
type MatchCriteria interface {
namespace.NamespaceSelectorProvider
object.ObjectSelectorProvider
GetMatchResources() v1alpha1.MatchResources
}
// Matcher decides if a request matches against matchCriteria
type Matcher struct {
namespaceMatcher *namespace.Matcher
objectMatcher *object.Matcher
}
// NewMatcher initialize the matcher with dependencies requires
func NewMatcher(
namespaceLister listersv1.NamespaceLister,
client kubernetes.Interface,
) *Matcher {
return &Matcher{
namespaceMatcher: &namespace.Matcher{
NamespaceLister: namespaceLister,
Client: client,
},
objectMatcher: &object.Matcher{},
}
}
// ValidateInitialization verify if the matcher is ready before use
func (m *Matcher) ValidateInitialization() error {
if err := m.namespaceMatcher.Validate(); err != nil {
return fmt.Errorf("namespaceMatcher is not properly setup: %v", err)
}
return nil
}
func (m *Matcher) Matches(attr admission.Attributes, o admission.ObjectInterfaces, criteria MatchCriteria) (bool, schema.GroupVersionKind, error) {
matches, matchNsErr := m.namespaceMatcher.MatchNamespaceSelector(criteria, attr)
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
if !matches && matchNsErr == nil {
return false, schema.GroupVersionKind{}, nil
}
matches, matchObjErr := m.objectMatcher.MatchObjectSelector(criteria, attr)
// Should not return an error here for policy which do not apply to the request, even if err is an unexpected scenario.
if !matches && matchObjErr == nil {
return false, schema.GroupVersionKind{}, nil
}
matchResources := criteria.GetMatchResources()
matchPolicy := matchResources.MatchPolicy
if isExcluded, _, err := matchesResourceRules(matchResources.ExcludeResourceRules, matchPolicy, attr, o); isExcluded || err != nil {
return false, schema.GroupVersionKind{}, err
}
var (
isMatch bool
matchKind schema.GroupVersionKind
matchErr error
)
if len(matchResources.ResourceRules) == 0 {
isMatch = true
matchKind = attr.GetKind()
} else {
isMatch, matchKind, matchErr = matchesResourceRules(matchResources.ResourceRules, matchPolicy, attr, o)
}
if matchErr != nil {
return false, schema.GroupVersionKind{}, matchErr
}
if !isMatch {
return false, schema.GroupVersionKind{}, nil
}
// now that we know this applies to this request otherwise, if there were selector errors, return them
if matchNsErr != nil {
return false, schema.GroupVersionKind{}, matchNsErr
}
if matchObjErr != nil {
return false, schema.GroupVersionKind{}, matchObjErr
}
return true, matchKind, nil
}
func matchesResourceRules(namedRules []v1alpha1.NamedRuleWithOperations, matchPolicy *v1alpha1.MatchPolicyType, attr admission.Attributes, o admission.ObjectInterfaces) (bool, schema.GroupVersionKind, error) {
matchKind := attr.GetKind()
for _, namedRule := range namedRules {
rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
ruleMatcher := rules.Matcher{
Rule: rule,
Attr: attr,
}
if !ruleMatcher.Matches() {
continue
}
// an empty name list always matches
if len(namedRule.ResourceNames) == 0 {
return true, matchKind, nil
}
// TODO: GetName() can return an empty string if the user is relying on
// the API server to generate the name... figure out what to do for this edge case
name := attr.GetName()
for _, matchedName := range namedRule.ResourceNames {
if name == matchedName {
return true, matchKind, nil
}
}
}
// if match policy is undefined or exact, don't perform fuzzy matching
// note that defaulting to fuzzy matching is set by the API
if matchPolicy == nil || *matchPolicy == v1alpha1.Exact {
return false, schema.GroupVersionKind{}, nil
}
attrWithOverride := &attrWithResourceOverride{Attributes: attr}
equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
for _, namedRule := range namedRules {
for _, equivalent := range equivalents {
if equivalent == attr.GetResource() {
// we have already checked the original resource
continue
}
attrWithOverride.resource = equivalent
rule := v1.RuleWithOperations(namedRule.RuleWithOperations)
m := rules.Matcher{
Rule: rule,
Attr: attrWithOverride,
}
if !m.Matches() {
continue
}
matchKind = o.GetEquivalentResourceMapper().KindFor(equivalent, attr.GetSubresource())
if matchKind.Empty() {
return false, schema.GroupVersionKind{}, fmt.Errorf("unable to convert to %v: unknown kind", equivalent)
}
// an empty name list always matches
if len(namedRule.ResourceNames) == 0 {
return true, matchKind, nil
}
// TODO: GetName() can return an empty string if the user is relying on
// the API server to generate the name... figure out what to do for this edge case
name := attr.GetName()
for _, matchedName := range namedRule.ResourceNames {
if name == matchedName {
return true, matchKind, nil
}
}
}
}
return false, schema.GroupVersionKind{}, nil
}
type attrWithResourceOverride struct {
admission.Attributes
resource schema.GroupVersionResource
}
func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { return a.resource }

View File

@ -0,0 +1,787 @@
/*
Copyright 2022 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 matching
import (
"fmt"
"strings"
"testing"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
"k8s.io/apiserver/pkg/apis/example"
)
var _ MatchCriteria = &fakeCriteria{}
type fakeCriteria struct {
matchResources v1alpha1.MatchResources
}
func (fc *fakeCriteria) GetMatchResources() v1alpha1.MatchResources {
return fc.matchResources
}
func (fc *fakeCriteria) GetParsedNamespaceSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(fc.matchResources.NamespaceSelector)
}
func (fc *fakeCriteria) GetParsedObjectSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(fc.matchResources.ObjectSelector)
}
func TestMatcher(t *testing.T) {
a := &Matcher{namespaceMatcher: &namespace.Matcher{}, objectMatcher: &object.Matcher{}}
allScopes := v1.AllScopes
exactMatch := v1alpha1.Exact
equivalentMatch := v1alpha1.Equivalent
mapper := runtime.NewEquivalentResourceRegistryWithIdentity(func(resource schema.GroupResource) string {
if resource.Resource == "deployments" {
// co-locate deployments in all API groups
return "/deployments"
}
return ""
})
mapper.RegisterKindFor(schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "", schema.GroupVersionKind{"extensions", "v1beta1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1beta1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1alpha1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "scale", schema.GroupVersionKind{"extensions", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", schema.GroupVersionKind{"autoscaling", "v1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "deployments"}, "scale", schema.GroupVersionKind{"apps", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha1", "deployments"}, "scale", schema.GroupVersionKind{"apps", "v1alpha1", "Scale"})
// register invalid kinds to trigger an error
mapper.RegisterKindFor(schema.GroupVersionResource{"example.com", "v1", "widgets"}, "", schema.GroupVersionKind{"", "", ""})
mapper.RegisterKindFor(schema.GroupVersionResource{"example.com", "v2", "widgets"}, "", schema.GroupVersionKind{"", "", ""})
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
// TODO write test cases for name matching and exclude matching
testcases := []struct {
name string
criteria *v1alpha1.MatchResources
attrs admission.Attributes
expectMatches bool
expectMatchKind *schema.GroupVersionKind
expectErr string
}{
{
name: "no rules (just write)",
criteria: &v1alpha1.MatchResources{NamespaceSelector: &metav1.LabelSelector{}, ResourceRules: []v1alpha1.NamedRuleWithOperations{}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
},
{
name: "wildcard rule, match as requested",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"apps", "v1", "Deployment"},
},
{
name: "specific rules, prefer exact match",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"apps", "v1", "Deployment"},
},
{
name: "specific rules, match miss",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
},
{
name: "specific rules, exact match miss",
criteria: &v1alpha1.MatchResources{
MatchPolicy: &exactMatch,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
},
{
name: "specific rules, equivalent match, prefer extensions",
criteria: &v1alpha1.MatchResources{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"extensions", "v1beta1", "Deployment"},
},
{
name: "specific rules, equivalent match, prefer apps",
criteria: &v1alpha1.MatchResources{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"apps", "v1beta1", "Deployment"},
},
{
name: "specific rules, subresource prefer exact match",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"autoscaling", "v1", "Scale"},
},
{
name: "specific rules, subresource match miss",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
},
{
name: "specific rules, subresource exact match miss",
criteria: &v1alpha1.MatchResources{
MatchPolicy: &exactMatch,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
},
{
name: "specific rules, subresource equivalent match, prefer extensions",
criteria: &v1alpha1.MatchResources{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"extensions", "v1beta1", "Scale"},
},
{
name: "specific rules, subresource equivalent match, prefer apps",
criteria: &v1alpha1.MatchResources{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}, {
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"apps", "v1beta1", "Scale"},
},
{
name: "specific rules, prefer exact match and name match",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
ResourceNames: []string{"name"},
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"autoscaling", "v1", "Scale"},
},
{
name: "specific rules, prefer exact match and name match miss",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
ResourceNames: []string{"wrong-name"},
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
},
{
name: "specific rules, subresource equivalent match, prefer extensions and name match",
criteria: &v1alpha1.MatchResources{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
ResourceNames: []string{"name"},
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"autoscaling", "v1", "Scale"},
},
{
name: "specific rules, subresource equivalent match, prefer extensions and name match miss",
criteria: &v1alpha1.MatchResources{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
ResourceNames: []string{"wrong-name"},
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
}}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
},
{
name: "exclude resource match on miss",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
},
}},
ExcludeResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}},
},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
expectMatchKind: &schema.GroupVersionKind{"autoscaling", "v1", "Scale"},
},
{
name: "exclude resource miss on match",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
},
}},
ExcludeResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}},
},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
},
{
name: "treat empty ResourceRules as match",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ExcludeResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
},
}},
},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: true,
},
{
name: "treat non-empty ResourceRules as no match",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{}},
},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
},
{
name: "erroring namespace selector on otherwise non-matching rule doesn't error",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "key ", Operator: "In", Values: []string{"bad value"}}}},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"deployments"}},
Operations: []v1alpha1.OperationType{"*"},
},
}},
},
attrs: admission.NewAttributesRecord(&example.Pod{}, nil, schema.GroupVersionKind{"example.apiserver.k8s.io", "v1", "Pod"}, "ns", "name", schema.GroupVersionResource{"example.apiserver.k8s.io", "v1", "pods"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
expectErr: "",
},
{
name: "erroring namespace selector on otherwise matching rule errors",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "key", Operator: "In", Values: []string{"bad value"}}}},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"pods"}},
Operations: []v1alpha1.OperationType{"*"},
},
}},
},
attrs: admission.NewAttributesRecord(&example.Pod{}, nil, schema.GroupVersionKind{"example.apiserver.k8s.io", "v1", "Pod"}, "ns", "name", schema.GroupVersionResource{"example.apiserver.k8s.io", "v1", "pods"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
expectErr: "bad value",
},
{
name: "erroring object selector on otherwise non-matching rule doesn't error",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "key", Operator: "In", Values: []string{"bad value"}}}},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"deployments"}},
Operations: []v1alpha1.OperationType{"*"},
},
}},
},
attrs: admission.NewAttributesRecord(&example.Pod{}, nil, schema.GroupVersionKind{"example.apiserver.k8s.io", "v1", "Pod"}, "ns", "name", schema.GroupVersionResource{"example.apiserver.k8s.io", "v1", "pods"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
expectErr: "",
},
{
name: "erroring object selector on otherwise matching rule errors",
criteria: &v1alpha1.MatchResources{
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "key", Operator: "In", Values: []string{"bad value"}}}},
ResourceRules: []v1alpha1.NamedRuleWithOperations{{
RuleWithOperations: v1alpha1.RuleWithOperations{
Rule: v1alpha1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"pods"}},
Operations: []v1alpha1.OperationType{"*"},
},
}},
},
attrs: admission.NewAttributesRecord(&example.Pod{}, nil, schema.GroupVersionKind{"example.apiserver.k8s.io", "v1", "Pod"}, "ns", "name", schema.GroupVersionResource{"example.apiserver.k8s.io", "v1", "pods"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectMatches: false,
expectErr: "bad value",
},
}
for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
matches, matchKind, err := a.Matches(testcase.attrs, interfaces, &fakeCriteria{matchResources: *testcase.criteria})
if err != nil {
if len(testcase.expectErr) == 0 {
t.Fatal(err)
}
if !strings.Contains(err.Error(), testcase.expectErr) {
t.Fatalf("expected error containing %q, got %s", testcase.expectErr, err.Error())
}
return
} else if len(testcase.expectErr) > 0 {
t.Fatalf("expected error %q, got no error", testcase.expectErr)
}
if testcase.expectMatchKind != nil {
if *testcase.expectMatchKind != matchKind {
t.Fatalf("expected matchKind %v, got %v", testcase.expectMatchKind, matchKind)
}
}
if matches != testcase.expectMatches {
t.Fatalf("expected matches = %v; got %v", testcase.expectMatches, matches)
}
})
}
}
type fakeNamespaceLister struct {
namespaces map[string]*corev1.Namespace
}
func (f fakeNamespaceLister) List(selector labels.Selector) (ret []*corev1.Namespace, err error) {
return nil, nil
}
func (f fakeNamespaceLister) Get(name string) (*corev1.Namespace, error) {
ns, ok := f.namespaces[name]
if ok {
return ns, nil
}
return nil, errors.NewNotFound(corev1.Resource("namespaces"), name)
}
func BenchmarkMatcher(b *testing.B) {
allScopes := v1.AllScopes
equivalentMatch := v1alpha1.Equivalent
namespace1Labels := map[string]string{"ns": "ns1"}
namespace1 := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "ns1",
Labels: namespace1Labels,
},
}
namespaceLister := fakeNamespaceLister{map[string]*corev1.Namespace{"ns": &namespace1}}
mapper := runtime.NewEquivalentResourceRegistryWithIdentity(func(resource schema.GroupResource) string {
if resource.Resource == "deployments" {
// co-locate deployments in all API groups
return "/deployments"
}
return ""
})
mapper.RegisterKindFor(schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "", schema.GroupVersionKind{"extensions", "v1beta1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1beta1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1alpha1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "scale", schema.GroupVersionKind{"extensions", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", schema.GroupVersionKind{"autoscaling", "v1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "deployments"}, "scale", schema.GroupVersionKind{"apps", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha1", "deployments"}, "scale", schema.GroupVersionKind{"apps", "v1alpha1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "statefulset"}, "", schema.GroupVersionKind{"apps", "v1", "StatefulSet"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "statefulset"}, "", schema.GroupVersionKind{"apps", "v1beta1", "StatefulSet"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta2", "statefulset"}, "", schema.GroupVersionKind{"apps", "v1beta2", "StatefulSet"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "statefulset"}, "scale", schema.GroupVersionKind{"apps", "v1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "statefulset"}, "scale", schema.GroupVersionKind{"apps", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha2", "statefulset"}, "scale", schema.GroupVersionKind{"apps", "v1beta2", "Scale"})
nsSelector := make(map[string]string)
for i := 0; i < 100; i++ {
nsSelector[fmt.Sprintf("key-%d", i)] = fmt.Sprintf("val-%d", i)
}
mr := v1alpha1.MatchResources{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{MatchLabels: nsSelector},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
},
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
},
},
},
}
criteria := &fakeCriteria{matchResources: mr}
attrs := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
matcher := &Matcher{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
for i := 0; i < b.N; i++ {
matcher.Matches(attrs, interfaces, criteria)
}
}
func BenchmarkShouldCallHookWithComplexRule(b *testing.B) {
allScopes := v1.AllScopes
equivalentMatch := v1alpha1.Equivalent
namespace1Labels := map[string]string{"ns": "ns1"}
namespace1 := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "ns1",
Labels: namespace1Labels,
},
}
namespaceLister := fakeNamespaceLister{map[string]*corev1.Namespace{"ns": &namespace1}}
mapper := runtime.NewEquivalentResourceRegistryWithIdentity(func(resource schema.GroupResource) string {
if resource.Resource == "deployments" {
// co-locate deployments in all API groups
return "/deployments"
}
return ""
})
mapper.RegisterKindFor(schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "", schema.GroupVersionKind{"extensions", "v1beta1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1beta1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1alpha1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "scale", schema.GroupVersionKind{"extensions", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", schema.GroupVersionKind{"autoscaling", "v1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "deployments"}, "scale", schema.GroupVersionKind{"apps", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha1", "deployments"}, "scale", schema.GroupVersionKind{"apps", "v1alpha1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "statefulset"}, "", schema.GroupVersionKind{"apps", "v1", "StatefulSet"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "statefulset"}, "", schema.GroupVersionKind{"apps", "v1beta1", "StatefulSet"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta2", "statefulset"}, "", schema.GroupVersionKind{"apps", "v1beta2", "StatefulSet"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "statefulset"}, "scale", schema.GroupVersionKind{"apps", "v1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "statefulset"}, "scale", schema.GroupVersionKind{"apps", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha2", "statefulset"}, "scale", schema.GroupVersionKind{"apps", "v1beta2", "Scale"})
mr := v1alpha1.MatchResources{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{},
}
for i := 0; i < 100; i++ {
rule := v1alpha1.NamedRuleWithOperations{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{
APIGroups: []string{fmt.Sprintf("app-%d", i)},
APIVersions: []string{fmt.Sprintf("v%d", i)},
Resources: []string{fmt.Sprintf("resource%d", i), fmt.Sprintf("resource%d/scale", i)},
Scope: &allScopes,
},
},
}
mr.ResourceRules = append(mr.ResourceRules, rule)
}
criteria := &fakeCriteria{matchResources: mr}
attrs := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
matcher := &Matcher{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
for i := 0; i < b.N; i++ {
matcher.Matches(attrs, interfaces, criteria)
}
}
func BenchmarkShouldCallHookWithComplexSelectorAndRule(b *testing.B) {
allScopes := v1.AllScopes
equivalentMatch := v1alpha1.Equivalent
namespace1Labels := map[string]string{"ns": "ns1"}
namespace1 := corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "ns1",
Labels: namespace1Labels,
},
}
namespaceLister := fakeNamespaceLister{map[string]*corev1.Namespace{"ns": &namespace1}}
mapper := runtime.NewEquivalentResourceRegistryWithIdentity(func(resource schema.GroupResource) string {
if resource.Resource == "deployments" {
// co-locate deployments in all API groups
return "/deployments"
}
return ""
})
mapper.RegisterKindFor(schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "", schema.GroupVersionKind{"extensions", "v1beta1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1beta1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha1", "deployments"}, "", schema.GroupVersionKind{"apps", "v1alpha1", "Deployment"})
mapper.RegisterKindFor(schema.GroupVersionResource{"extensions", "v1beta1", "deployments"}, "scale", schema.GroupVersionKind{"extensions", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", schema.GroupVersionKind{"autoscaling", "v1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "deployments"}, "scale", schema.GroupVersionKind{"apps", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha1", "deployments"}, "scale", schema.GroupVersionKind{"apps", "v1alpha1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "statefulset"}, "", schema.GroupVersionKind{"apps", "v1", "StatefulSet"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "statefulset"}, "", schema.GroupVersionKind{"apps", "v1beta1", "StatefulSet"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta2", "statefulset"}, "", schema.GroupVersionKind{"apps", "v1beta2", "StatefulSet"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1", "statefulset"}, "scale", schema.GroupVersionKind{"apps", "v1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1beta1", "statefulset"}, "scale", schema.GroupVersionKind{"apps", "v1beta1", "Scale"})
mapper.RegisterKindFor(schema.GroupVersionResource{"apps", "v1alpha2", "statefulset"}, "scale", schema.GroupVersionKind{"apps", "v1beta2", "Scale"})
nsSelector := make(map[string]string)
for i := 0; i < 100; i++ {
nsSelector[fmt.Sprintf("key-%d", i)] = fmt.Sprintf("val-%d", i)
}
mr := v1alpha1.MatchResources{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{MatchLabels: nsSelector},
ObjectSelector: &metav1.LabelSelector{},
ResourceRules: []v1alpha1.NamedRuleWithOperations{},
}
for i := 0; i < 100; i++ {
rule := v1alpha1.NamedRuleWithOperations{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"*"},
Rule: v1.Rule{
APIGroups: []string{fmt.Sprintf("app-%d", i)},
APIVersions: []string{fmt.Sprintf("v%d", i)},
Resources: []string{fmt.Sprintf("resource%d", i), fmt.Sprintf("resource%d/scale", i)},
Scope: &allScopes,
},
},
}
mr.ResourceRules = append(mr.ResourceRules, rule)
}
criteria := &fakeCriteria{matchResources: mr}
attrs := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"autoscaling", "v1", "Scale"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "scale", admission.Create, &metav1.CreateOptions{}, false, nil)
interfaces := &admission.RuntimeObjectInterfaces{EquivalentResourceMapper: mapper}
matcher := &Matcher{namespaceMatcher: &namespace.Matcher{NamespaceLister: namespaceLister}, objectMatcher: &object.Matcher{}}
for i := 0; i < b.N; i++ {
matcher.Matches(attrs, interfaces, criteria)
}
}

View File

@ -17,37 +17,43 @@ limitations under the License.
package cel
import (
"encoding/json"
"fmt"
"net/http"
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type PolicyDecisionKind string
type policyDecisionKind string
const (
Admit PolicyDecisionKind = "Admit"
Deny PolicyDecisionKind = "Deny"
admit policyDecisionKind = "admit"
deny policyDecisionKind = "deny"
)
type PolicyDecision struct {
Kind PolicyDecisionKind `json:"kind"`
Message any `json:"message"`
type policyDecision struct {
kind policyDecisionKind
message string
reason metav1.StatusReason
}
type PolicyDecisionWithMetadata struct {
PolicyDecision `json:"decision"`
Definition PolicyDefinition `json:"definition"`
Binding PolicyBinding `json:"binding"`
type policyDecisionWithMetadata struct {
policyDecision
definition *v1alpha1.ValidatingAdmissionPolicy
binding *v1alpha1.ValidatingAdmissionPolicyBinding
}
type PolicyError struct {
Decisions []PolicyDecisionWithMetadata
}
func (p *PolicyError) Error() string {
// Just format the error as JSON
jsonText, err := json.Marshal(p.Decisions)
if err != nil {
return fmt.Sprintf("error formatting PolicyError: %s", err.Error())
func reasonToCode(r metav1.StatusReason) int32 {
switch r {
case metav1.StatusReasonForbidden:
return http.StatusForbidden
case metav1.StatusReasonUnauthorized:
return http.StatusUnauthorized
case metav1.StatusReasonRequestEntityTooLarge:
return http.StatusRequestEntityTooLarge
case metav1.StatusReasonInvalid:
return http.StatusUnprocessableEntity
default:
// It should not reach here since we only allow above reason to be set from API level
return http.StatusUnprocessableEntity
}
return string(jsonText)
}

View File

@ -0,0 +1,310 @@
/*
Copyright 2022 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 cel
import (
"fmt"
"reflect"
"strings"
celtypes "github.com/google/cel-go/common/types"
"github.com/google/cel-go/interpreter"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/api/admissionregistration/v1alpha1"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel/matching"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
var _ ValidatorCompiler = &CELValidatorCompiler{}
var _ matching.MatchCriteria = &matchCriteria{}
type matchCriteria struct {
constraints *v1alpha1.MatchResources
}
// GetParsedNamespaceSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedNamespaceSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.NamespaceSelector)
}
// GetParsedObjectSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedObjectSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.ObjectSelector)
}
// GetMatchResources returns the matchConstraints
func (m *matchCriteria) GetMatchResources() v1alpha1.MatchResources {
return *m.constraints
}
// CELValidatorCompiler implement the interface ValidatorCompiler.
type CELValidatorCompiler struct {
Matcher *matching.Matcher
}
// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request
func (c *CELValidatorCompiler) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) {
criteria := matchCriteria{constraints: definition.Spec.MatchConstraints}
return c.Matcher.Matches(a, o, &criteria)
}
// BindingMatches returns whether this ValidatingAdmissionPolicyBinding matches the provided admission resource request
func (c *CELValidatorCompiler) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) {
if binding.Spec.MatchResources == nil {
return true, nil
}
criteria := matchCriteria{constraints: binding.Spec.MatchResources}
isMatch, _, err := c.Matcher.Matches(a, o, &criteria)
return isMatch, err
}
// ValidateInitialization checks if Matcher is initialized.
func (c *CELValidatorCompiler) ValidateInitialization() error {
return c.Matcher.ValidateInitialization()
}
type validationActivation struct {
object, oldObject, params, request interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *validationActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case ObjectVarName:
return a.object, true
case OldObjectVarName:
return a.oldObject, true
case ParamsVarName:
return a.params, true
case RequestVarName:
return a.request, true
default:
return nil, false
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *validationActivation) Parent() interpreter.Activation {
return nil
}
// Compile compiles the cel expression defined in ValidatingAdmissionPolicy
func (c *CELValidatorCompiler) Compile(p *v1alpha1.ValidatingAdmissionPolicy) Validator {
if len(p.Spec.Validations) == 0 {
return nil
}
hasParam := false
if p.Spec.ParamKind != nil {
hasParam = true
}
compilationResults := make([]CompilationResult, len(p.Spec.Validations))
for i, validation := range p.Spec.Validations {
compilationResults[i] = CompileValidatingPolicyExpression(validation.Expression, hasParam)
}
return &CELValidator{policy: p, compilationResults: compilationResults}
}
// CELValidator implements the Validator interface
type CELValidator struct {
policy *v1alpha1.ValidatingAdmissionPolicy
compilationResults []CompilationResult
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func objectToResolveVal(r runtime.Object) (interface{}, error) {
if r == nil {
return nil, nil
}
v, err := convertObjectToUnstructured(r)
if err != nil {
return nil, err
}
return v.Object, nil
}
func policyDecisionKindForError(f v1alpha1.FailurePolicyType) policyDecisionKind {
if f == v1alpha1.Ignore {
return admit
}
return deny
}
// Validate validates all cel expressions in Validator and returns a PolicyDecision for each CEL expression or returns an error.
// An error will be returned if failed to convert the object/oldObject/params/request to unstructured.
// Each PolicyDecision will have a decision and a message.
// policyDecision.message will be empty if the decision is allowed and no error met.
func (v *CELValidator) Validate(a admission.Attributes, o admission.ObjectInterfaces, versionedParams runtime.Object, matchKind schema.GroupVersionKind) ([]policyDecision, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
decisions := make([]policyDecision, len(v.compilationResults))
var err error
versionedAttr, err := generic.NewVersionedAttributes(a, matchKind, o)
if err != nil {
return nil, err
}
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, err
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, err
}
paramsVal, err := objectToResolveVal(versionedParams)
if err != nil {
return nil, err
}
request := createAdmissionRequest(versionedAttr.Attributes)
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, err
}
va := &validationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
}
var f v1alpha1.FailurePolicyType
if v.policy.Spec.FailurePolicy == nil {
f = v1alpha1.Fail
} else {
f = *v.policy.Spec.FailurePolicy
}
for i, compilationResult := range v.compilationResults {
validation := v.policy.Spec.Validations[i]
var policyDecision = &decisions[i]
if compilationResult.Error != nil {
policyDecision.kind = policyDecisionKindForError(f)
policyDecision.message = fmt.Sprintf("compilation error: %v", compilationResult.Error)
continue
}
if compilationResult.Program == nil {
policyDecision.kind = policyDecisionKindForError(f)
policyDecision.message = "unexpected internal error compiling expression"
continue
}
evalResult, _, err := compilationResult.Program.Eval(va)
if err != nil {
policyDecision.kind = policyDecisionKindForError(f)
policyDecision.message = fmt.Sprintf("expression '%v' resulted in error: %v", v.policy.Spec.Validations[i].Expression, err)
} else if evalResult != celtypes.True {
policyDecision.kind = deny
if validation.Reason == nil {
policyDecision.reason = metav1.StatusReasonInvalid
} else {
policyDecision.reason = *validation.Reason
}
if len(validation.Message) > 0 {
policyDecision.message = strings.TrimSpace(validation.Message)
} else {
policyDecision.message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
}
} else {
policyDecision.kind = admit
}
}
return decisions, nil
}
func createAdmissionRequest(attr admission.Attributes) *admissionv1.AdmissionRequest {
// FIXME: how to get resource GVK, GVR and subresource?
gvk := attr.GetKind()
gvr := attr.GetResource()
subresource := attr.GetSubresource()
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
var userInfo authenticationv1.UserInfo
if aUserInfo != nil {
userInfo = authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
}
dryRun := attr.IsDryRun()
return &admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
// Leave Object and OldObject unset since we don't provide access to them via request
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
}
}

View File

@ -0,0 +1,572 @@
/*
Copyright 2022 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 cel
import (
"strings"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
)
func TestCompile(t *testing.T) {
cases := []struct {
name string
policy *v1alpha1.ValidatingAdmissionPolicy
errorExpressions map[string]string
}{
{
name: "invalid syntax",
policy: &v1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1alpha1.ValidatingAdmissionPolicySpec{
FailurePolicy: func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Fail")
return &r
}(),
ParamKind: &v1alpha1.ParamKind{
APIVersion: "rules.example.com/v1",
Kind: "ReplicaLimit",
},
Validations: []v1alpha1.Validation{
{
Expression: "1 < 'asdf'",
},
{
Expression: "1 < 2",
},
},
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: func() *v1alpha1.MatchPolicyType {
r := v1alpha1.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"CREATE"},
Rule: v1.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
},
errorExpressions: map[string]string{
"1 < 'asdf'": "found no matching overload for '_<_' applied to '(int, string)",
},
},
{
name: "valid syntax",
policy: &v1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1alpha1.ValidatingAdmissionPolicySpec{
FailurePolicy: func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Fail")
return &r
}(),
Validations: []v1alpha1.Validation{
{
Expression: "1 < 2",
},
{
Expression: "object.spec.string.matches('[0-9]+')",
},
{
Expression: "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'",
},
},
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: func() *v1alpha1.MatchPolicyType {
r := v1alpha1.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"CREATE"},
Rule: v1.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var c CELValidatorCompiler
validator := c.Compile(tc.policy)
if validator == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.policy.Spec.Validations
CompilationResults := validator.(*CELValidator).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
meets := make([]bool, len(validations))
for expr, expectErr := range tc.errorExpressions {
for i, result := range CompilationResults {
if validations[i].Expression == expr {
if result.Error == nil {
t.Errorf("Expect expression '%s' to contain error '%v' but got no error", expr, expectErr)
} else if !strings.Contains(result.Error.Error(), expectErr) {
t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
}
meets[i] = true
}
}
}
for i, meet := range meets {
if !meet && CompilationResults[i].Error != nil {
t.Errorf("Unexpected err '%v' for expression '%s'", CompilationResults[i].Error, validations[i].Expression)
}
}
})
}
}
func getValidPolicy(validations []v1alpha1.Validation, params *v1alpha1.ParamKind, fp *v1alpha1.FailurePolicyType) *v1alpha1.ValidatingAdmissionPolicy {
if fp == nil {
fp = func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Fail")
return &r
}()
}
return &v1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1alpha1.ValidatingAdmissionPolicySpec{
FailurePolicy: fp,
Validations: validations,
ParamKind: params,
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: func() *v1alpha1.MatchPolicyType {
r := v1alpha1.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"CREATE"},
Rule: v1.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
}
}
func generatedDecision(k policyDecisionKind, m string, r metav1.StatusReason) policyDecision {
return policyDecision{kind: k, message: m, reason: r}
}
func TestValidate(t *testing.T) {
// we fake the paramKind in ValidatingAdmissionPolicy for testing since the params is directly passed from cel admission
// Inside validator.go, we only check if paramKind exists
hasParamKind := &v1alpha1.ParamKind{
APIVersion: "v1",
Kind: "ConfigMap",
}
ignorePolicy := func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Ignore")
return &r
}()
forbiddenReason := func() *metav1.StatusReason {
r := metav1.StatusReasonForbidden
return &r
}()
configMapParams := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Data: map[string]string{
"fakeString": "fake",
},
}
crdParams := &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
"testSize": 10,
},
},
}
podObject := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: corev1.PodSpec{
NodeName: "testnode",
},
}
cases := []struct {
name string
policy *v1alpha1.ValidatingAdmissionPolicy
attributes admission.Attributes
params runtime.Object
policyDecisions []policyDecision
}{
{
name: "valid syntax for object",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "valid syntax for metadata",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "object.metadata.name == 'endpoints1'",
},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "valid syntax for oldObject",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject == null",
},
{
Expression: "object != null",
},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
generatedDecision(admit, "", ""),
},
},
{
name: "valid syntax for request",
policy: getValidPolicy([]v1alpha1.Validation{
{Expression: "request.operation == 'CREATE'"},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "valid syntax for configMap",
policy: getValidPolicy([]v1alpha1.Validation{
{Expression: "request.namespace != params.data.fakeString"},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "test failure policy with Ignore",
policy: getValidPolicy([]v1alpha1.Validation{
{Expression: "object.subsets.size() > 2"},
}, hasParamKind, ignorePolicy),
attributes: newValidAttribute(nil, false),
params: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Data: map[string]string{
"fakeString": "fake",
},
},
policyDecisions: []policyDecision{
generatedDecision(deny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid),
},
},
{
name: "test failure policy with multiple validations",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "has(object.subsets)",
},
{
Expression: "object.subsets.size() > 2",
},
}, hasParamKind, ignorePolicy),
attributes: newValidAttribute(nil, false),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
generatedDecision(deny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid),
},
},
{
name: "test failure policy with multiple failed validations",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject != null",
},
{
Expression: "object.subsets.size() > 2",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "failed expression: oldObject != null", metav1.StatusReasonInvalid),
generatedDecision(deny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid),
},
},
{
name: "test Object nul in delete",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject != null",
},
{
Expression: "object == null",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
generatedDecision(admit, "", ""),
},
},
{
name: "test reason for failed validation",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject == null",
Reason: forbiddenReason,
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "failed expression: oldObject == null", metav1.StatusReasonForbidden),
},
},
{
name: "test message for failed validation",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject == null",
Reason: forbiddenReason,
Message: "old object should be present",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "old object should be present", metav1.StatusReasonForbidden),
},
},
{
name: "test runtime error",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "oldObject.x == 100",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "resulted in error", ""),
},
},
{
name: "test against crd param",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "object.subsets.size() < params.spec.testSize",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: crdParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
{
name: "test compile failure with FailurePolicy Fail",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "fail to compile test",
},
{
Expression: "object.subsets.size() > params.spec.testSize",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: crdParams,
policyDecisions: []policyDecision{
generatedDecision(deny, "compilation error: compilation failed: ERROR: <input>:1:6: Syntax error:", ""),
generatedDecision(deny, "failed expression: object.subsets.size() > params.spec.testSize", metav1.StatusReasonInvalid),
},
},
{
name: "test compile failure with FailurePolicy Ignore",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "fail to compile test",
},
{
Expression: "object.subsets.size() > params.spec.testSize",
},
}, hasParamKind, ignorePolicy),
attributes: newValidAttribute(nil, false),
params: crdParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "compilation error: compilation failed: ERROR:", ""),
generatedDecision(deny, "failed expression: object.subsets.size() > params.spec.testSize", metav1.StatusReasonInvalid),
},
},
{
name: "test pod",
policy: getValidPolicy([]v1alpha1.Validation{
{
Expression: "object.spec.nodeName == 'testnode'",
},
}, nil, nil),
attributes: newValidAttribute(&podObject, false),
params: crdParams,
policyDecisions: []policyDecision{
generatedDecision(admit, "", ""),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := CELValidatorCompiler{}
validator := c.Compile(tc.policy)
if validator == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.policy.Spec.Validations
CompilationResults := validator.(*CELValidator).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
policyResults, err := validator.Validate(tc.attributes, newObjectInterfacesForTest(), tc.params, tc.attributes.GetKind())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
require.Equal(t, len(policyResults), len(tc.policyDecisions))
for i, policyDecision := range tc.policyDecisions {
if policyDecision.kind != policyResults[i].kind {
t.Errorf("Expected policy decision kind '%v' but got '%v'", policyDecision.kind, policyResults[i].kind)
}
if !strings.Contains(policyResults[i].message, policyDecision.message) {
t.Errorf("Expected policy decision message contains '%v' but got '%v'", policyDecision.message, policyResults[i].message)
}
if policyDecision.reason != policyResults[i].reason {
t.Errorf("Expected policy decision reason '%v' but got '%v'", policyDecision.reason, policyResults[i].reason)
}
}
})
}
}
// newObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file.
func newObjectInterfacesForTest() admission.ObjectInterfaces {
scheme := runtime.NewScheme()
corev1.AddToScheme(scheme)
return admission.NewObjectInterfacesFromScheme(scheme)
}
func newValidAttribute(object runtime.Object, isDelete bool) admission.Attributes {
var oldObject runtime.Object
if !isDelete {
if object == nil {
object = &corev1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "endpoints1",
},
Subsets: []corev1.EndpointSubset{
{
Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}},
},
},
}
}
} else {
object = nil
oldObject = &corev1.Endpoints{
Subsets: []corev1.EndpointSubset{
{
Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}},
},
},
}
}
return admission.NewAttributesRecord(object, oldObject, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil)
}

View File

@ -53,7 +53,7 @@ func newHandlerForTestWithClock(c clientset.Interface, cacheClock clock.Clock) (
if err != nil {
return nil, f, err
}
pluginInitializer := kubeadmission.New(c, f, nil, nil, nil)
pluginInitializer := kubeadmission.New(c, nil, f, nil, nil, nil)
pluginInitializer.Initialize(handler)
err = admission.ValidateInitialization(handler)
return handler, f, err

View File

@ -219,7 +219,7 @@ func (a *attrWithResourceOverride) GetResource() schema.GroupVersionResource { r
// Dispatch is called by the downstream Validate or Admit methods.
func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
if rules.IsWebhookConfigurationResource(attr) {
if rules.IsExemptAdmissionConfigurationResource(attr) {
return nil
}
if !a.WaitForReady() {

View File

@ -116,12 +116,12 @@ func (r *Matcher) resource() bool {
return false
}
// IsWebhookConfigurationResource determines if an admission.Attributes object is describing
// the admission of a ValidatingWebhookConfiguration or a MutatingWebhookConfiguration
func IsWebhookConfigurationResource(attr admission.Attributes) bool {
// IsExemptAdmissionConfigurationResource determines if an admission.Attributes object is describing
// the admission of a ValidatingWebhookConfiguration or a MutatingWebhookConfiguration or a ValidatingAdmissionPolicy or a ValidatingAdmissionPolicyBinding
func IsExemptAdmissionConfigurationResource(attr admission.Attributes) bool {
gvk := attr.GetKind()
if gvk.Group == "admissionregistration.k8s.io" {
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" {
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" {
return true
}
}

View File

@ -28,6 +28,7 @@ import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
@ -35,6 +36,7 @@ import (
apiserverapiv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
apiserverapiv1alpha1 "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
"k8s.io/apiserver/pkg/server"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
@ -85,7 +87,7 @@ func NewAdmissionOptions() *AdmissionOptions {
// admission plugins. The apiserver always runs the validating ones
// after all the mutating ones, so their relative order in this list
// doesn't matter.
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, validatingwebhook.PluginName},
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, cel.PluginName, validatingwebhook.PluginName},
DefaultOffPlugins: sets.NewString(),
}
server.RegisterAllAdmissionPlugins(options.Plugins)
@ -145,7 +147,11 @@ func (a *AdmissionOptions) ApplyTo(
if err != nil {
return err
}
genericInitializer := initializer.New(clientset, informers, c.Authorization.Authorizer, features, c.DrainedNotify())
dynamicClient, err := dynamic.NewForConfig(kubeAPIServerClientConfig)
if err != nil {
return err
}
genericInitializer := initializer.New(clientset, dynamicClient, informers, c.Authorization.Authorizer, features, c.DrainedNotify())
initializersChain := admission.PluginInitializers{genericInitializer}
initializersChain = append(initializersChain, pluginInitializers...)

View File

@ -36,7 +36,7 @@ func TestEnabledPluginNames(t *testing.T) {
}{
// scenario 0: check if a call to enabledPluginNames sets expected values.
{
expectedPluginNames: []string{"NamespaceLifecycle", "MutatingAdmissionWebhook", "ValidatingAdmissionWebhook"},
expectedPluginNames: []string{"NamespaceLifecycle", "MutatingAdmissionWebhook", "ValidatingAdmissionPolicy", "ValidatingAdmissionWebhook"},
},
// scenario 1: use default off plugins if no specified

View File

@ -19,6 +19,7 @@ package server
// This file exists to force the desired plugin implementations to be linked into genericapi pkg.
import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
@ -29,4 +30,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
lifecycle.Register(plugins)
validatingwebhook.Register(plugins)
mutatingwebhook.Register(plugins)
cel.Register(plugins)
}