Merge pull request #113314 from cici37/celIntegration
CEL validation in Admission chain Kubernetes-commit: 595ea324113580ae61f4a15ab3e5b22303a195cf
This commit is contained in:
commit
aa0e1e5e62
8
go.mod
8
go.mod
|
|
@ -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
8
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] = ¶mInfo{
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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{})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue