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
|
google.golang.org/protobuf v1.28.1
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/square/go-jose.v2 v2.2.2
|
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/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/component-base v0.0.0-20221108061007-abdc0eb56a1d
|
||||||
k8s.io/klog/v2 v2.80.1
|
k8s.io/klog/v2 v2.80.1
|
||||||
k8s.io/kms v0.0.0-20221028080743-a9ba1c11c0c6
|
k8s.io/kms v0.0.0-20221028080743-a9ba1c11c0c6
|
||||||
|
|
@ -122,9 +122,9 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
replace (
|
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/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/component-base => k8s.io/component-base v0.0.0-20221108061007-abdc0eb56a1d
|
||||||
k8s.io/kms => k8s.io/kms v0.0.0-20221028080743-a9ba1c11c0c6
|
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-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.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/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-20221108053748-98c1aa6b3d0a h1:GaCla9HtNyi63kysI/cyeA4bv6wRkIyuiUeXpaTF+dw=
|
||||||
k8s.io/api v0.0.0-20221108053747-3f61c95cab71/go.mod h1:PSXY9/fSNyKgKHUU+O9scnZiW8m+V1znqk49oI6hAEY=
|
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 h1:zX/AC2CNrYwngyVHVHcsCL36uUtC/3tiZOE6/gfojvc=
|
||||||
k8s.io/apimachinery v0.0.0-20221108052757-4fe4321a9d5e/go.mod h1:VXMmlsE7YRJ5vyAyWpkKIfFkEbDNpVs0ObpkuQf1WfM=
|
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-20221108054913-4b1a9fdfb58c h1:73hBKeNTruSueGRemXkcxxOJ2OV58wJvfdo0+ZMkucA=
|
||||||
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/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 h1:IF51IULm49B+VT4PJOBd4z/SxtU0WVfInKrnykCG/t0=
|
||||||
k8s.io/component-base v0.0.0-20221108061007-abdc0eb56a1d/go.mod h1:yN3pjWt18ANoCwZlZZaB+9OdFe2a6rgTUE51kThAe3Q=
|
k8s.io/component-base v0.0.0-20221108061007-abdc0eb56a1d/go.mod h1:yN3pjWt18ANoCwZlZZaB+9OdFe2a6rgTUE51kThAe3Q=
|
||||||
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
|
k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4=
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package initializer
|
||||||
import (
|
import (
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/component-base/featuregate"
|
"k8s.io/component-base/featuregate"
|
||||||
|
|
@ -26,6 +27,7 @@ import (
|
||||||
|
|
||||||
type pluginInitializer struct {
|
type pluginInitializer struct {
|
||||||
externalClient kubernetes.Interface
|
externalClient kubernetes.Interface
|
||||||
|
dynamicClient dynamic.Interface
|
||||||
externalInformers informers.SharedInformerFactory
|
externalInformers informers.SharedInformerFactory
|
||||||
authorizer authorizer.Authorizer
|
authorizer authorizer.Authorizer
|
||||||
featureGates featuregate.FeatureGate
|
featureGates featuregate.FeatureGate
|
||||||
|
|
@ -37,6 +39,7 @@ type pluginInitializer struct {
|
||||||
// during compilation when they update a level.
|
// during compilation when they update a level.
|
||||||
func New(
|
func New(
|
||||||
extClientset kubernetes.Interface,
|
extClientset kubernetes.Interface,
|
||||||
|
dynamicClient dynamic.Interface,
|
||||||
extInformers informers.SharedInformerFactory,
|
extInformers informers.SharedInformerFactory,
|
||||||
authz authorizer.Authorizer,
|
authz authorizer.Authorizer,
|
||||||
featureGates featuregate.FeatureGate,
|
featureGates featuregate.FeatureGate,
|
||||||
|
|
@ -44,6 +47,7 @@ func New(
|
||||||
) pluginInitializer {
|
) pluginInitializer {
|
||||||
return pluginInitializer{
|
return pluginInitializer{
|
||||||
externalClient: extClientset,
|
externalClient: extClientset,
|
||||||
|
dynamicClient: dynamicClient,
|
||||||
externalInformers: extInformers,
|
externalInformers: extInformers,
|
||||||
authorizer: authz,
|
authorizer: authz,
|
||||||
featureGates: featureGates,
|
featureGates: featureGates,
|
||||||
|
|
@ -68,6 +72,10 @@ func (i pluginInitializer) Initialize(plugin admission.Interface) {
|
||||||
wants.SetExternalKubeClientSet(i.externalClient)
|
wants.SetExternalKubeClientSet(i.externalClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wants, ok := plugin.(WantsDynamicClient); ok {
|
||||||
|
wants.SetDynamicClient(i.dynamicClient)
|
||||||
|
}
|
||||||
|
|
||||||
if wants, ok := plugin.(WantsExternalKubeInformerFactory); ok {
|
if wants, ok := plugin.(WantsExternalKubeInformerFactory); ok {
|
||||||
wants.SetExternalKubeInformerFactory(i.externalInformers)
|
wants.SetExternalKubeInformerFactory(i.externalInformers)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import (
|
||||||
// TestWantsAuthorizer ensures that the authorizer is injected
|
// TestWantsAuthorizer ensures that the authorizer is injected
|
||||||
// when the WantsAuthorizer interface is implemented by a plugin.
|
// when the WantsAuthorizer interface is implemented by a plugin.
|
||||||
func TestWantsAuthorizer(t *testing.T) {
|
func TestWantsAuthorizer(t *testing.T) {
|
||||||
target := initializer.New(nil, nil, &TestAuthorizer{}, nil, nil)
|
target := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, nil)
|
||||||
wantAuthorizerAdmission := &WantAuthorizerAdmission{}
|
wantAuthorizerAdmission := &WantAuthorizerAdmission{}
|
||||||
target.Initialize(wantAuthorizerAdmission)
|
target.Initialize(wantAuthorizerAdmission)
|
||||||
if wantAuthorizerAdmission.auth == nil {
|
if wantAuthorizerAdmission.auth == nil {
|
||||||
|
|
@ -44,7 +44,7 @@ func TestWantsAuthorizer(t *testing.T) {
|
||||||
// when the WantsExternalKubeClientSet interface is implemented by a plugin.
|
// when the WantsExternalKubeClientSet interface is implemented by a plugin.
|
||||||
func TestWantsExternalKubeClientSet(t *testing.T) {
|
func TestWantsExternalKubeClientSet(t *testing.T) {
|
||||||
cs := &fake.Clientset{}
|
cs := &fake.Clientset{}
|
||||||
target := initializer.New(cs, nil, &TestAuthorizer{}, nil, nil)
|
target := initializer.New(cs, nil, nil, &TestAuthorizer{}, nil, nil)
|
||||||
wantExternalKubeClientSet := &WantExternalKubeClientSet{}
|
wantExternalKubeClientSet := &WantExternalKubeClientSet{}
|
||||||
target.Initialize(wantExternalKubeClientSet)
|
target.Initialize(wantExternalKubeClientSet)
|
||||||
if wantExternalKubeClientSet.cs != cs {
|
if wantExternalKubeClientSet.cs != cs {
|
||||||
|
|
@ -57,7 +57,7 @@ func TestWantsExternalKubeClientSet(t *testing.T) {
|
||||||
func TestWantsExternalKubeInformerFactory(t *testing.T) {
|
func TestWantsExternalKubeInformerFactory(t *testing.T) {
|
||||||
cs := &fake.Clientset{}
|
cs := &fake.Clientset{}
|
||||||
sf := informers.NewSharedInformerFactory(cs, time.Duration(1)*time.Second)
|
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{}
|
wantExternalKubeInformerFactory := &WantExternalKubeInformerFactory{}
|
||||||
target.Initialize(wantExternalKubeInformerFactory)
|
target.Initialize(wantExternalKubeInformerFactory)
|
||||||
if wantExternalKubeInformerFactory.sf != sf {
|
if wantExternalKubeInformerFactory.sf != sf {
|
||||||
|
|
@ -69,7 +69,7 @@ func TestWantsExternalKubeInformerFactory(t *testing.T) {
|
||||||
// when the WantsShutdownSignal interface is implemented by a plugin.
|
// when the WantsShutdownSignal interface is implemented by a plugin.
|
||||||
func TestWantsShutdownNotification(t *testing.T) {
|
func TestWantsShutdownNotification(t *testing.T) {
|
||||||
stopCh := make(chan struct{})
|
stopCh := make(chan struct{})
|
||||||
target := initializer.New(nil, nil, &TestAuthorizer{}, nil, stopCh)
|
target := initializer.New(nil, nil, nil, &TestAuthorizer{}, nil, stopCh)
|
||||||
wantDrainedNotification := &WantDrainedNotification{}
|
wantDrainedNotification := &WantDrainedNotification{}
|
||||||
target.Initialize(wantDrainedNotification)
|
target.Initialize(wantDrainedNotification)
|
||||||
if wantDrainedNotification.stopCh == nil {
|
if wantDrainedNotification.stopCh == nil {
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,11 @@ limitations under the License.
|
||||||
package initializer
|
package initializer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
quota "k8s.io/apiserver/pkg/quota/v1"
|
quota "k8s.io/apiserver/pkg/quota/v1"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/component-base/featuregate"
|
"k8s.io/component-base/featuregate"
|
||||||
|
|
@ -68,3 +70,14 @@ type WantsFeatures interface {
|
||||||
InspectFeatureGates(featuregate.FeatureGate)
|
InspectFeatureGates(featuregate.FeatureGate)
|
||||||
admission.InitializationValidator
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apiserver/pkg/features"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/component-base/featuregate"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"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"
|
"k8s.io/client-go/tools/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -38,7 +45,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// PluginName indicates the name of admission plug-in
|
// PluginName indicates the name of admission plug-in
|
||||||
PluginName = "CEL"
|
PluginName = "ValidatingAdmissionPolicy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Register registers a plugin
|
// Register registers a plugin
|
||||||
|
|
@ -54,28 +61,89 @@ func Register(plugins *admission.Plugins) {
|
||||||
|
|
||||||
type celAdmissionPlugin struct {
|
type celAdmissionPlugin struct {
|
||||||
evaluator CELPolicyEvaluator
|
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{}
|
var _ admission.ValidationInterface = &celAdmissionPlugin{}
|
||||||
|
|
||||||
func NewPlugin() (*celAdmissionPlugin, error) {
|
func NewPlugin() (admission.Interface, error) {
|
||||||
result := &celAdmissionPlugin{}
|
result := &celAdmissionPlugin{}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) SetCELPolicyEvaluator(evaluator CELPolicyEvaluator) {
|
func (c *celAdmissionPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||||
c.evaluator = evaluator
|
c.informerFactory = f
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once clientset and informer factory are provided, creates and starts the
|
func (c *celAdmissionPlugin) SetExternalKubeClientSet(client kubernetes.Interface) {
|
||||||
// admission controller
|
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 {
|
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
|
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,
|
a admission.Attributes,
|
||||||
o admission.ObjectInterfaces,
|
o admission.ObjectInterfaces,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
|
if !c.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
deadlined, cancel := context.WithTimeout(ctx, 2*time.Second)
|
deadlined, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||||
defer cancel()
|
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) {
|
if !cache.WaitForNamedCacheSync("cel-admission-plugin", deadlined.Done(), c.evaluator.HasSynced) {
|
||||||
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
return admission.NewForbidden(a, fmt.Errorf("not yet ready to handle request"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.evaluator.Validate(ctx, a, o)
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"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"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/cel/internal/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/cel/internal/generic"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/client-go/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ CELPolicyEvaluator = &celAdmissionController{}
|
||||||
|
|
||||||
// celAdmissionController is the top-level controller for admission control using CEL
|
// celAdmissionController is the top-level controller for admission control using CEL
|
||||||
// it is responsible for watching policy definitions, bindings, and config param CRDs
|
// it is responsible for watching policy definitions, bindings, and config param CRDs
|
||||||
type celAdmissionController struct {
|
type celAdmissionController struct {
|
||||||
// Context under which the controller runs
|
// Context under which the controller runs
|
||||||
runningContext context.Context
|
runningContext context.Context
|
||||||
|
|
||||||
policyDefinitionsController generic.Controller[PolicyDefinition]
|
policyDefinitionsController generic.Controller[*v1alpha1.ValidatingAdmissionPolicy]
|
||||||
policyBindingController generic.Controller[PolicyBinding]
|
policyBindingController generic.Controller[*v1alpha1.ValidatingAdmissionPolicyBinding]
|
||||||
|
|
||||||
// dynamicclient used to create informers to watch the param crd types
|
// dynamicclient used to create informers to watch the param crd types
|
||||||
dynamicClient dynamic.Interface
|
dynamicClient dynamic.Interface
|
||||||
|
|
@ -50,7 +57,7 @@ type celAdmissionController struct {
|
||||||
|
|
||||||
// Provided to the policy's Compile function as an injected dependency to
|
// Provided to the policy's Compile function as an injected dependency to
|
||||||
// assist with compiling its expressions to CEL
|
// assist with compiling its expressions to CEL
|
||||||
objectConverter ObjectConverter
|
validatorCompiler ValidatorCompiler
|
||||||
|
|
||||||
// Lock which protects:
|
// Lock which protects:
|
||||||
// - definitionInfo
|
// - definitionInfo
|
||||||
|
|
@ -61,21 +68,26 @@ type celAdmissionController struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
|
||||||
// controller and metadata
|
// controller and metadata
|
||||||
paramsCRDControllers map[schema.GroupVersionKind]*paramInfo
|
paramsCRDControllers map[v1alpha1.ParamKind]*paramInfo
|
||||||
|
|
||||||
// Index for each definition namespace/name, contains all binding
|
// Index for each definition namespace/name, contains all binding
|
||||||
// namespace/names known to exist for that definition
|
// 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
|
// Index for each bindings namespace/name. Contains compiled templates
|
||||||
// for the binding depending on the policy/param combination.
|
// 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
|
// Map from namespace/name of a definition to a set of namespace/name
|
||||||
// of bindings which depend on it.
|
// of bindings which depend on it.
|
||||||
// All keys must have at least one dependent binding
|
// All keys must have at least one dependent binding
|
||||||
// All binding names MUST exist as a key bindingInfos
|
// 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 {
|
type definitionInfo struct {
|
||||||
|
|
@ -86,16 +98,16 @@ type definitionInfo struct {
|
||||||
|
|
||||||
// Last value seen by this controller to be used in policy enforcement
|
// Last value seen by this controller to be used in policy enforcement
|
||||||
// May not be nil
|
// May not be nil
|
||||||
lastReconciledValue PolicyDefinition
|
lastReconciledValue *v1alpha1.ValidatingAdmissionPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
type bindingInfo struct {
|
type bindingInfo struct {
|
||||||
// Compiled CEL expression turned into an evaluator
|
// Compiled CEL expression turned into an validator
|
||||||
evaluator EvaluatorFunc
|
validator atomic.Pointer[Validator]
|
||||||
|
|
||||||
// Last value seen by this controller to be used in policy enforcement
|
// Last value seen by this controller to be used in policy enforcement
|
||||||
// May not be nil
|
// May not be nil
|
||||||
lastReconciledValue PolicyBinding
|
lastReconciledValue *v1alpha1.ValidatingAdmissionPolicyBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
type paramInfo struct {
|
type paramInfo struct {
|
||||||
|
|
@ -106,31 +118,33 @@ type paramInfo struct {
|
||||||
stop func()
|
stop func()
|
||||||
|
|
||||||
// Policy Definitions which refer to this param CRD
|
// Policy Definitions which refer to this param CRD
|
||||||
dependentDefinitions sets.String
|
dependentDefinitions sets.Set[namespacedName]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdmissionController(
|
func NewAdmissionController(
|
||||||
// Informers
|
|
||||||
policyDefinitionsInformer cache.SharedIndexInformer,
|
|
||||||
policyBindingInformer cache.SharedIndexInformer,
|
|
||||||
|
|
||||||
// Injected Dependencies
|
// Injected Dependencies
|
||||||
objectConverter ObjectConverter,
|
informerFactory informers.SharedInformerFactory,
|
||||||
|
client kubernetes.Interface,
|
||||||
restMapper meta.RESTMapper,
|
restMapper meta.RESTMapper,
|
||||||
dynamicClient dynamic.Interface,
|
dynamicClient dynamic.Interface,
|
||||||
) CELPolicyEvaluator {
|
) CELPolicyEvaluator {
|
||||||
|
matcher := matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)
|
||||||
|
validatorCompiler := &CELValidatorCompiler{
|
||||||
|
Matcher: matcher,
|
||||||
|
}
|
||||||
c := &celAdmissionController{
|
c := &celAdmissionController{
|
||||||
definitionInfo: make(map[string]*definitionInfo),
|
definitionInfo: make(map[namespacedName]*definitionInfo),
|
||||||
bindingInfos: make(map[string]*bindingInfo),
|
bindingInfos: make(map[namespacedName]*bindingInfo),
|
||||||
paramsCRDControllers: make(map[schema.GroupVersionKind]*paramInfo),
|
paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo),
|
||||||
definitionsToBindings: make(map[string]sets.String),
|
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
|
||||||
dynamicClient: dynamicClient,
|
dynamicClient: dynamicClient,
|
||||||
objectConverter: objectConverter,
|
validatorCompiler: validatorCompiler,
|
||||||
restMapper: restMapper,
|
restMapper: restMapper,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.policyDefinitionsController = generic.NewController(
|
c.policyDefinitionsController = generic.NewController(
|
||||||
generic.NewInformer[PolicyDefinition](policyDefinitionsInformer),
|
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy](
|
||||||
|
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
|
||||||
c.reconcilePolicyDefinition,
|
c.reconcilePolicyDefinition,
|
||||||
generic.ControllerOptions{
|
generic.ControllerOptions{
|
||||||
Workers: 1,
|
Workers: 1,
|
||||||
|
|
@ -138,7 +152,8 @@ func NewAdmissionController(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
c.policyBindingController = generic.NewController(
|
c.policyBindingController = generic.NewController(
|
||||||
generic.NewInformer[PolicyBinding](policyBindingInformer),
|
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
|
||||||
|
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()),
|
||||||
c.reconcilePolicyBinding,
|
c.reconcilePolicyBinding,
|
||||||
generic.ControllerOptions{
|
generic.ControllerOptions{
|
||||||
Workers: 1,
|
Workers: 1,
|
||||||
|
|
@ -187,30 +202,57 @@ func (c *celAdmissionController) Validate(
|
||||||
c.mutex.RLock()
|
c.mutex.RLock()
|
||||||
defer c.mutex.RUnlock()
|
defer c.mutex.RUnlock()
|
||||||
|
|
||||||
var allDecisions []PolicyDecisionWithMetadata = nil
|
var deniedDecisions []policyDecisionWithMetadata
|
||||||
|
|
||||||
addConfigError := func(err error, definition PolicyDefinition, binding PolicyBinding) {
|
addConfigError := func(err error, definition *v1alpha1.ValidatingAdmissionPolicy, binding *v1alpha1.ValidatingAdmissionPolicyBinding) {
|
||||||
wrappedError := fmt.Errorf("configuration error: %w", err)
|
// we always default the FailurePolicy if it is unset and validate it in API level
|
||||||
switch p := definition.GetFailurePolicy(); p {
|
var policy v1alpha1.FailurePolicyType
|
||||||
case Ignore:
|
if definition.Spec.FailurePolicy == nil {
|
||||||
klog.Info(wrappedError)
|
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
|
return
|
||||||
case Fail:
|
case v1alpha1.Fail:
|
||||||
allDecisions = append(allDecisions, PolicyDecisionWithMetadata{
|
var message string
|
||||||
PolicyDecision: PolicyDecision{
|
if binding == nil {
|
||||||
Kind: Deny,
|
message = fmt.Errorf("failed to configure policy: %w", err).Error()
|
||||||
Message: wrappedError.Error(),
|
} else {
|
||||||
|
message = fmt.Errorf("failed to configure binding: %w", err).Error()
|
||||||
|
}
|
||||||
|
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
|
||||||
|
policyDecision: policyDecision{
|
||||||
|
kind: deny,
|
||||||
|
message: message,
|
||||||
},
|
},
|
||||||
Definition: definition,
|
definition: definition,
|
||||||
Binding: binding,
|
binding: binding,
|
||||||
})
|
})
|
||||||
default:
|
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 {
|
for definitionNamespacedName, definitionInfo := range c.definitionInfo {
|
||||||
definition := definitionInfo.lastReconciledValue
|
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
|
// Policy definition does not match request
|
||||||
continue
|
continue
|
||||||
} else if definitionInfo.configurationError != nil {
|
} else if definitionInfo.configurationError != nil {
|
||||||
|
|
@ -221,8 +263,6 @@ func (c *celAdmissionController) Validate(
|
||||||
|
|
||||||
dependentBindings := c.definitionsToBindings[definitionNamespacedName]
|
dependentBindings := c.definitionsToBindings[definitionNamespacedName]
|
||||||
if len(dependentBindings) == 0 {
|
if len(dependentBindings) == 0 {
|
||||||
// Definition has no known bindings yet.
|
|
||||||
addConfigError(errors.New("no bindings found"), definition, nil)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,40 +271,61 @@ func (c *celAdmissionController) Validate(
|
||||||
// be a bindingInfo for it
|
// be a bindingInfo for it
|
||||||
bindingInfo := c.bindingInfos[namespacedBindingName]
|
bindingInfo := c.bindingInfos[namespacedBindingName]
|
||||||
binding := bindingInfo.lastReconciledValue
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var param *unstructured.Unstructured
|
var param *unstructured.Unstructured
|
||||||
|
|
||||||
// If definition has no paramsource, always provide nil params to
|
// If definition has paramKind, paramRef is required in binding.
|
||||||
// evaluator. If binding specifies a params to use they are ignored.
|
// If definition has no paramKind, paramRef set in binding will be ignored.
|
||||||
// Done this way so you can configure params before definition is ready.
|
paramKind := definition.Spec.ParamKind
|
||||||
if paramSource := definition.GetParamSource(); paramSource != nil {
|
paramRef := binding.Spec.ParamRef
|
||||||
paramsNamespace, paramsName := binding.GetTargetParams()
|
if paramKind != nil && paramRef != nil {
|
||||||
|
|
||||||
// Find the params referred by the binding by looking its name up
|
// Find the params referred by the binding by looking its name up
|
||||||
// in our informer for its CRD
|
// in our informer for its CRD
|
||||||
paramInfo, ok := c.paramsCRDControllers[*paramSource]
|
paramInfo, ok := c.paramsCRDControllers[*paramKind]
|
||||||
if !ok {
|
if !ok {
|
||||||
addConfigError(fmt.Errorf("paramSource kind `%v` not known",
|
addConfigError(fmt.Errorf("paramKind kind `%v` not known",
|
||||||
paramSource.String()), definition, binding)
|
paramKind.String()), definition, binding)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(paramsNamespace) == 0 {
|
// If the param informer for this admission policy has not yet
|
||||||
param, err = paramInfo.controller.Informer().Get(paramsName)
|
// 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 {
|
} else {
|
||||||
param, err = paramInfo.controller.Informer().Namespaced(paramsNamespace).Get(paramsName)
|
param, err = paramInfo.controller.Informer().Namespaced(paramRef.Namespace).Get(paramRef.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Apply failure policy
|
// Apply failure policy
|
||||||
addConfigError(err, definition, binding)
|
addConfigError(err, definition, binding)
|
||||||
|
|
||||||
if k8serrors.IsNotFound(err) {
|
if k8serrors.IsInvalid(err) {
|
||||||
// Param doesnt exist yet?
|
// Param mis-configured
|
||||||
// Maybe just have to wait a bit.
|
// 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
|
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
|
// 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 {
|
if err != nil {
|
||||||
// compilation error. Apply failure policy
|
// runtime error. Apply failure policy
|
||||||
wrappedError := fmt.Errorf("failed to compile CEL expression: %w", err)
|
wrappedError := fmt.Errorf("failed to evaluate CEL expression: %w", err)
|
||||||
addConfigError(wrappedError, definition, binding)
|
addConfigError(wrappedError, definition, binding)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c.bindingInfos[namespacedBindingName] = bindingInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
decisions := bindingInfo.evaluator(a, param)
|
|
||||||
for _, decision := range decisions {
|
for _, decision := range decisions {
|
||||||
switch decision.Kind {
|
switch decision.kind {
|
||||||
case Admit:
|
case admit:
|
||||||
// Do nothing
|
// TODO: add metrics for ignored error here
|
||||||
case Deny:
|
case deny:
|
||||||
allDecisions = append(allDecisions, PolicyDecisionWithMetadata{
|
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
|
||||||
Definition: definition,
|
definition: definition,
|
||||||
Binding: binding,
|
binding: binding,
|
||||||
PolicyDecision: decision,
|
policyDecision: decision,
|
||||||
})
|
})
|
||||||
default:
|
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 {
|
if len(deniedDecisions) > 0 {
|
||||||
return k8serrors.NewConflict(
|
// TODO: refactor admission.NewForbidden so the name extraction is reusable but the code/reason is customizable
|
||||||
a.GetResource().GroupResource(), a.GetName(),
|
var message string
|
||||||
&PolicyError{
|
deniedDecision := deniedDecisions[0]
|
||||||
Decisions: allDecisions,
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *celAdmissionController) HasSynced() bool {
|
func (c *celAdmissionController) HasSynced() bool {
|
||||||
return c.policyBindingController.HasSynced() &&
|
return c.policyBindingController.HasSynced() &&
|
||||||
c.policyDefinitionsController.HasSynced()
|
c.policyDefinitionsController.HasSynced()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *celAdmissionController) ValidateInitialization() error {
|
||||||
|
return c.validatorCompiler.ValidateInitialization()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/api/admissionregistration/v1alpha1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
|
@ -30,28 +31,27 @@ import (
|
||||||
"k8s.io/client-go/tools/cache"
|
"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()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
// Namespace for policydefinition is empty. Leaving usage here for compatibility
|
// Namespace for policydefinition is empty.
|
||||||
// with future NamespacedPolicyDefinition
|
nn := getNamespaceName(namespace, name)
|
||||||
namespacedName := namespace + "/" + name
|
info, ok := c.definitionInfo[nn]
|
||||||
info, ok := c.definitionInfo[namespacedName]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
info = &definitionInfo{}
|
info = &definitionInfo{}
|
||||||
c.definitionInfo[namespacedName] = info
|
c.definitionInfo[nn] = info
|
||||||
}
|
}
|
||||||
|
|
||||||
var paramSource *schema.GroupVersionKind
|
var paramSource *v1alpha1.ParamKind
|
||||||
if definition != nil {
|
if definition != nil {
|
||||||
paramSource = definition.GetParamSource()
|
paramSource = definition.Spec.ParamKind
|
||||||
}
|
}
|
||||||
|
|
||||||
// If param source has changed, remove definition as dependent of old params
|
// 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 there are no more dependents of old param, stop and clean up controller
|
||||||
if info.lastReconciledValue != nil && info.lastReconciledValue.GetParamSource() != nil {
|
if info.lastReconciledValue != nil && info.lastReconciledValue.Spec.ParamKind != nil {
|
||||||
oldParamSource := *info.lastReconciledValue.GetParamSource()
|
oldParamSource := *info.lastReconciledValue.Spec.ParamKind
|
||||||
|
|
||||||
// If we are:
|
// If we are:
|
||||||
// - switching from having a param to not having a param (includes deletion)
|
// - 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.
|
// we remove dependency on the controller.
|
||||||
if paramSource == nil || *paramSource != oldParamSource {
|
if paramSource == nil || *paramSource != oldParamSource {
|
||||||
if oldParamInfo, ok := c.paramsCRDControllers[oldParamSource]; ok {
|
if oldParamInfo, ok := c.paramsCRDControllers[oldParamSource]; ok {
|
||||||
oldParamInfo.dependentDefinitions.Delete(namespacedName)
|
oldParamInfo.dependentDefinitions.Delete(nn)
|
||||||
if len(oldParamInfo.dependentDefinitions) == 0 {
|
if len(oldParamInfo.dependentDefinitions) == 0 {
|
||||||
oldParamInfo.stop()
|
oldParamInfo.stop()
|
||||||
delete(c.paramsCRDControllers, oldParamSource)
|
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
|
// Reset all previously compiled evaluators in case something relevant in
|
||||||
// definition has changed.
|
// definition has changed.
|
||||||
for key := range c.definitionsToBindings[namespacedName] {
|
for key := range c.definitionsToBindings[nn] {
|
||||||
bindingInfo := c.bindingInfos[key]
|
bindingInfo := c.bindingInfos[key]
|
||||||
bindingInfo.evaluator = nil
|
bindingInfo.validator.Store(nil)
|
||||||
c.bindingInfos[key] = bindingInfo
|
c.bindingInfos[key] = bindingInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
if definition == nil {
|
if definition == nil {
|
||||||
delete(c.definitionInfo, namespacedName)
|
delete(c.definitionInfo, nn)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,12 +91,28 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
|
||||||
}
|
}
|
||||||
|
|
||||||
// find GVR for params
|
// 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 {
|
if err != nil {
|
||||||
// Failed to resolve. Return error so we retry again (rate limited)
|
// Failed to resolve. Return error so we retry again (rate limited)
|
||||||
// Save a record of this definition with an evaluator that unconditionally
|
// 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
|
return info.configurationError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,7 +142,7 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
|
||||||
c.paramsCRDControllers[*paramSource] = ¶mInfo{
|
c.paramsCRDControllers[*paramSource] = ¶mInfo{
|
||||||
controller: controller,
|
controller: controller,
|
||||||
stop: instanceCancel,
|
stop: instanceCancel,
|
||||||
dependentDefinitions: sets.NewString(namespacedName),
|
dependentDefinitions: sets.New(nn),
|
||||||
}
|
}
|
||||||
|
|
||||||
go informer.Informer().Run(instanceContext.Done())
|
go informer.Informer().Run(instanceContext.Done())
|
||||||
|
|
@ -136,37 +152,37 @@ func (c *celAdmissionController) reconcilePolicyDefinition(namespace, name strin
|
||||||
return nil
|
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()
|
c.mutex.Lock()
|
||||||
defer c.mutex.Unlock()
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
// Namespace for PolicyBinding is empty. In the future a namespaced binding
|
// Namespace for PolicyBinding is empty. In the future a namespaced binding
|
||||||
// may be added
|
// may be added
|
||||||
// https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042
|
// https://github.com/kubernetes/enhancements/blob/bf5c3c81ea2081d60c1dc7c832faa98479e06209/keps/sig-api-machinery/3488-cel-admission-control/README.md?plain=1#L1042
|
||||||
namespacedName := namespace + "/" + name
|
nn := getNamespaceName(namespace, name)
|
||||||
info, ok := c.bindingInfos[namespacedName]
|
info, ok := c.bindingInfos[nn]
|
||||||
if !ok {
|
if !ok {
|
||||||
info = &bindingInfo{}
|
info = &bindingInfo{}
|
||||||
c.bindingInfos[namespacedName] = info
|
c.bindingInfos[nn] = info
|
||||||
}
|
}
|
||||||
|
|
||||||
oldNamespacedDefinitionName := ""
|
var oldNamespacedDefinitionName namespacedName
|
||||||
if info.lastReconciledValue != nil {
|
if info.lastReconciledValue != nil {
|
||||||
oldefinitionNamespace, oldefinitionName := info.lastReconciledValue.GetTargetDefinition()
|
// All validating policies are cluster-scoped so have empty namespace
|
||||||
oldNamespacedDefinitionName = oldefinitionNamespace + "/" + oldefinitionName
|
oldNamespacedDefinitionName = getNamespaceName("", info.lastReconciledValue.Spec.PolicyName)
|
||||||
}
|
}
|
||||||
|
|
||||||
namespacedDefinitionName := ""
|
var namespacedDefinitionName namespacedName
|
||||||
if binding != nil {
|
if binding != nil {
|
||||||
newDefinitionNamespace, newDefinitionName := binding.GetTargetDefinition()
|
// All validating policies are cluster-scoped so have empty namespace
|
||||||
namespacedDefinitionName = newDefinitionNamespace + "/" + newDefinitionName
|
namespacedDefinitionName = getNamespaceName("", binding.Spec.PolicyName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove record of binding from old definition if the referred policy
|
// Remove record of binding from old definition if the referred policy
|
||||||
// has changed
|
// has changed
|
||||||
if oldNamespacedDefinitionName != namespacedDefinitionName {
|
if oldNamespacedDefinitionName != namespacedDefinitionName {
|
||||||
if dependentBindings, ok := c.definitionsToBindings[oldNamespacedDefinitionName]; ok {
|
if dependentBindings, ok := c.definitionsToBindings[oldNamespacedDefinitionName]; ok {
|
||||||
dependentBindings.Delete(namespacedName)
|
dependentBindings.Delete(nn)
|
||||||
|
|
||||||
// if there are no more dependent bindings, remove knowledge of the
|
// if there are no more dependent bindings, remove knowledge of the
|
||||||
// definition altogether
|
// definition altogether
|
||||||
|
|
@ -177,19 +193,19 @@ func (c *celAdmissionController) reconcilePolicyBinding(namespace, name string,
|
||||||
}
|
}
|
||||||
|
|
||||||
if binding == nil {
|
if binding == nil {
|
||||||
delete(c.bindingInfos, namespacedName)
|
delete(c.bindingInfos, nn)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add record of binding to new definition
|
// Add record of binding to new definition
|
||||||
if dependentBindings, ok := c.definitionsToBindings[namespacedDefinitionName]; ok {
|
if dependentBindings, ok := c.definitionsToBindings[namespacedDefinitionName]; ok {
|
||||||
dependentBindings.Insert(namespacedName)
|
dependentBindings.Insert(nn)
|
||||||
} else {
|
} else {
|
||||||
c.definitionsToBindings[namespacedDefinitionName] = sets.NewString(namespacedName)
|
c.definitionsToBindings[namespacedDefinitionName] = sets.New(nn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove compiled template for old binding
|
// Remove compiled template for old binding
|
||||||
info.evaluator = nil
|
info.validator.Store(nil)
|
||||||
info.lastReconciledValue = binding
|
info.lastReconciledValue = binding
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -201,3 +217,10 @@ func (c *celAdmissionController) reconcileParams(namespace, name string, params
|
||||||
// checker errors to the status of the resources.
|
// checker errors to the status of the resources.
|
||||||
return nil
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CELPolicyEvaluator interface {
|
type CELPolicyEvaluator interface {
|
||||||
|
admission.InitializationValidator
|
||||||
|
|
||||||
Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error
|
Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error
|
||||||
HasSynced() bool
|
HasSynced() bool
|
||||||
}
|
Run(stopCh <-chan struct{})
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,92 +17,34 @@ limitations under the License.
|
||||||
package cel
|
package cel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/api/admissionregistration/v1alpha1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/cel"
|
|
||||||
|
|
||||||
"github.com/google/cel-go/common/types/ref"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type FailurePolicy string
|
// Validator defines the func used to validate the cel expressions
|
||||||
|
// matchKind provides the GroupVersionKind that the object should be
|
||||||
const (
|
// validated by CEL expressions as.
|
||||||
Fail FailurePolicy = "Fail"
|
type Validator interface {
|
||||||
Ignore FailurePolicy = "Ignore"
|
Validate(a admission.Attributes, o admission.ObjectInterfaces, versionedParams runtime.Object, matchKind schema.GroupVersionKind) ([]policyDecision, error)
|
||||||
)
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PolicyDefinition is an interface for internal policy binding type.
|
// ValidatorCompiler is Dependency Injected into the PolicyDefinition's `Compile`
|
||||||
// Implemented by mock/testing types, and to be implemented by the public API
|
// function to assist with converting types and values to/from CEL-typed values.
|
||||||
// types once they have completed API review.
|
type ValidatorCompiler interface {
|
||||||
//
|
admission.InitializationValidator
|
||||||
// The interface closely mirrors the format and functionality of the
|
|
||||||
// PolicyDefinition proposed in the KEP.
|
|
||||||
type PolicyDefinition interface {
|
|
||||||
runtime.Object
|
|
||||||
|
|
||||||
// Matches says whether this policy definition matches the provided admission
|
// Matches says whether this policy definition matches the provided admission
|
||||||
// resource request
|
// resource request
|
||||||
Matches(a admission.Attributes) bool
|
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error)
|
||||||
|
|
||||||
Compile(
|
// Matches says whether this policy definition matches the provided admission
|
||||||
// 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
|
|
||||||
// resource request
|
// 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
|
// Compile is used for the cel expression compilation
|
||||||
// by this binding.
|
Compile(
|
||||||
GetTargetDefinition() (namespace, name string)
|
policy *v1alpha1.ValidatingAdmissionPolicy,
|
||||||
|
) Validator
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ type ControllerOptions struct {
|
||||||
Workers uint
|
Workers uint
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c controller[T]) Informer() Informer[T] {
|
func (c *controller[T]) Informer() Informer[T] {
|
||||||
return c.informer
|
return c.informer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ func NewController[T runtime.Object](
|
||||||
options: options,
|
options: options,
|
||||||
informer: informer,
|
informer: informer,
|
||||||
reconciler: reconciler,
|
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)
|
klog.Infof("starting %s", c.options.Name)
|
||||||
defer klog.Infof("stopping %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{}) {
|
enqueue := func(obj interface{}) {
|
||||||
var key string
|
var key string
|
||||||
var err error
|
var err error
|
||||||
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
|
if key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err != nil {
|
||||||
utilruntime.HandleError(err)
|
utilruntime.HandleError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +193,7 @@ func (c *controller[T]) HasSynced() bool {
|
||||||
|
|
||||||
func (c *controller[T]) runWorker() {
|
func (c *controller[T]) runWorker() {
|
||||||
for {
|
for {
|
||||||
obj, shutdown := c.queue.Get()
|
key, shutdown := c.queue.Get()
|
||||||
if shutdown {
|
if shutdown {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -221,9 +229,9 @@ func (c *controller[T]) runWorker() {
|
||||||
// Finally, if no error occurs we Forget this item so it is allowed
|
// Finally, if no error occurs we Forget this item so it is allowed
|
||||||
// to be re-enqueued without a long rate limit
|
// to be re-enqueued without a long rate limit
|
||||||
c.queue.Forget(obj)
|
c.queue.Forget(obj)
|
||||||
klog.Infof("Successfully synced '%s'", key)
|
klog.V(4).Infof("syncAdmissionPolicy(%q)", key)
|
||||||
return nil
|
return nil
|
||||||
}(obj)
|
}(key)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utilruntime.HandleError(err)
|
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
|
package cel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"net/http"
|
||||||
"fmt"
|
|
||||||
|
"k8s.io/api/admissionregistration/v1alpha1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PolicyDecisionKind string
|
type policyDecisionKind string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Admit PolicyDecisionKind = "Admit"
|
admit policyDecisionKind = "admit"
|
||||||
Deny PolicyDecisionKind = "Deny"
|
deny policyDecisionKind = "deny"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PolicyDecision struct {
|
type policyDecision struct {
|
||||||
Kind PolicyDecisionKind `json:"kind"`
|
kind policyDecisionKind
|
||||||
Message any `json:"message"`
|
message string
|
||||||
|
reason metav1.StatusReason
|
||||||
}
|
}
|
||||||
|
|
||||||
type PolicyDecisionWithMetadata struct {
|
type policyDecisionWithMetadata struct {
|
||||||
PolicyDecision `json:"decision"`
|
policyDecision
|
||||||
Definition PolicyDefinition `json:"definition"`
|
definition *v1alpha1.ValidatingAdmissionPolicy
|
||||||
Binding PolicyBinding `json:"binding"`
|
binding *v1alpha1.ValidatingAdmissionPolicyBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
type PolicyError struct {
|
func reasonToCode(r metav1.StatusReason) int32 {
|
||||||
Decisions []PolicyDecisionWithMetadata
|
switch r {
|
||||||
}
|
case metav1.StatusReasonForbidden:
|
||||||
|
return http.StatusForbidden
|
||||||
func (p *PolicyError) Error() string {
|
case metav1.StatusReasonUnauthorized:
|
||||||
// Just format the error as JSON
|
return http.StatusUnauthorized
|
||||||
jsonText, err := json.Marshal(p.Decisions)
|
case metav1.StatusReasonRequestEntityTooLarge:
|
||||||
if err != nil {
|
return http.StatusRequestEntityTooLarge
|
||||||
return fmt.Sprintf("error formatting PolicyError: %s", err.Error())
|
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 {
|
if err != nil {
|
||||||
return nil, f, err
|
return nil, f, err
|
||||||
}
|
}
|
||||||
pluginInitializer := kubeadmission.New(c, f, nil, nil, nil)
|
pluginInitializer := kubeadmission.New(c, nil, f, nil, nil, nil)
|
||||||
pluginInitializer.Initialize(handler)
|
pluginInitializer.Initialize(handler)
|
||||||
err = admission.ValidateInitialization(handler)
|
err = admission.ValidateInitialization(handler)
|
||||||
return handler, f, err
|
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.
|
// Dispatch is called by the downstream Validate or Admit methods.
|
||||||
func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
|
func (a *Webhook) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
|
||||||
if rules.IsWebhookConfigurationResource(attr) {
|
if rules.IsExemptAdmissionConfigurationResource(attr) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !a.WaitForReady() {
|
if !a.WaitForReady() {
|
||||||
|
|
|
||||||
|
|
@ -116,12 +116,12 @@ func (r *Matcher) resource() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWebhookConfigurationResource determines if an admission.Attributes object is describing
|
// IsExemptAdmissionConfigurationResource determines if an admission.Attributes object is describing
|
||||||
// the admission of a ValidatingWebhookConfiguration or a MutatingWebhookConfiguration
|
// the admission of a ValidatingWebhookConfiguration or a MutatingWebhookConfiguration or a ValidatingAdmissionPolicy or a ValidatingAdmissionPolicyBinding
|
||||||
func IsWebhookConfigurationResource(attr admission.Attributes) bool {
|
func IsExemptAdmissionConfigurationResource(attr admission.Attributes) bool {
|
||||||
gvk := attr.GetKind()
|
gvk := attr.GetKind()
|
||||||
if gvk.Group == "admissionregistration.k8s.io" {
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/initializer"
|
"k8s.io/apiserver/pkg/admission/initializer"
|
||||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||||
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||||
|
|
@ -35,6 +36,7 @@ import (
|
||||||
apiserverapiv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
|
apiserverapiv1 "k8s.io/apiserver/pkg/apis/apiserver/v1"
|
||||||
apiserverapiv1alpha1 "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
|
apiserverapiv1alpha1 "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1"
|
||||||
"k8s.io/apiserver/pkg/server"
|
"k8s.io/apiserver/pkg/server"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
|
|
@ -85,7 +87,7 @@ func NewAdmissionOptions() *AdmissionOptions {
|
||||||
// admission plugins. The apiserver always runs the validating ones
|
// admission plugins. The apiserver always runs the validating ones
|
||||||
// after all the mutating ones, so their relative order in this list
|
// after all the mutating ones, so their relative order in this list
|
||||||
// doesn't matter.
|
// doesn't matter.
|
||||||
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, validatingwebhook.PluginName},
|
RecommendedPluginOrder: []string{lifecycle.PluginName, mutatingwebhook.PluginName, cel.PluginName, validatingwebhook.PluginName},
|
||||||
DefaultOffPlugins: sets.NewString(),
|
DefaultOffPlugins: sets.NewString(),
|
||||||
}
|
}
|
||||||
server.RegisterAllAdmissionPlugins(options.Plugins)
|
server.RegisterAllAdmissionPlugins(options.Plugins)
|
||||||
|
|
@ -145,7 +147,11 @@ func (a *AdmissionOptions) ApplyTo(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 := admission.PluginInitializers{genericInitializer}
|
||||||
initializersChain = append(initializersChain, pluginInitializers...)
|
initializersChain = append(initializersChain, pluginInitializers...)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ func TestEnabledPluginNames(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
// scenario 0: check if a call to enabledPluginNames sets expected values.
|
// 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
|
// 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.
|
// This file exists to force the desired plugin implementations to be linked into genericapi pkg.
|
||||||
import (
|
import (
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
"k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle"
|
||||||
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
mutatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||||
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
validatingwebhook "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||||
|
|
@ -29,4 +30,5 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) {
|
||||||
lifecycle.Register(plugins)
|
lifecycle.Register(plugins)
|
||||||
validatingwebhook.Register(plugins)
|
validatingwebhook.Register(plugins)
|
||||||
mutatingwebhook.Register(plugins)
|
mutatingwebhook.Register(plugins)
|
||||||
|
cel.Register(plugins)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue