Merge pull request #78505 from caesarxuchao/dynamic-object-selector
Adding ObjectSelector to admission webhooks Kubernetes-commit: bada1c6b1eef959825c3dca1d3944e1ac4c31184
This commit is contained in:
commit
268a6b65e7
|
|
@ -396,7 +396,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "k8s.io/api",
|
"ImportPath": "k8s.io/api",
|
||||||
"Rev": "0b6636a6b587"
|
"Rev": "c1e9adbde704"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "k8s.io/apimachinery",
|
"ImportPath": "k8s.io/apimachinery",
|
||||||
|
|
|
||||||
4
go.mod
4
go.mod
|
|
@ -57,7 +57,7 @@ require (
|
||||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
|
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.1
|
gopkg.in/yaml.v2 v2.2.1
|
||||||
gotest.tools v2.2.0+incompatible // indirect
|
gotest.tools v2.2.0+incompatible // indirect
|
||||||
k8s.io/api v0.0.0-20190602125758-0b6636a6b587
|
k8s.io/api v0.0.0-20190602125759-c1e9adbde704
|
||||||
k8s.io/apimachinery v0.0.0-20190602125621-c0632ccbde11
|
k8s.io/apimachinery v0.0.0-20190602125621-c0632ccbde11
|
||||||
k8s.io/client-go v0.0.0-20190531132439-88ff0afc48bb
|
k8s.io/client-go v0.0.0-20190531132439-88ff0afc48bb
|
||||||
k8s.io/component-base v0.0.0-20190602130718-4ec519775454
|
k8s.io/component-base v0.0.0-20190602130718-4ec519775454
|
||||||
|
|
@ -72,7 +72,7 @@ replace (
|
||||||
golang.org/x/sync => golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
|
golang.org/x/sync => golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
|
||||||
golang.org/x/sys => golang.org/x/sys v0.0.0-20190209173611-3b5209105503
|
golang.org/x/sys => golang.org/x/sys v0.0.0-20190209173611-3b5209105503
|
||||||
golang.org/x/tools => golang.org/x/tools v0.0.0-20190313210603-aa82965741a9
|
golang.org/x/tools => golang.org/x/tools v0.0.0-20190313210603-aa82965741a9
|
||||||
k8s.io/api => k8s.io/api v0.0.0-20190602125758-0b6636a6b587
|
k8s.io/api => k8s.io/api v0.0.0-20190602125759-c1e9adbde704
|
||||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190602125621-c0632ccbde11
|
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190602125621-c0632ccbde11
|
||||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20190531132439-88ff0afc48bb
|
k8s.io/client-go => k8s.io/client-go v0.0.0-20190531132439-88ff0afc48bb
|
||||||
k8s.io/component-base => k8s.io/component-base v0.0.0-20190602130718-4ec519775454
|
k8s.io/component-base => k8s.io/component-base v0.0.0-20190602130718-4ec519775454
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -192,7 +192,7 @@ gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||||
k8s.io/api v0.0.0-20190602125758-0b6636a6b587/go.mod h1:6v+AckiDRrvZFUHXmDOahi8suThDPhjRhq47xjNtGtM=
|
k8s.io/api v0.0.0-20190602125759-c1e9adbde704/go.mod h1:8b8mSgV/I0gJKSPkwXL06YqDsRGS+n5mviEfpVnf4l4=
|
||||||
k8s.io/apimachinery v0.0.0-20190602125621-c0632ccbde11/go.mod h1:u/2VL7tgEMV0FFTV9q0JO+7cnTsV44LP8Pmx41R4AQ4=
|
k8s.io/apimachinery v0.0.0-20190602125621-c0632ccbde11/go.mod h1:u/2VL7tgEMV0FFTV9q0JO+7cnTsV44LP8Pmx41R4AQ4=
|
||||||
k8s.io/client-go v0.0.0-20190531132439-88ff0afc48bb/go.mod h1:oBWDlQWEpK7nPTuJljTF4w6W/zjopIHAPOJu70zfOHE=
|
k8s.io/client-go v0.0.0-20190531132439-88ff0afc48bb/go.mod h1:oBWDlQWEpK7nPTuJljTF4w6W/zjopIHAPOJu70zfOHE=
|
||||||
k8s.io/component-base v0.0.0-20190602130718-4ec519775454/go.mod h1:bS4Et9rV53C0WG/JRa+efnzveAUGHN+6XuIC9ZgGDkc=
|
k8s.io/component-base v0.0.0-20190602130718-4ec519775454/go.mod h1:bS4Et9rV53C0WG/JRa+efnzveAUGHN+6XuIC9ZgGDkc=
|
||||||
|
|
|
||||||
|
|
@ -44,21 +44,24 @@ type attributesRecord struct {
|
||||||
// But ValidatingAdmissionWebhook add annotations concurrently.
|
// But ValidatingAdmissionWebhook add annotations concurrently.
|
||||||
annotations map[string]string
|
annotations map[string]string
|
||||||
annotationsLock sync.RWMutex
|
annotationsLock sync.RWMutex
|
||||||
|
|
||||||
|
reinvocationContext ReinvocationContext
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAttributesRecord(object runtime.Object, oldObject runtime.Object, kind schema.GroupVersionKind, namespace, name string, resource schema.GroupVersionResource, subresource string, operation Operation, operationOptions runtime.Object, dryRun bool, userInfo user.Info) Attributes {
|
func NewAttributesRecord(object runtime.Object, oldObject runtime.Object, kind schema.GroupVersionKind, namespace, name string, resource schema.GroupVersionResource, subresource string, operation Operation, operationOptions runtime.Object, dryRun bool, userInfo user.Info) Attributes {
|
||||||
return &attributesRecord{
|
return &attributesRecord{
|
||||||
kind: kind,
|
kind: kind,
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
name: name,
|
name: name,
|
||||||
resource: resource,
|
resource: resource,
|
||||||
subresource: subresource,
|
subresource: subresource,
|
||||||
operation: operation,
|
operation: operation,
|
||||||
options: operationOptions,
|
options: operationOptions,
|
||||||
dryRun: dryRun,
|
dryRun: dryRun,
|
||||||
object: object,
|
object: object,
|
||||||
oldObject: oldObject,
|
oldObject: oldObject,
|
||||||
userInfo: userInfo,
|
userInfo: userInfo,
|
||||||
|
reinvocationContext: &reinvocationContext{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,6 +143,46 @@ func (record *attributesRecord) AddAnnotation(key, value string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (record *attributesRecord) GetReinvocationContext() ReinvocationContext {
|
||||||
|
return record.reinvocationContext
|
||||||
|
}
|
||||||
|
|
||||||
|
type reinvocationContext struct {
|
||||||
|
// isReinvoke is true when admission plugins are being reinvoked
|
||||||
|
isReinvoke bool
|
||||||
|
// reinvokeRequested is true when an admission plugin requested a re-invocation of the chain
|
||||||
|
reinvokeRequested bool
|
||||||
|
// values stores reinvoke context values per plugin.
|
||||||
|
values map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *reinvocationContext) IsReinvoke() bool {
|
||||||
|
return rc.isReinvoke
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *reinvocationContext) SetIsReinvoke() {
|
||||||
|
rc.isReinvoke = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *reinvocationContext) ShouldReinvoke() bool {
|
||||||
|
return rc.reinvokeRequested
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *reinvocationContext) SetShouldReinvoke() {
|
||||||
|
rc.reinvokeRequested = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *reinvocationContext) SetValue(plugin string, v interface{}) {
|
||||||
|
if rc.values == nil {
|
||||||
|
rc.values = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
rc.values[plugin] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *reinvocationContext) Value(plugin string) interface{} {
|
||||||
|
return rc.values[plugin]
|
||||||
|
}
|
||||||
|
|
||||||
func checkKeyFormat(key string) error {
|
func checkKeyFormat(key string) error {
|
||||||
parts := strings.Split(key, "/")
|
parts := strings.Split(key, "/")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"k8s.io/api/admissionregistration/v1beta1"
|
"k8s.io/api/admissionregistration/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
|
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
|
||||||
|
|
@ -48,7 +49,7 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with an empty list
|
// Start with an empty list
|
||||||
manager.configuration.Store(&v1beta1.MutatingWebhookConfiguration{})
|
manager.configuration.Store([]webhook.WebhookAccessor{})
|
||||||
|
|
||||||
// On any change, rebuild the config
|
// On any change, rebuild the config
|
||||||
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
|
|
@ -61,8 +62,8 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g
|
||||||
}
|
}
|
||||||
|
|
||||||
// Webhooks returns the merged MutatingWebhookConfiguration.
|
// Webhooks returns the merged MutatingWebhookConfiguration.
|
||||||
func (m *mutatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook {
|
func (m *mutatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor {
|
||||||
return m.configuration.Load().(*v1beta1.MutatingWebhookConfiguration).Webhooks
|
return m.configuration.Load().([]webhook.WebhookAccessor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mutatingWebhookConfigurationManager) HasSynced() bool {
|
func (m *mutatingWebhookConfigurationManager) HasSynced() bool {
|
||||||
|
|
@ -78,16 +79,24 @@ func (m *mutatingWebhookConfigurationManager) updateConfiguration() {
|
||||||
m.configuration.Store(mergeMutatingWebhookConfigurations(configurations))
|
m.configuration.Store(mergeMutatingWebhookConfigurations(configurations))
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhookConfiguration) *v1beta1.MutatingWebhookConfiguration {
|
func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {
|
||||||
var ret v1beta1.MutatingWebhookConfiguration
|
|
||||||
// The internal order of webhooks for each configuration is provided by the user
|
// The internal order of webhooks for each configuration is provided by the user
|
||||||
// but configurations themselves can be in any order. As we are going to run these
|
// but configurations themselves can be in any order. As we are going to run these
|
||||||
// webhooks in serial, they are sorted here to have a deterministic order.
|
// webhooks in serial, they are sorted here to have a deterministic order.
|
||||||
sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName)
|
sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName)
|
||||||
|
accessors := []webhook.WebhookAccessor{}
|
||||||
for _, c := range configurations {
|
for _, c := range configurations {
|
||||||
ret.Webhooks = append(ret.Webhooks, c.Webhooks...)
|
// webhook names are not validated for uniqueness, so we check for duplicates and
|
||||||
|
// add a int suffix to distinguish between them
|
||||||
|
names := map[string]int{}
|
||||||
|
for i := range c.Webhooks {
|
||||||
|
n := c.Webhooks[i].Name
|
||||||
|
uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
|
||||||
|
names[n]++
|
||||||
|
accessors = append(accessors, webhook.NewMutatingWebhookAccessor(uid, &c.Webhooks[i]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &ret
|
return accessors
|
||||||
}
|
}
|
||||||
|
|
||||||
type MutatingWebhookConfigurationSorter []*v1beta1.MutatingWebhookConfiguration
|
type MutatingWebhookConfigurationSorter []*v1beta1.MutatingWebhookConfiguration
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ func TestGetMutatingWebhookConfig(t *testing.T) {
|
||||||
|
|
||||||
webhookConfiguration := &v1beta1.MutatingWebhookConfiguration{
|
webhookConfiguration := &v1beta1.MutatingWebhookConfiguration{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
|
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
|
||||||
Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}},
|
Webhooks: []v1beta1.MutatingWebhook{{Name: "webhook1.1"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
mutatingInformer := informerFactory.Admissionregistration().V1beta1().MutatingWebhookConfigurations()
|
mutatingInformer := informerFactory.Admissionregistration().V1beta1().MutatingWebhookConfigurations()
|
||||||
|
|
@ -57,7 +57,14 @@ func TestGetMutatingWebhookConfig(t *testing.T) {
|
||||||
if len(configurations) == 0 {
|
if len(configurations) == 0 {
|
||||||
t.Errorf("expected non empty webhooks")
|
t.Errorf("expected non empty webhooks")
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) {
|
for i := range configurations {
|
||||||
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations)
|
h, ok := configurations[i].GetMutatingWebhook()
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected mutating webhook")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) {
|
||||||
|
t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"k8s.io/api/admissionregistration/v1beta1"
|
"k8s.io/api/admissionregistration/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
|
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
|
||||||
|
|
@ -48,7 +49,7 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with an empty list
|
// Start with an empty list
|
||||||
manager.configuration.Store(&v1beta1.ValidatingWebhookConfiguration{})
|
manager.configuration.Store([]webhook.WebhookAccessor{})
|
||||||
|
|
||||||
// On any change, rebuild the config
|
// On any change, rebuild the config
|
||||||
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||||
|
|
@ -61,8 +62,8 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Webhooks returns the merged ValidatingWebhookConfiguration.
|
// Webhooks returns the merged ValidatingWebhookConfiguration.
|
||||||
func (v *validatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook {
|
func (v *validatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor {
|
||||||
return v.configuration.Load().(*v1beta1.ValidatingWebhookConfiguration).Webhooks
|
return v.configuration.Load().([]webhook.WebhookAccessor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasSynced returns true if the shared informers have synced.
|
// HasSynced returns true if the shared informers have synced.
|
||||||
|
|
@ -79,15 +80,21 @@ func (v *validatingWebhookConfigurationManager) updateConfiguration() {
|
||||||
v.configuration.Store(mergeValidatingWebhookConfigurations(configurations))
|
v.configuration.Store(mergeValidatingWebhookConfigurations(configurations))
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeValidatingWebhookConfigurations(
|
func mergeValidatingWebhookConfigurations(configurations []*v1beta1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {
|
||||||
configurations []*v1beta1.ValidatingWebhookConfiguration,
|
|
||||||
) *v1beta1.ValidatingWebhookConfiguration {
|
|
||||||
sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
|
sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
|
||||||
var ret v1beta1.ValidatingWebhookConfiguration
|
accessors := []webhook.WebhookAccessor{}
|
||||||
for _, c := range configurations {
|
for _, c := range configurations {
|
||||||
ret.Webhooks = append(ret.Webhooks, c.Webhooks...)
|
// webhook names are not validated for uniqueness, so we check for duplicates and
|
||||||
|
// add a int suffix to distinguish between them
|
||||||
|
names := map[string]int{}
|
||||||
|
for i := range c.Webhooks {
|
||||||
|
n := c.Webhooks[i].Name
|
||||||
|
uid := fmt.Sprintf("%s/%s/%d", c.Name, n, names[n])
|
||||||
|
names[n]++
|
||||||
|
accessors = append(accessors, webhook.NewValidatingWebhookAccessor(uid, &c.Webhooks[i]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &ret
|
return accessors
|
||||||
}
|
}
|
||||||
|
|
||||||
type ValidatingWebhookConfigurationSorter []*v1beta1.ValidatingWebhookConfiguration
|
type ValidatingWebhookConfigurationSorter []*v1beta1.ValidatingWebhookConfiguration
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func TestGetValidatingWebhookConfig(t *testing.T) {
|
||||||
|
|
||||||
webhookConfiguration := &v1beta1.ValidatingWebhookConfiguration{
|
webhookConfiguration := &v1beta1.ValidatingWebhookConfiguration{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
|
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
|
||||||
Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}},
|
Webhooks: []v1beta1.ValidatingWebhook{{Name: "webhook1.1"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
validatingInformer := informerFactory.Admissionregistration().V1beta1().ValidatingWebhookConfigurations()
|
validatingInformer := informerFactory.Admissionregistration().V1beta1().ValidatingWebhookConfigurations()
|
||||||
|
|
@ -59,7 +59,14 @@ func TestGetValidatingWebhookConfig(t *testing.T) {
|
||||||
if len(configurations) == 0 {
|
if len(configurations) == 0 {
|
||||||
t.Errorf("expected non empty webhooks")
|
t.Errorf("expected non empty webhooks")
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) {
|
for i := range configurations {
|
||||||
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations)
|
h, ok := configurations[i].GetValidatingWebhook()
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Expected validating webhook")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) {
|
||||||
|
t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,9 @@ type Attributes interface {
|
||||||
// An error is returned if the format of key is invalid. When trying to overwrite annotation with a new value, an error is returned.
|
// An error is returned if the format of key is invalid. When trying to overwrite annotation with a new value, an error is returned.
|
||||||
// Both ValidationInterface and MutationInterface are allowed to add Annotations.
|
// Both ValidationInterface and MutationInterface are allowed to add Annotations.
|
||||||
AddAnnotation(key, value string) error
|
AddAnnotation(key, value string) error
|
||||||
|
|
||||||
|
// GetReinvocationContext tracks the admission request information relevant to the re-invocation policy.
|
||||||
|
GetReinvocationContext() ReinvocationContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectInterfaces is an interface used by AdmissionController to get object interfaces
|
// ObjectInterfaces is an interface used by AdmissionController to get object interfaces
|
||||||
|
|
@ -91,6 +94,22 @@ type AnnotationsGetter interface {
|
||||||
GetAnnotations() map[string]string
|
GetAnnotations() map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReinvocationContext provides access to the admission related state required to implement the re-invocation policy.
|
||||||
|
type ReinvocationContext interface {
|
||||||
|
// IsReinvoke returns true if the current admission check is a re-invocation.
|
||||||
|
IsReinvoke() bool
|
||||||
|
// SetIsReinvoke sets the current admission check as a re-invocation.
|
||||||
|
SetIsReinvoke()
|
||||||
|
// ShouldReinvoke returns true if any plugin has requested a re-invocation.
|
||||||
|
ShouldReinvoke() bool
|
||||||
|
// SetShouldReinvoke signals that a re-invocation is desired.
|
||||||
|
SetShouldReinvoke()
|
||||||
|
// AddValue set a value for a plugin name, possibly overriding a previous value.
|
||||||
|
SetValue(plugin string, v interface{})
|
||||||
|
// Value reads a value for a webhook.
|
||||||
|
Value(plugin string) interface{}
|
||||||
|
}
|
||||||
|
|
||||||
// Interface is an abstract, pluggable interface for Admission Control decisions.
|
// Interface is an abstract, pluggable interface for Admission Control decisions.
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
// Handles returns true if this admission controller can handle the given operation
|
// Handles returns true if this admission controller can handle the given operation
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/api/admissionregistration/v1beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebhookAccessor provides a common interface to both mutating and validating webhook types.
|
||||||
|
type WebhookAccessor interface {
|
||||||
|
// GetUID gets a string that uniquely identifies the webhook.
|
||||||
|
GetUID() string
|
||||||
|
|
||||||
|
// GetName gets the webhook Name field. Note that the name is scoped to the webhook
|
||||||
|
// configuration and does not provide a globally unique identity, if a unique identity is
|
||||||
|
// needed, use GetUID.
|
||||||
|
GetName() string
|
||||||
|
// GetClientConfig gets the webhook ClientConfig field.
|
||||||
|
GetClientConfig() v1beta1.WebhookClientConfig
|
||||||
|
// GetRules gets the webhook Rules field.
|
||||||
|
GetRules() []v1beta1.RuleWithOperations
|
||||||
|
// GetFailurePolicy gets the webhook FailurePolicy field.
|
||||||
|
GetFailurePolicy() *v1beta1.FailurePolicyType
|
||||||
|
// GetMatchPolicy gets the webhook MatchPolicy field.
|
||||||
|
GetMatchPolicy() *v1beta1.MatchPolicyType
|
||||||
|
// GetNamespaceSelector gets the webhook NamespaceSelector field.
|
||||||
|
GetNamespaceSelector() *metav1.LabelSelector
|
||||||
|
// GetObjectSelector gets the webhook ObjectSelector field.
|
||||||
|
GetObjectSelector() *metav1.LabelSelector
|
||||||
|
// GetSideEffects gets the webhook SideEffects field.
|
||||||
|
GetSideEffects() *v1beta1.SideEffectClass
|
||||||
|
// GetTimeoutSeconds gets the webhook TimeoutSeconds field.
|
||||||
|
GetTimeoutSeconds() *int32
|
||||||
|
// GetAdmissionReviewVersions gets the webhook AdmissionReviewVersions field.
|
||||||
|
GetAdmissionReviewVersions() []string
|
||||||
|
|
||||||
|
// GetMutatingWebhook if the accessor contains a MutatingWebhook, returns it and true, else returns false.
|
||||||
|
GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool)
|
||||||
|
// GetValidatingWebhook if the accessor contains a ValidatingWebhook, returns it and true, else returns false.
|
||||||
|
GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMutatingWebhookAccessor creates an accessor for a MutatingWebhook.
|
||||||
|
func NewMutatingWebhookAccessor(uid string, h *v1beta1.MutatingWebhook) WebhookAccessor {
|
||||||
|
return mutatingWebhookAccessor{uid: uid, MutatingWebhook: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mutatingWebhookAccessor struct {
|
||||||
|
*v1beta1.MutatingWebhook
|
||||||
|
uid string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mutatingWebhookAccessor) GetUID() string {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetName() string {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig {
|
||||||
|
return m.ClientConfig
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations {
|
||||||
|
return m.Rules
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType {
|
||||||
|
return m.FailurePolicy
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
|
||||||
|
return m.MatchPolicy
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
|
||||||
|
return m.NamespaceSelector
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
|
||||||
|
return m.ObjectSelector
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
|
||||||
|
return m.SideEffects
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetTimeoutSeconds() *int32 {
|
||||||
|
return m.TimeoutSeconds
|
||||||
|
}
|
||||||
|
func (m mutatingWebhookAccessor) GetAdmissionReviewVersions() []string {
|
||||||
|
return m.AdmissionReviewVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mutatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) {
|
||||||
|
return m.MutatingWebhook, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mutatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewValidatingWebhookAccessor creates an accessor for a ValidatingWebhook.
|
||||||
|
func NewValidatingWebhookAccessor(uid string, h *v1beta1.ValidatingWebhook) WebhookAccessor {
|
||||||
|
return validatingWebhookAccessor{uid: uid, ValidatingWebhook: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
type validatingWebhookAccessor struct {
|
||||||
|
*v1beta1.ValidatingWebhook
|
||||||
|
uid string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v validatingWebhookAccessor) GetUID() string {
|
||||||
|
return v.uid
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetName() string {
|
||||||
|
return v.Name
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig {
|
||||||
|
return v.ClientConfig
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations {
|
||||||
|
return v.Rules
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType {
|
||||||
|
return v.FailurePolicy
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
|
||||||
|
return v.MatchPolicy
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
|
||||||
|
return v.NamespaceSelector
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
|
||||||
|
return v.ObjectSelector
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
|
||||||
|
return v.SideEffects
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetTimeoutSeconds() *int32 {
|
||||||
|
return v.TimeoutSeconds
|
||||||
|
}
|
||||||
|
func (v validatingWebhookAccessor) GetAdmissionReviewVersions() []string {
|
||||||
|
return v.AdmissionReviewVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v validatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v validatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) {
|
||||||
|
return v.ValidatingWebhook, true
|
||||||
|
}
|
||||||
|
|
@ -19,15 +19,15 @@ package generic
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"k8s.io/api/admissionregistration/v1beta1"
|
|
||||||
"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/admission/plugin/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Source can list dynamic webhook plugins.
|
// Source can list dynamic webhook plugins.
|
||||||
type Source interface {
|
type Source interface {
|
||||||
Webhooks() []v1beta1.Webhook
|
Webhooks() []webhook.WebhookAccessor
|
||||||
HasSynced() bool
|
HasSynced() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ type Source interface {
|
||||||
// variants of the object and old object.
|
// variants of the object and old object.
|
||||||
type VersionedAttributes struct {
|
type VersionedAttributes struct {
|
||||||
// Attributes holds the original admission attributes
|
// Attributes holds the original admission attributes
|
||||||
Attributes admission.Attributes
|
admission.Attributes
|
||||||
// VersionedOldObject holds Attributes.OldObject (if non-nil), converted to VersionedKind.
|
// VersionedOldObject holds Attributes.OldObject (if non-nil), converted to VersionedKind.
|
||||||
// It must never be mutated.
|
// It must never be mutated.
|
||||||
VersionedOldObject runtime.Object
|
VersionedOldObject runtime.Object
|
||||||
|
|
@ -48,11 +48,18 @@ type VersionedAttributes struct {
|
||||||
Dirty bool
|
Dirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetObject overrides the Attributes.GetObject()
|
||||||
|
func (v *VersionedAttributes) GetObject() runtime.Object {
|
||||||
|
if v.VersionedObject != nil {
|
||||||
|
return v.VersionedObject
|
||||||
|
}
|
||||||
|
return v.Attributes.GetObject()
|
||||||
|
}
|
||||||
|
|
||||||
// WebhookInvocation describes how to call a webhook, including the resource and subresource the webhook registered for,
|
// WebhookInvocation describes how to call a webhook, including the resource and subresource the webhook registered for,
|
||||||
// and the kind that should be sent to the webhook.
|
// and the kind that should be sent to the webhook.
|
||||||
type WebhookInvocation struct {
|
type WebhookInvocation struct {
|
||||||
Webhook *v1beta1.Webhook
|
Webhook webhook.WebhookAccessor
|
||||||
|
|
||||||
Resource schema.GroupVersionResource
|
Resource schema.GroupVersionResource
|
||||||
Subresource string
|
Subresource string
|
||||||
Kind schema.GroupVersionKind
|
Kind schema.GroupVersionKind
|
||||||
|
|
@ -60,6 +67,9 @@ type WebhookInvocation struct {
|
||||||
|
|
||||||
// Dispatcher dispatches webhook call to a list of webhooks with admission attributes as argument.
|
// Dispatcher dispatches webhook call to a list of webhooks with admission attributes as argument.
|
||||||
type Dispatcher interface {
|
type Dispatcher interface {
|
||||||
// Dispatch a request to the webhooks using the given webhooks. A non-nil error means the request is rejected.
|
// Dispatch a request to the webhooks. Dispatcher may choose not to
|
||||||
Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []*WebhookInvocation) error
|
// call a hook, either because the rules of the hook does not match, or
|
||||||
|
// the namespaceSelector or the objectSelector of the hook does not
|
||||||
|
// match. A non-nil error means the request is rejected.
|
||||||
|
Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,12 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/object"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/rules"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/rules"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
@ -42,8 +44,9 @@ type Webhook struct {
|
||||||
sourceFactory sourceFactory
|
sourceFactory sourceFactory
|
||||||
|
|
||||||
hookSource Source
|
hookSource Source
|
||||||
clientManager *webhook.ClientManager
|
clientManager *webhookutil.ClientManager
|
||||||
namespaceMatcher *namespace.Matcher
|
namespaceMatcher *namespace.Matcher
|
||||||
|
objectMatcher *object.Matcher
|
||||||
dispatcher Dispatcher
|
dispatcher Dispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +56,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type sourceFactory func(f informers.SharedInformerFactory) Source
|
type sourceFactory func(f informers.SharedInformerFactory) Source
|
||||||
type dispatcherFactory func(cm *webhook.ClientManager) Dispatcher
|
type dispatcherFactory func(cm *webhookutil.ClientManager) Dispatcher
|
||||||
|
|
||||||
// NewWebhook creates a new generic admission webhook.
|
// NewWebhook creates a new generic admission webhook.
|
||||||
func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) {
|
func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) {
|
||||||
|
|
@ -62,23 +65,24 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cm, err := webhook.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme)
|
cm, err := webhookutil.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
authInfoResolver, err := webhook.NewDefaultAuthenticationInfoResolver(kubeconfigFile)
|
authInfoResolver, err := webhookutil.NewDefaultAuthenticationInfoResolver(kubeconfigFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Set defaults which may be overridden later.
|
// Set defaults which may be overridden later.
|
||||||
cm.SetAuthenticationInfoResolver(authInfoResolver)
|
cm.SetAuthenticationInfoResolver(authInfoResolver)
|
||||||
cm.SetServiceResolver(webhook.NewDefaultServiceResolver())
|
cm.SetServiceResolver(webhookutil.NewDefaultServiceResolver())
|
||||||
|
|
||||||
return &Webhook{
|
return &Webhook{
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
sourceFactory: sourceFactory,
|
sourceFactory: sourceFactory,
|
||||||
clientManager: &cm,
|
clientManager: &cm,
|
||||||
namespaceMatcher: &namespace.Matcher{},
|
namespaceMatcher: &namespace.Matcher{},
|
||||||
|
objectMatcher: &object.Matcher{},
|
||||||
dispatcher: dispatcherFactory(&cm),
|
dispatcher: dispatcherFactory(&cm),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -86,13 +90,13 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
|
||||||
// SetAuthenticationInfoResolverWrapper sets the
|
// SetAuthenticationInfoResolverWrapper sets the
|
||||||
// AuthenticationInfoResolverWrapper.
|
// AuthenticationInfoResolverWrapper.
|
||||||
// TODO find a better way wire this, but keep this pull small for now.
|
// TODO find a better way wire this, but keep this pull small for now.
|
||||||
func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhook.AuthenticationInfoResolverWrapper) {
|
func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhookutil.AuthenticationInfoResolverWrapper) {
|
||||||
a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper)
|
a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetServiceResolver sets a service resolver for the webhook admission plugin.
|
// SetServiceResolver sets a service resolver for the webhook admission plugin.
|
||||||
// Passing a nil resolver does not have an effect, instead a default one will be used.
|
// Passing a nil resolver does not have an effect, instead a default one will be used.
|
||||||
func (a *Webhook) SetServiceResolver(sr webhook.ServiceResolver) {
|
func (a *Webhook) SetServiceResolver(sr webhookutil.ServiceResolver) {
|
||||||
a.clientManager.SetServiceResolver(sr)
|
a.clientManager.SetServiceResolver(sr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,12 +130,12 @@ func (a *Webhook) ValidateInitialization() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldCallHook returns invocation details if the webhook should be called, nil if the webhook should not be called,
|
// ShouldCallHook returns invocation details if the webhook should be called, nil if the webhook should not be called,
|
||||||
// or an error if an error was encountered during evaluation.
|
// or an error if an error was encountered during evaluation.
|
||||||
func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes, o admission.ObjectInterfaces) (*WebhookInvocation, *apierrors.StatusError) {
|
func (a *Webhook) ShouldCallHook(h webhook.WebhookAccessor, attr admission.Attributes, o admission.ObjectInterfaces) (*WebhookInvocation, *apierrors.StatusError) {
|
||||||
var err *apierrors.StatusError
|
var err *apierrors.StatusError
|
||||||
var invocation *WebhookInvocation
|
var invocation *WebhookInvocation
|
||||||
for _, r := range h.Rules {
|
for _, r := range h.GetRules() {
|
||||||
m := rules.Matcher{Rule: r, Attr: attr}
|
m := rules.Matcher{Rule: r, Attr: attr}
|
||||||
if m.Matches() {
|
if m.Matches() {
|
||||||
invocation = &WebhookInvocation{
|
invocation = &WebhookInvocation{
|
||||||
|
|
@ -143,12 +147,12 @@ func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes,
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if invocation == nil && h.MatchPolicy != nil && *h.MatchPolicy == v1beta1.Equivalent {
|
if invocation == nil && h.GetMatchPolicy() != nil && *h.GetMatchPolicy() == v1beta1.Equivalent {
|
||||||
attrWithOverride := &attrWithResourceOverride{Attributes: attr}
|
attrWithOverride := &attrWithResourceOverride{Attributes: attr}
|
||||||
equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
|
equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
|
||||||
// honor earlier rules first
|
// honor earlier rules first
|
||||||
OuterLoop:
|
OuterLoop:
|
||||||
for _, r := range h.Rules {
|
for _, r := range h.GetRules() {
|
||||||
// see if the rule matches any of the equivalent resources
|
// see if the rule matches any of the equivalent resources
|
||||||
for _, equivalent := range equivalents {
|
for _, equivalent := range equivalents {
|
||||||
if equivalent == attr.GetResource() {
|
if equivalent == attr.GetResource() {
|
||||||
|
|
@ -183,6 +187,11 @@ func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
matches, err = a.objectMatcher.MatchObjectSelector(h, attr)
|
||||||
|
if !matches || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return invocation, nil
|
return invocation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,21 +214,5 @@ func (a *Webhook) Dispatch(attr admission.Attributes, o admission.ObjectInterfac
|
||||||
// TODO: Figure out if adding one second timeout make sense here.
|
// TODO: Figure out if adding one second timeout make sense here.
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
|
|
||||||
var relevantHooks []*WebhookInvocation
|
return a.dispatcher.Dispatch(ctx, attr, o, hooks)
|
||||||
for i := range hooks {
|
|
||||||
invocation, err := a.shouldCallHook(&hooks[i], attr, o)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if invocation != nil {
|
|
||||||
relevantHooks = append(relevantHooks, invocation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(relevantHooks) == 0 {
|
|
||||||
// no matching hooks
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.dispatcher.Dispatch(ctx, attr, o, relevantHooks)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package generic
|
package generic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -25,11 +26,13 @@ import (
|
||||||
"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/admission/plugin/webhook"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShouldCallHook(t *testing.T) {
|
func TestShouldCallHook(t *testing.T) {
|
||||||
a := &Webhook{namespaceMatcher: &namespace.Matcher{}}
|
a := &Webhook{namespaceMatcher: &namespace.Matcher{}, objectMatcher: &object.Matcher{}}
|
||||||
|
|
||||||
allScopes := v1beta1.AllScopes
|
allScopes := v1beta1.AllScopes
|
||||||
exactMatch := v1beta1.Exact
|
exactMatch := v1beta1.Exact
|
||||||
|
|
@ -61,7 +64,7 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
||||||
webhook *v1beta1.Webhook
|
webhook *v1beta1.ValidatingWebhook
|
||||||
attrs admission.Attributes
|
attrs admission.Attributes
|
||||||
|
|
||||||
expectCall bool
|
expectCall bool
|
||||||
|
|
@ -72,14 +75,15 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no rules (just write)",
|
name: "no rules (just write)",
|
||||||
webhook: &v1beta1.Webhook{Rules: []v1beta1.RuleWithOperations{}},
|
webhook: &v1beta1.ValidatingWebhook{Rules: []v1beta1.RuleWithOperations{}},
|
||||||
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
expectCall: false,
|
expectCall: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid kind lookup",
|
name: "invalid kind lookup",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
MatchPolicy: &equivalentMatch,
|
MatchPolicy: &equivalentMatch,
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
|
|
@ -91,8 +95,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wildcard rule, match as requested",
|
name: "wildcard rule, match as requested",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
|
||||||
|
|
@ -105,8 +110,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, prefer exact match",
|
name: "specific rules, prefer exact match",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
||||||
|
|
@ -125,8 +131,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, match miss",
|
name: "specific rules, match miss",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
||||||
|
|
@ -139,9 +146,10 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, exact match miss",
|
name: "specific rules, exact match miss",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
MatchPolicy: &exactMatch,
|
MatchPolicy: &exactMatch,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
||||||
|
|
@ -154,9 +162,10 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, equivalent match, prefer extensions",
|
name: "specific rules, equivalent match, prefer extensions",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
MatchPolicy: &equivalentMatch,
|
MatchPolicy: &equivalentMatch,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
||||||
|
|
@ -172,9 +181,10 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, equivalent match, prefer apps",
|
name: "specific rules, equivalent match, prefer apps",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
MatchPolicy: &equivalentMatch,
|
MatchPolicy: &equivalentMatch,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments"}, Scope: &allScopes},
|
||||||
|
|
@ -191,8 +201,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "specific rules, subresource prefer exact match",
|
name: "specific rules, subresource prefer exact match",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
||||||
|
|
@ -211,8 +222,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, subresource match miss",
|
name: "specific rules, subresource match miss",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
||||||
|
|
@ -225,9 +237,10 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, subresource exact match miss",
|
name: "specific rules, subresource exact match miss",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
MatchPolicy: &exactMatch,
|
MatchPolicy: &exactMatch,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
||||||
|
|
@ -240,9 +253,10 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, subresource equivalent match, prefer extensions",
|
name: "specific rules, subresource equivalent match, prefer extensions",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
MatchPolicy: &equivalentMatch,
|
MatchPolicy: &equivalentMatch,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"extensions"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
||||||
|
|
@ -258,9 +272,10 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "specific rules, subresource equivalent match, prefer apps",
|
name: "specific rules, subresource equivalent match, prefer apps",
|
||||||
webhook: &v1beta1.Webhook{
|
webhook: &v1beta1.ValidatingWebhook{
|
||||||
MatchPolicy: &equivalentMatch,
|
MatchPolicy: &equivalentMatch,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: []v1beta1.RuleWithOperations{{
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
Operations: []v1beta1.OperationType{"*"},
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
Rule: v1beta1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
Rule: v1beta1.Rule{APIGroups: []string{"apps"}, APIVersions: []string{"v1beta1"}, Resources: []string{"deployments", "deployments/scale"}, Scope: &allScopes},
|
||||||
|
|
@ -276,9 +291,9 @@ func TestShouldCallHook(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testcase := range testcases {
|
for i, testcase := range testcases {
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
invocation, err := a.shouldCallHook(testcase.webhook, testcase.attrs, interfaces)
|
invocation, err := a.ShouldCallHook(webhook.NewValidatingWebhookAccessor(fmt.Sprintf("webhook-%d", i), testcase.webhook), testcase.attrs, interfaces)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(testcase.expectErr) == 0 {
|
if len(testcase.expectErr) == 0 {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jsonpatch "github.com/evanphx/json-patch"
|
jsonpatch "github.com/evanphx/json-patch"
|
||||||
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
"k8s.io/klog"
|
"k8s.io/klog"
|
||||||
|
|
||||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||||
|
|
@ -35,30 +36,71 @@ import (
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
|
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/util"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/util"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mutatingDispatcher struct {
|
type mutatingDispatcher struct {
|
||||||
cm *webhook.ClientManager
|
cm *webhookutil.ClientManager
|
||||||
plugin *Plugin
|
plugin *Plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMutatingDispatcher(p *Plugin) func(cm *webhook.ClientManager) generic.Dispatcher {
|
func newMutatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher {
|
||||||
return func(cm *webhook.ClientManager) generic.Dispatcher {
|
return func(cm *webhookutil.ClientManager) generic.Dispatcher {
|
||||||
return &mutatingDispatcher{cm, p}
|
return &mutatingDispatcher{cm, p}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ generic.Dispatcher = &mutatingDispatcher{}
|
var _ generic.Dispatcher = &mutatingDispatcher{}
|
||||||
|
|
||||||
func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, relevantHooks []*generic.WebhookInvocation) error {
|
func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
|
||||||
|
reinvokeCtx := attr.GetReinvocationContext()
|
||||||
|
var webhookReinvokeCtx *webhookReinvokeContext
|
||||||
|
if v := reinvokeCtx.Value(PluginName); v != nil {
|
||||||
|
webhookReinvokeCtx = v.(*webhookReinvokeContext)
|
||||||
|
} else {
|
||||||
|
webhookReinvokeCtx = &webhookReinvokeContext{}
|
||||||
|
reinvokeCtx.SetValue(PluginName, webhookReinvokeCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reinvokeCtx.IsReinvoke() && webhookReinvokeCtx.IsOutputChangedSinceLastWebhookInvocation(attr.GetObject()) {
|
||||||
|
// If the object has changed, we know the in-tree plugin re-invocations have mutated the object,
|
||||||
|
// and we need to reinvoke all eligible webhooks.
|
||||||
|
webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject())
|
||||||
|
}()
|
||||||
var versionedAttr *generic.VersionedAttributes
|
var versionedAttr *generic.VersionedAttributes
|
||||||
for _, invocation := range relevantHooks {
|
for _, hook := range hooks {
|
||||||
hook := invocation.Webhook
|
attrForCheck := attr
|
||||||
|
if versionedAttr != nil {
|
||||||
|
attrForCheck = versionedAttr
|
||||||
|
}
|
||||||
|
invocation, statusErr := a.plugin.ShouldCallHook(hook, attrForCheck, o)
|
||||||
|
if statusErr != nil {
|
||||||
|
return statusErr
|
||||||
|
}
|
||||||
|
if invocation == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hook, ok := invocation.Webhook.GetMutatingWebhook()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("mutating webhook dispatch requires v1beta1.MutatingWebhook, but got %T", hook)
|
||||||
|
}
|
||||||
|
// This means that during reinvocation, a webhook will not be
|
||||||
|
// called for the first time. For example, if the webhook is
|
||||||
|
// skipped in the first round because of mismatching labels,
|
||||||
|
// even if the labels become matching, the webhook does not
|
||||||
|
// get called during reinvocation.
|
||||||
|
if reinvokeCtx.IsReinvoke() && !webhookReinvokeCtx.ShouldReinvokeWebhook(invocation.Webhook.GetUID()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if versionedAttr == nil {
|
if versionedAttr == nil {
|
||||||
// First webhook, create versioned attributes
|
// First webhook, create versioned attributes
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -73,14 +115,23 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
|
||||||
}
|
}
|
||||||
|
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
err := a.callAttrMutatingHook(ctx, invocation, versionedAttr, o)
|
|
||||||
|
changed, err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, o)
|
||||||
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "admit", hook.Name)
|
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "admit", hook.Name)
|
||||||
|
if changed {
|
||||||
|
// Patch had changed the object. Prepare to reinvoke all previous webhooks that are eligible for re-invocation.
|
||||||
|
webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||||
|
reinvokeCtx.SetShouldReinvoke()
|
||||||
|
}
|
||||||
|
if hook.ReinvocationPolicy != nil && *hook.ReinvocationPolicy == v1beta1.IfNeededReinvocationPolicy {
|
||||||
|
webhookReinvokeCtx.AddReinvocableWebhookToPreviouslyInvoked(invocation.Webhook.GetUID())
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
|
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
|
||||||
if callErr, ok := err.(*webhook.ErrCallingWebhook); ok {
|
if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
|
||||||
if ignoreClientCallFailures {
|
if ignoreClientCallFailures {
|
||||||
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
|
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
|
||||||
utilruntime.HandleError(callErr)
|
utilruntime.HandleError(callErr)
|
||||||
|
|
@ -96,32 +147,33 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
|
||||||
if versionedAttr != nil && versionedAttr.VersionedObject != nil && versionedAttr.Dirty {
|
if versionedAttr != nil && versionedAttr.VersionedObject != nil && versionedAttr.Dirty {
|
||||||
return o.GetObjectConvertor().Convert(versionedAttr.VersionedObject, versionedAttr.Attributes.GetObject(), nil)
|
return o.GetObjectConvertor().Convert(versionedAttr.VersionedObject, versionedAttr.Attributes.GetObject(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// note that callAttrMutatingHook updates attr
|
// note that callAttrMutatingHook updates attr
|
||||||
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) error {
|
|
||||||
h := invocation.Webhook
|
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) (bool, error) {
|
||||||
if attr.Attributes.IsDryRun() {
|
if attr.Attributes.IsDryRun() {
|
||||||
if h.SideEffects == nil {
|
if h.SideEffects == nil {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
|
||||||
}
|
}
|
||||||
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
|
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
|
||||||
return webhookerrors.NewDryRunUnsupportedErr(h.Name)
|
return false, webhookerrors.NewDryRunUnsupportedErr(h.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently dispatcher only supports `v1beta1` AdmissionReview
|
// Currently dispatcher only supports `v1beta1` AdmissionReview
|
||||||
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
|
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
|
||||||
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, h) {
|
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReview")}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReview")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the webhook request
|
// Make the webhook request
|
||||||
request := request.CreateAdmissionReview(attr, invocation)
|
request := request.CreateAdmissionReview(attr, invocation)
|
||||||
client, err := a.cm.HookClient(util.HookClientConfigForWebhook(h))
|
client, err := a.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||||
}
|
}
|
||||||
response := &admissionv1beta1.AdmissionReview{}
|
response := &admissionv1beta1.AdmissionReview{}
|
||||||
r := client.Post().Context(ctx).Body(&request)
|
r := client.Post().Context(ctx).Body(&request)
|
||||||
|
|
@ -129,11 +181,11 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocatio
|
||||||
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
|
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
|
||||||
}
|
}
|
||||||
if err := r.Do().Into(response); err != nil {
|
if err := r.Do().Into(response); err != nil {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.Response == nil {
|
if response.Response == nil {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
|
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range response.Response.AuditAnnotations {
|
for k, v := range response.Response.AuditAnnotations {
|
||||||
|
|
@ -144,34 +196,34 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocatio
|
||||||
}
|
}
|
||||||
|
|
||||||
if !response.Response.Allowed {
|
if !response.Response.Allowed {
|
||||||
return webhookerrors.ToStatusErr(h.Name, response.Response.Result)
|
return false, webhookerrors.ToStatusErr(h.Name, response.Response.Result)
|
||||||
}
|
}
|
||||||
|
|
||||||
patchJS := response.Response.Patch
|
patchJS := response.Response.Patch
|
||||||
if len(patchJS) == 0 {
|
if len(patchJS) == 0 {
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
patchObj, err := jsonpatch.DecodePatch(patchJS)
|
patchObj, err := jsonpatch.DecodePatch(patchJS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
if len(patchObj) == 0 {
|
if len(patchObj) == 0 {
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if a non-empty patch was provided, and we have no object we can apply it to (e.g. a DELETE admission operation), error
|
// if a non-empty patch was provided, and we have no object we can apply it to (e.g. a DELETE admission operation), error
|
||||||
if attr.VersionedObject == nil {
|
if attr.VersionedObject == nil {
|
||||||
return apierrors.NewInternalError(fmt.Errorf("admission webhook %q attempted to modify the object, which is not supported for this operation", h.Name))
|
return false, apierrors.NewInternalError(fmt.Errorf("admission webhook %q attempted to modify the object, which is not supported for this operation", h.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), false)
|
jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), false)
|
||||||
objJS, err := runtime.Encode(jsonSerializer, attr.VersionedObject)
|
objJS, err := runtime.Encode(jsonSerializer, attr.VersionedObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
patchedJS, err := patchObj.Apply(objJS)
|
patchedJS, err := patchObj.Apply(objJS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var newVersionedObject runtime.Object
|
var newVersionedObject runtime.Object
|
||||||
|
|
@ -182,16 +234,20 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocatio
|
||||||
} else {
|
} else {
|
||||||
newVersionedObject, err = o.GetObjectCreater().New(attr.VersionedKind)
|
newVersionedObject, err = o.GetObjectCreater().New(attr.VersionedKind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: if we have multiple mutating webhooks, we can remember the json
|
// TODO: if we have multiple mutating webhooks, we can remember the json
|
||||||
// instead of encoding and decoding for each one.
|
// instead of encoding and decoding for each one.
|
||||||
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
|
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changed := !apiequality.Semantic.DeepEqual(attr.VersionedObject, newVersionedObject)
|
||||||
|
|
||||||
attr.Dirty = true
|
attr.Dirty = true
|
||||||
attr.VersionedObject = newVersionedObject
|
attr.VersionedObject = newVersionedObject
|
||||||
o.GetObjectDefaulter().Default(attr.VersionedObject)
|
o.GetObjectDefaulter().Default(attr.VersionedObject)
|
||||||
return nil
|
return changed, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,70 +46,80 @@ func TestAdmit(t *testing.T) {
|
||||||
defer close(stopCh)
|
defer close(stopCh)
|
||||||
|
|
||||||
testCases := append(webhooktesting.NewMutatingTestCases(serverURL),
|
testCases := append(webhooktesting.NewMutatingTestCases(serverURL),
|
||||||
webhooktesting.NewNonMutatingTestCases(serverURL)...)
|
webhooktesting.ConvertToMutatingTestCases(webhooktesting.NewNonMutatingTestCases(serverURL))...)
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
wh, err := NewMutatingWebhook(nil)
|
t.Run(tt.Name, func(t *testing.T) {
|
||||||
if err != nil {
|
wh, err := NewMutatingWebhook(nil)
|
||||||
t.Errorf("%s: failed to create mutating webhook: %v", tt.Name, err)
|
if err != nil {
|
||||||
continue
|
t.Errorf("failed to create mutating webhook: %v", err)
|
||||||
}
|
return
|
||||||
|
|
||||||
ns := "webhook-test"
|
|
||||||
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh)
|
|
||||||
|
|
||||||
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
|
|
||||||
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
|
|
||||||
wh.SetExternalKubeClientSet(client)
|
|
||||||
wh.SetExternalKubeInformerFactory(informer)
|
|
||||||
|
|
||||||
informer.Start(stopCh)
|
|
||||||
informer.WaitForCacheSync(stopCh)
|
|
||||||
|
|
||||||
if err = wh.ValidateInitialization(); err != nil {
|
|
||||||
t.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var attr admission.Attributes
|
|
||||||
if tt.IsCRD {
|
|
||||||
attr = webhooktesting.NewAttributeUnstructured(ns, tt.AdditionalLabels, tt.IsDryRun)
|
|
||||||
} else {
|
|
||||||
attr = webhooktesting.NewAttribute(ns, tt.AdditionalLabels, tt.IsDryRun)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wh.Admit(attr, objectInterfaces)
|
|
||||||
if tt.ExpectAllow != (err == nil) {
|
|
||||||
t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
|
|
||||||
}
|
|
||||||
if tt.ExpectLabels != nil {
|
|
||||||
if !reflect.DeepEqual(tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels()) {
|
|
||||||
t.Errorf("%s: expected labels '%v', but got '%v'", tt.Name, tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels())
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// ErrWebhookRejected is not an error for our purposes
|
ns := "webhook-test"
|
||||||
if tt.ErrorContains != "" {
|
client, informer := webhooktesting.NewFakeMutatingDataSource(ns, tt.Webhooks, stopCh)
|
||||||
if err == nil || !strings.Contains(err.Error(), tt.ErrorContains) {
|
|
||||||
t.Errorf("%s: expected an error saying %q, but got: %v", tt.Name, tt.ErrorContains, err)
|
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
|
||||||
|
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
|
||||||
|
wh.SetExternalKubeClientSet(client)
|
||||||
|
wh.SetExternalKubeInformerFactory(informer)
|
||||||
|
|
||||||
|
informer.Start(stopCh)
|
||||||
|
informer.WaitForCacheSync(stopCh)
|
||||||
|
|
||||||
|
if err = wh.ValidateInitialization(); err != nil {
|
||||||
|
t.Errorf("failed to validate initialization: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if statusErr, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
|
var attr admission.Attributes
|
||||||
t.Errorf("%s: expected a StatusError, got %T", tt.Name, err)
|
if tt.IsCRD {
|
||||||
} else if isStatusErr {
|
attr = webhooktesting.NewAttributeUnstructured(ns, tt.AdditionalLabels, tt.IsDryRun)
|
||||||
if statusErr.ErrStatus.Code != tt.ExpectStatusCode {
|
} else {
|
||||||
t.Errorf("%s: expected status code %d, got %d", tt.Name, tt.ExpectStatusCode, statusErr.ErrStatus.Code)
|
attr = webhooktesting.NewAttribute(ns, tt.AdditionalLabels, tt.IsDryRun)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
|
err = wh.Admit(attr, objectInterfaces)
|
||||||
if !ok {
|
if tt.ExpectAllow != (err == nil) {
|
||||||
t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
|
t.Errorf("expected allowed=%v, but got err=%v", tt.ExpectAllow, err)
|
||||||
continue
|
}
|
||||||
}
|
if tt.ExpectLabels != nil {
|
||||||
if len(tt.ExpectAnnotations) == 0 {
|
if !reflect.DeepEqual(tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels()) {
|
||||||
assert.Empty(t, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
|
t.Errorf("expected labels '%v', but got '%v'", tt.ExpectLabels, attr.GetObject().(metav1.Object).GetLabels())
|
||||||
} else {
|
}
|
||||||
assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
|
}
|
||||||
}
|
// ErrWebhookRejected is not an error for our purposes
|
||||||
|
if tt.ErrorContains != "" {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), tt.ErrorContains) {
|
||||||
|
t.Errorf("expected an error saying %q, but got: %v", tt.ErrorContains, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if statusErr, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
|
||||||
|
t.Errorf("expected a StatusError, got %T", err)
|
||||||
|
} else if isStatusErr {
|
||||||
|
if statusErr.ErrStatus.Code != tt.ExpectStatusCode {
|
||||||
|
t.Errorf("expected status code %d, got %d", tt.ExpectStatusCode, statusErr.ErrStatus.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(tt.ExpectAnnotations) == 0 {
|
||||||
|
assert.Empty(t, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
|
||||||
|
}
|
||||||
|
reinvocationCtx := fakeAttr.Attributes.GetReinvocationContext()
|
||||||
|
reinvocationCtx.SetIsReinvoke()
|
||||||
|
for webhook, expectReinvoke := range tt.ExpectReinvokeWebhooks {
|
||||||
|
shouldReinvoke := reinvocationCtx.Value(PluginName).(*webhookReinvokeContext).ShouldReinvokeWebhook(webhook)
|
||||||
|
if expectReinvoke != shouldReinvoke {
|
||||||
|
t.Errorf("expected reinvocationContext.ShouldReinvokeWebhook(%s)=%t, but got %t", webhook, expectReinvoke, shouldReinvoke)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,7 +146,7 @@ func TestAdmitCachedClient(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
|
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
|
||||||
ns := "webhook-test"
|
ns := "webhook-test"
|
||||||
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh)
|
client, informer := webhooktesting.NewFakeMutatingDataSource(ns, webhooktesting.ConvertToMutatingWebhooks(tt.Webhooks), stopCh)
|
||||||
|
|
||||||
// override the webhook source. The client cache will stay the same.
|
// override the webhook source. The client cache will stay the same.
|
||||||
cacheMisses := new(int32)
|
cacheMisses := new(int32)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 mutating
|
||||||
|
|
||||||
|
import (
|
||||||
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
)
|
||||||
|
|
||||||
|
type webhookReinvokeContext struct {
|
||||||
|
// lastWebhookOutput holds the result of the last webhook admission plugin call
|
||||||
|
lastWebhookOutput runtime.Object
|
||||||
|
// previouslyInvokedReinvocableWebhooks holds the set of webhooks that have been invoked and
|
||||||
|
// should be reinvoked if a later mutation occurs
|
||||||
|
previouslyInvokedReinvocableWebhooks sets.String
|
||||||
|
// reinvokeWebhooks holds the set of webhooks that should be reinvoked
|
||||||
|
reinvokeWebhooks sets.String
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) ShouldReinvokeWebhook(webhook string) bool {
|
||||||
|
return rc.reinvokeWebhooks.Has(webhook)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) IsOutputChangedSinceLastWebhookInvocation(object runtime.Object) bool {
|
||||||
|
return !apiequality.Semantic.DeepEqual(rc.lastWebhookOutput, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) SetLastWebhookInvocationOutput(object runtime.Object) {
|
||||||
|
if object == nil {
|
||||||
|
rc.lastWebhookOutput = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rc.lastWebhookOutput = object.DeepCopyObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) AddReinvocableWebhookToPreviouslyInvoked(webhook string) {
|
||||||
|
if rc.previouslyInvokedReinvocableWebhooks == nil {
|
||||||
|
rc.previouslyInvokedReinvocableWebhooks = sets.NewString()
|
||||||
|
}
|
||||||
|
rc.previouslyInvokedReinvocableWebhooks.Insert(webhook)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *webhookReinvokeContext) RequireReinvokingPreviouslyInvokedPlugins() {
|
||||||
|
if len(rc.previouslyInvokedReinvocableWebhooks) > 0 {
|
||||||
|
if rc.reinvokeWebhooks == nil {
|
||||||
|
rc.reinvokeWebhooks = sets.NewString()
|
||||||
|
}
|
||||||
|
for s := range rc.previouslyInvokedReinvocableWebhooks {
|
||||||
|
rc.reinvokeWebhooks.Insert(s)
|
||||||
|
}
|
||||||
|
rc.previouslyInvokedReinvocableWebhooks = sets.NewString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,13 +19,13 @@ package namespace
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"k8s.io/api/admissionregistration/v1beta1"
|
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
corelisters "k8s.io/client-go/listers/core/v1"
|
corelisters "k8s.io/client-go/listers/core/v1"
|
||||||
)
|
)
|
||||||
|
|
@ -86,7 +86,7 @@ func (m *Matcher) GetNamespaceLabels(attr admission.Attributes) (map[string]stri
|
||||||
|
|
||||||
// MatchNamespaceSelector decideds whether the request matches the
|
// MatchNamespaceSelector decideds whether the request matches the
|
||||||
// namespaceSelctor of the webhook. Only when they match, the webhook is called.
|
// namespaceSelctor of the webhook. Only when they match, the webhook is called.
|
||||||
func (m *Matcher) MatchNamespaceSelector(h *v1beta1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
|
func (m *Matcher) MatchNamespaceSelector(h webhook.WebhookAccessor, attr admission.Attributes) (bool, *apierrors.StatusError) {
|
||||||
namespaceName := attr.GetNamespace()
|
namespaceName := attr.GetNamespace()
|
||||||
if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" {
|
if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" {
|
||||||
// If the request is about a cluster scoped resource, and it is not a
|
// If the request is about a cluster scoped resource, and it is not a
|
||||||
|
|
@ -96,7 +96,7 @@ func (m *Matcher) MatchNamespaceSelector(h *v1beta1.Webhook, attr admission.Attr
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
// TODO: adding an LRU cache to cache the translation
|
// TODO: adding an LRU cache to cache the translation
|
||||||
selector, err := metav1.LabelSelectorAsSelector(h.NamespaceSelector)
|
selector, err := metav1.LabelSelectorAsSelector(h.GetNamespaceSelector())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, apierrors.NewInternalError(err)
|
return false, apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"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/admission/plugin/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeNamespaceLister struct {
|
type fakeNamespaceLister struct {
|
||||||
|
|
@ -114,12 +115,12 @@ func TestGetNamespaceLabels(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNotExemptClusterScopedResource(t *testing.T) {
|
func TestNotExemptClusterScopedResource(t *testing.T) {
|
||||||
hook := ®istrationv1beta1.Webhook{
|
hook := ®istrationv1beta1.ValidatingWebhook{
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
}
|
}
|
||||||
attr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "mock-name", schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "", admission.Create, &metav1.CreateOptions{}, false, nil)
|
attr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "mock-name", schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||||
matcher := Matcher{}
|
matcher := Matcher{}
|
||||||
matches, err := matcher.MatchNamespaceSelector(hook, attr)
|
matches, err := matcher.MatchNamespaceSelector(webhook.NewValidatingWebhookAccessor("mock-hook", hook), attr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 object defines the utilities that are used by the webhook plugin to
|
||||||
|
// decide if a webhook should run, as long as either the old object or the new
|
||||||
|
// object has labels matching the webhook config's objectSelector.
|
||||||
|
package object // import "k8s.io/apiserver/pkg/admission/plugin/webhook/object"
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 object
|
||||||
|
|
||||||
|
import (
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
|
"k8s.io/klog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Matcher decides if a request selected by the ObjectSelector.
|
||||||
|
type Matcher struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchObject(obj runtime.Object, selector labels.Selector) bool {
|
||||||
|
if obj == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
accessor, err := meta.Accessor(obj)
|
||||||
|
if err != nil {
|
||||||
|
klog.V(5).Infof("cannot access metadata of %v: %v", obj, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return selector.Matches(labels.Set(accessor.GetLabels()))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchObjectSelector decideds whether the request matches the ObjectSelector
|
||||||
|
// of the webhook. Only when they match, the webhook is called.
|
||||||
|
func (m *Matcher) MatchObjectSelector(h webhook.WebhookAccessor, attr admission.Attributes) (bool, *apierrors.StatusError) {
|
||||||
|
// TODO: adding an LRU cache to cache the translation
|
||||||
|
selector, err := metav1.LabelSelectorAsSelector(h.GetObjectSelector())
|
||||||
|
if err != nil {
|
||||||
|
return false, apierrors.NewInternalError(err)
|
||||||
|
}
|
||||||
|
if selector.Empty() {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return matchObject(attr.GetObject(), selector) || matchObject(attr.GetOldObject(), selector), nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 object
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/api/admissionregistration/v1beta1"
|
||||||
|
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"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObjectSelector(t *testing.T) {
|
||||||
|
nodeLevel1 := &corev1.Node{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"runlevel": "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
nodeLevel2 := &corev1.Node{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"runlevel": "2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
runLevel1Excluder := &metav1.LabelSelector{
|
||||||
|
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||||
|
{
|
||||||
|
Key: "runlevel",
|
||||||
|
Operator: metav1.LabelSelectorOpNotIn,
|
||||||
|
Values: []string{"1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
matcher := &Matcher{}
|
||||||
|
allScopes := v1beta1.AllScopes
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
objectSelector *metav1.LabelSelector
|
||||||
|
attrs admission.Attributes
|
||||||
|
|
||||||
|
expectCall bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty object selector matches everything",
|
||||||
|
objectSelector: &metav1.LabelSelector{},
|
||||||
|
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
|
expectCall: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matches new object",
|
||||||
|
objectSelector: runLevel1Excluder,
|
||||||
|
attrs: admission.NewAttributesRecord(nodeLevel2, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
|
expectCall: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matches old object",
|
||||||
|
objectSelector: runLevel1Excluder,
|
||||||
|
attrs: admission.NewAttributesRecord(nil, nodeLevel2, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Delete, &metav1.DeleteOptions{}, false, nil),
|
||||||
|
expectCall: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not match new object",
|
||||||
|
objectSelector: runLevel1Excluder,
|
||||||
|
attrs: admission.NewAttributesRecord(nodeLevel1, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
|
expectCall: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not match old object",
|
||||||
|
objectSelector: runLevel1Excluder,
|
||||||
|
attrs: admission.NewAttributesRecord(nil, nodeLevel1, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
|
expectCall: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "does not match object that does not implement Object interface",
|
||||||
|
objectSelector: runLevel1Excluder,
|
||||||
|
attrs: admission.NewAttributesRecord(&corev1.NodeProxyOptions{}, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
|
expectCall: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty selector matches everything, including object that does not implement Object interface",
|
||||||
|
objectSelector: &metav1.LabelSelector{},
|
||||||
|
attrs: admission.NewAttributesRecord(&corev1.NodeProxyOptions{}, nil, schema.GroupVersionKind{}, "", "name", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
|
||||||
|
expectCall: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
hook := &v1beta1.ValidatingWebhook{
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: testcase.objectSelector,
|
||||||
|
Rules: []v1beta1.RuleWithOperations{{
|
||||||
|
Operations: []v1beta1.OperationType{"*"},
|
||||||
|
Rule: v1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*"}, Scope: &allScopes},
|
||||||
|
}}}
|
||||||
|
|
||||||
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
match, err := matcher.MatchObjectSelector(webhook.NewValidatingWebhookAccessor("mock-hook", hook), testcase.attrs)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if testcase.expectCall && !match {
|
||||||
|
t.Errorf("expected the webhook to be called")
|
||||||
|
}
|
||||||
|
if !testcase.expectCall && match {
|
||||||
|
t.Errorf("expected the webhook to be called")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,8 +49,11 @@ var sideEffectsNone = registrationv1beta1.SideEffectClassNone
|
||||||
var sideEffectsSome = registrationv1beta1.SideEffectClassSome
|
var sideEffectsSome = registrationv1beta1.SideEffectClassSome
|
||||||
var sideEffectsNoneOnDryRun = registrationv1beta1.SideEffectClassNoneOnDryRun
|
var sideEffectsNoneOnDryRun = registrationv1beta1.SideEffectClassNoneOnDryRun
|
||||||
|
|
||||||
// NewFakeDataSource returns a mock client and informer returning the given webhooks.
|
var reinvokeNever = registrationv1beta1.NeverReinvocationPolicy
|
||||||
func NewFakeDataSource(name string, webhooks []registrationv1beta1.Webhook, mutating bool, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
|
var reinvokeIfNeeded = registrationv1beta1.IfNeededReinvocationPolicy
|
||||||
|
|
||||||
|
// NewFakeValidatingDataSource returns a mock client and informer returning the given webhooks.
|
||||||
|
func NewFakeValidatingDataSource(name string, webhooks []registrationv1beta1.ValidatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
|
||||||
var objs = []runtime.Object{
|
var objs = []runtime.Object{
|
||||||
&corev1.Namespace{
|
&corev1.Namespace{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
|
@ -61,21 +64,37 @@ func NewFakeDataSource(name string, webhooks []registrationv1beta1.Webhook, muta
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if mutating {
|
objs = append(objs, ®istrationv1beta1.ValidatingWebhookConfiguration{
|
||||||
objs = append(objs, ®istrationv1beta1.MutatingWebhookConfiguration{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-webhooks",
|
||||||
|
},
|
||||||
|
Webhooks: webhooks,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := fakeclientset.NewSimpleClientset(objs...)
|
||||||
|
informerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||||
|
|
||||||
|
return client, informerFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFakeMutatingDataSource returns a mock client and informer returning the given webhooks.
|
||||||
|
func NewFakeMutatingDataSource(name string, webhooks []registrationv1beta1.MutatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
|
||||||
|
var objs = []runtime.Object{
|
||||||
|
&corev1.Namespace{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "test-webhooks",
|
Name: name,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"runlevel": "0",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Webhooks: webhooks,
|
},
|
||||||
})
|
|
||||||
} else {
|
|
||||||
objs = append(objs, ®istrationv1beta1.ValidatingWebhookConfiguration{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test-webhooks",
|
|
||||||
},
|
|
||||||
Webhooks: webhooks,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
objs = append(objs, ®istrationv1beta1.MutatingWebhookConfiguration{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-webhooks",
|
||||||
|
},
|
||||||
|
Webhooks: webhooks,
|
||||||
|
})
|
||||||
|
|
||||||
client := fakeclientset.NewSimpleClientset(objs...)
|
client := fakeclientset.NewSimpleClientset(objs...)
|
||||||
informerFactory := informers.NewSharedInformerFactory(client, 0)
|
informerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||||
|
|
@ -181,51 +200,88 @@ func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1beta1.WebhookC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test is a webhook test case.
|
// ValidatingTest is a validating webhook test case.
|
||||||
type Test struct {
|
type ValidatingTest struct {
|
||||||
Name string
|
Name string
|
||||||
Webhooks []registrationv1beta1.Webhook
|
Webhooks []registrationv1beta1.ValidatingWebhook
|
||||||
Path string
|
Path string
|
||||||
IsCRD bool
|
IsCRD bool
|
||||||
IsDryRun bool
|
IsDryRun bool
|
||||||
AdditionalLabels map[string]string
|
AdditionalLabels map[string]string
|
||||||
ExpectLabels map[string]string
|
ExpectLabels map[string]string
|
||||||
ExpectAllow bool
|
ExpectAllow bool
|
||||||
ErrorContains string
|
ErrorContains string
|
||||||
ExpectAnnotations map[string]string
|
ExpectAnnotations map[string]string
|
||||||
ExpectStatusCode int32
|
ExpectStatusCode int32
|
||||||
|
ExpectReinvokeWebhooks map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MutatingTest is a mutating webhook test case.
|
||||||
|
type MutatingTest struct {
|
||||||
|
Name string
|
||||||
|
Webhooks []registrationv1beta1.MutatingWebhook
|
||||||
|
Path string
|
||||||
|
IsCRD bool
|
||||||
|
IsDryRun bool
|
||||||
|
AdditionalLabels map[string]string
|
||||||
|
ExpectLabels map[string]string
|
||||||
|
ExpectAllow bool
|
||||||
|
ErrorContains string
|
||||||
|
ExpectAnnotations map[string]string
|
||||||
|
ExpectStatusCode int32
|
||||||
|
ExpectReinvokeWebhooks map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToMutatingTestCases converts a validating test case to a mutating one for test purposes.
|
||||||
|
func ConvertToMutatingTestCases(tests []ValidatingTest) []MutatingTest {
|
||||||
|
r := make([]MutatingTest, len(tests))
|
||||||
|
for i, t := range tests {
|
||||||
|
r[i] = MutatingTest{t.Name, ConvertToMutatingWebhooks(t.Webhooks), t.Path, t.IsCRD, t.IsDryRun, t.AdditionalLabels, t.ExpectLabels, t.ExpectAllow, t.ErrorContains, t.ExpectAnnotations, t.ExpectStatusCode, t.ExpectReinvokeWebhooks}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToMutatingWebhooks converts a validating webhook to a mutating one for test purposes.
|
||||||
|
func ConvertToMutatingWebhooks(webhooks []registrationv1beta1.ValidatingWebhook) []registrationv1beta1.MutatingWebhook {
|
||||||
|
mutating := make([]registrationv1beta1.MutatingWebhook, len(webhooks))
|
||||||
|
for i, h := range webhooks {
|
||||||
|
mutating[i] = registrationv1beta1.MutatingWebhook{h.Name, h.ClientConfig, h.Rules, h.FailurePolicy, h.MatchPolicy, h.NamespaceSelector, h.ObjectSelector, h.SideEffects, h.TimeoutSeconds, h.AdmissionReviewVersions, nil}
|
||||||
|
}
|
||||||
|
return mutating
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewNonMutatingTestCases returns test cases with a given base url.
|
// NewNonMutatingTestCases returns test cases with a given base url.
|
||||||
// All test cases in NewNonMutatingTestCases have no Patch set in
|
// All test cases in NewNonMutatingTestCases have no Patch set in
|
||||||
// AdmissionResponse. The test cases are used by both MutatingAdmissionWebhook
|
// AdmissionResponse. The test cases are used by both MutatingAdmissionWebhook
|
||||||
// and ValidatingAdmissionWebhook.
|
// and ValidatingAdmissionWebhook.
|
||||||
func NewNonMutatingTestCases(url *url.URL) []Test {
|
func NewNonMutatingTestCases(url *url.URL) []ValidatingTest {
|
||||||
policyFail := registrationv1beta1.Fail
|
policyFail := registrationv1beta1.Fail
|
||||||
policyIgnore := registrationv1beta1.Ignore
|
policyIgnore := registrationv1beta1.Ignore
|
||||||
ccfgURL := urlConfigGenerator{url}.ccfgURL
|
ccfgURL := urlConfigGenerator{url}.ccfgURL
|
||||||
|
|
||||||
return []Test{
|
return []ValidatingTest{
|
||||||
{
|
{
|
||||||
Name: "no match",
|
Name: "no match",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "nomatch",
|
Name: "nomatch",
|
||||||
ClientConfig: ccfgSVC("disallow"),
|
ClientConfig: ccfgSVC("disallow"),
|
||||||
Rules: []registrationv1beta1.RuleWithOperations{{
|
Rules: []registrationv1beta1.RuleWithOperations{{
|
||||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.Create},
|
Operations: []registrationv1beta1.OperationType{registrationv1beta1.Create},
|
||||||
}},
|
}},
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectAllow: true,
|
ExpectAllow: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & allow",
|
Name: "match & allow",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "allow.example.com",
|
Name: "allow.example.com",
|
||||||
ClientConfig: ccfgSVC("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectAllow: true,
|
ExpectAllow: true,
|
||||||
|
|
@ -233,11 +289,12 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & disallow",
|
Name: "match & disallow",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "disallow",
|
Name: "disallow",
|
||||||
ClientConfig: ccfgSVC("disallow"),
|
ClientConfig: ccfgSVC("disallow"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectStatusCode: http.StatusForbidden,
|
ExpectStatusCode: http.StatusForbidden,
|
||||||
|
|
@ -245,11 +302,12 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & disallow ii",
|
Name: "match & disallow ii",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "disallowReason",
|
Name: "disallowReason",
|
||||||
ClientConfig: ccfgSVC("disallowReason"),
|
ClientConfig: ccfgSVC("disallowReason"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectStatusCode: http.StatusForbidden,
|
ExpectStatusCode: http.StatusForbidden,
|
||||||
|
|
@ -257,7 +315,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & disallow & but allowed because namespaceSelector exempt the ns",
|
Name: "match & disallow & but allowed because namespaceSelector exempt the ns",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "disallow",
|
Name: "disallow",
|
||||||
ClientConfig: ccfgSVC("disallow"),
|
ClientConfig: ccfgSVC("disallow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
|
|
@ -268,6 +326,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
Operator: metav1.LabelSelectorOpIn,
|
Operator: metav1.LabelSelectorOpIn,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
||||||
|
|
@ -275,7 +334,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & disallow & but allowed because namespaceSelector exempt the ns ii",
|
Name: "match & disallow & but allowed because namespaceSelector exempt the ns ii",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "disallow",
|
Name: "disallow",
|
||||||
ClientConfig: ccfgSVC("disallow"),
|
ClientConfig: ccfgSVC("disallow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
|
|
@ -286,17 +345,19 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
Operator: metav1.LabelSelectorOpNotIn,
|
Operator: metav1.LabelSelectorOpNotIn,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectAllow: true,
|
ExpectAllow: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & fail (but allow because fail open)",
|
Name: "match & fail (but allow because fail open)",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "internalErr A",
|
Name: "internalErr A",
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}, {
|
}, {
|
||||||
|
|
@ -304,6 +365,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}, {
|
}, {
|
||||||
|
|
@ -311,6 +373,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -319,22 +382,25 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & fail (but disallow because fail close on nil FailurePolicy)",
|
Name: "match & fail (but disallow because fail close on nil FailurePolicy)",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "internalErr A",
|
Name: "internalErr A",
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}, {
|
}, {
|
||||||
Name: "internalErr B",
|
Name: "internalErr B",
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}, {
|
}, {
|
||||||
Name: "internalErr C",
|
Name: "internalErr C",
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -343,11 +409,12 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & fail (but fail because fail closed)",
|
Name: "match & fail (but fail because fail closed)",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "internalErr A",
|
Name: "internalErr A",
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyFail,
|
FailurePolicy: &policyFail,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}, {
|
}, {
|
||||||
|
|
@ -355,6 +422,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyFail,
|
FailurePolicy: &policyFail,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}, {
|
}, {
|
||||||
|
|
@ -362,6 +430,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyFail,
|
FailurePolicy: &policyFail,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -370,11 +439,12 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & allow (url)",
|
Name: "match & allow (url)",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "allow.example.com",
|
Name: "allow.example.com",
|
||||||
ClientConfig: ccfgURL("allow"),
|
ClientConfig: ccfgURL("allow"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectAllow: true,
|
ExpectAllow: true,
|
||||||
|
|
@ -382,35 +452,38 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & disallow (url)",
|
Name: "match & disallow (url)",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "disallow",
|
Name: "disallow",
|
||||||
ClientConfig: ccfgURL("disallow"),
|
ClientConfig: ccfgURL("disallow"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectStatusCode: http.StatusForbidden,
|
ExpectStatusCode: http.StatusForbidden,
|
||||||
ErrorContains: "without explanation",
|
ErrorContains: "without explanation",
|
||||||
}, {
|
}, {
|
||||||
Name: "absent response and fail open",
|
Name: "absent response and fail open",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "nilResponse",
|
Name: "nilResponse",
|
||||||
ClientConfig: ccfgURL("nilResponse"),
|
ClientConfig: ccfgURL("nilResponse"),
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectAllow: true,
|
ExpectAllow: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "absent response and fail closed",
|
Name: "absent response and fail closed",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "nilResponse",
|
Name: "nilResponse",
|
||||||
ClientConfig: ccfgURL("nilResponse"),
|
ClientConfig: ccfgURL("nilResponse"),
|
||||||
FailurePolicy: &policyFail,
|
FailurePolicy: &policyFail,
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectStatusCode: http.StatusInternalServerError,
|
ExpectStatusCode: http.StatusInternalServerError,
|
||||||
|
|
@ -418,13 +491,14 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "no match dry run",
|
Name: "no match dry run",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "nomatch",
|
Name: "nomatch",
|
||||||
ClientConfig: ccfgSVC("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: []registrationv1beta1.RuleWithOperations{{
|
Rules: []registrationv1beta1.RuleWithOperations{{
|
||||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.Create},
|
Operations: []registrationv1beta1.OperationType{registrationv1beta1.Create},
|
||||||
}},
|
}},
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
SideEffects: &sideEffectsSome,
|
SideEffects: &sideEffectsSome,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -433,11 +507,12 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match dry run side effects Unknown",
|
Name: "match dry run side effects Unknown",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "allow",
|
Name: "allow",
|
||||||
ClientConfig: ccfgSVC("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
SideEffects: &sideEffectsUnknown,
|
SideEffects: &sideEffectsUnknown,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -447,11 +522,12 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match dry run side effects None",
|
Name: "match dry run side effects None",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "allow",
|
Name: "allow",
|
||||||
ClientConfig: ccfgSVC("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
SideEffects: &sideEffectsNone,
|
SideEffects: &sideEffectsNone,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -461,11 +537,12 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match dry run side effects Some",
|
Name: "match dry run side effects Some",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "allow",
|
Name: "allow",
|
||||||
ClientConfig: ccfgSVC("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
SideEffects: &sideEffectsSome,
|
SideEffects: &sideEffectsSome,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -475,11 +552,12 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match dry run side effects NoneOnDryRun",
|
Name: "match dry run side effects NoneOnDryRun",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "allow",
|
Name: "allow",
|
||||||
ClientConfig: ccfgSVC("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
SideEffects: &sideEffectsNoneOnDryRun,
|
SideEffects: &sideEffectsNoneOnDryRun,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -489,15 +567,40 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "illegal annotation format",
|
Name: "illegal annotation format",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "invalidAnnotation",
|
Name: "invalidAnnotation",
|
||||||
ClientConfig: ccfgURL("invalidAnnotation"),
|
ClientConfig: ccfgURL("invalidAnnotation"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectAllow: true,
|
ExpectAllow: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "skip webhook whose objectSelector does not match",
|
||||||
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
|
Name: "allow.example.com",
|
||||||
|
ClientConfig: ccfgSVC("allow"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
}, {
|
||||||
|
Name: "shouldNotBeCalled",
|
||||||
|
ClientConfig: ccfgSVC("shouldNotBeCalled"),
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{
|
||||||
|
"label": "nonexistent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
}},
|
||||||
|
ExpectAllow: true,
|
||||||
|
ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"},
|
||||||
|
},
|
||||||
// No need to test everything with the url case, since only the
|
// No need to test everything with the url case, since only the
|
||||||
// connection is different.
|
// connection is different.
|
||||||
}
|
}
|
||||||
|
|
@ -506,15 +609,16 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
|
||||||
// NewMutatingTestCases returns test cases with a given base url.
|
// NewMutatingTestCases returns test cases with a given base url.
|
||||||
// All test cases in NewMutatingTestCases have Patch set in
|
// All test cases in NewMutatingTestCases have Patch set in
|
||||||
// AdmissionResponse. The test cases are only used by both MutatingAdmissionWebhook.
|
// AdmissionResponse. The test cases are only used by both MutatingAdmissionWebhook.
|
||||||
func NewMutatingTestCases(url *url.URL) []Test {
|
func NewMutatingTestCases(url *url.URL) []MutatingTest {
|
||||||
return []Test{
|
return []MutatingTest{
|
||||||
{
|
{
|
||||||
Name: "match & remove label",
|
Name: "match & remove label",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
Name: "removelabel.example.com",
|
Name: "removelabel.example.com",
|
||||||
ClientConfig: ccfgSVC("removeLabel"),
|
ClientConfig: ccfgSVC("removeLabel"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectAllow: true,
|
ExpectAllow: true,
|
||||||
|
|
@ -524,11 +628,12 @@ func NewMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & add label",
|
Name: "match & add label",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
Name: "addLabel",
|
Name: "addLabel",
|
||||||
ClientConfig: ccfgSVC("addLabel"),
|
ClientConfig: ccfgSVC("addLabel"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectAllow: true,
|
ExpectAllow: true,
|
||||||
|
|
@ -536,11 +641,12 @@ func NewMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match CRD & add label",
|
Name: "match CRD & add label",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
Name: "addLabel",
|
Name: "addLabel",
|
||||||
ClientConfig: ccfgSVC("addLabel"),
|
ClientConfig: ccfgSVC("addLabel"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
IsCRD: true,
|
IsCRD: true,
|
||||||
|
|
@ -549,11 +655,12 @@ func NewMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match CRD & remove label",
|
Name: "match CRD & remove label",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
Name: "removelabel.example.com",
|
Name: "removelabel.example.com",
|
||||||
ClientConfig: ccfgSVC("removeLabel"),
|
ClientConfig: ccfgSVC("removeLabel"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
IsCRD: true,
|
IsCRD: true,
|
||||||
|
|
@ -564,11 +671,12 @@ func NewMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & invalid mutation",
|
Name: "match & invalid mutation",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
Name: "invalidMutation",
|
Name: "invalidMutation",
|
||||||
ClientConfig: ccfgSVC("invalidMutation"),
|
ClientConfig: ccfgSVC("invalidMutation"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
ExpectStatusCode: http.StatusInternalServerError,
|
ExpectStatusCode: http.StatusInternalServerError,
|
||||||
|
|
@ -576,11 +684,12 @@ func NewMutatingTestCases(url *url.URL) []Test {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "match & remove label dry run unsupported",
|
Name: "match & remove label dry run unsupported",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
Name: "removeLabel",
|
Name: "removeLabel",
|
||||||
ClientConfig: ccfgSVC("removeLabel"),
|
ClientConfig: ccfgSVC("removeLabel"),
|
||||||
Rules: matchEverythingRules,
|
Rules: matchEverythingRules,
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
SideEffects: &sideEffectsUnknown,
|
SideEffects: &sideEffectsUnknown,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -588,15 +697,107 @@ func NewMutatingTestCases(url *url.URL) []Test {
|
||||||
ExpectStatusCode: http.StatusBadRequest,
|
ExpectStatusCode: http.StatusBadRequest,
|
||||||
ErrorContains: "does not support dry run",
|
ErrorContains: "does not support dry run",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "first webhook remove labels, second webhook shouldn't be called",
|
||||||
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
|
Name: "removelabel.example.com",
|
||||||
|
ClientConfig: ccfgSVC("removeLabel"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{
|
||||||
|
"remove": "me",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
}, {
|
||||||
|
Name: "shouldNotBeCalled",
|
||||||
|
ClientConfig: ccfgSVC("shouldNotBeCalled"),
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{
|
||||||
|
"remove": "me",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
}},
|
||||||
|
ExpectAllow: true,
|
||||||
|
AdditionalLabels: map[string]string{"remove": "me"},
|
||||||
|
ExpectLabels: map[string]string{"pod.name": "my-pod"},
|
||||||
|
ExpectAnnotations: map[string]string{"removelabel.example.com/key1": "value1"},
|
||||||
|
},
|
||||||
// No need to test everything with the url case, since only the
|
// No need to test everything with the url case, since only the
|
||||||
// connection is different.
|
// connection is different.
|
||||||
|
{
|
||||||
|
Name: "match & reinvoke if needed policy",
|
||||||
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
|
Name: "addLabel",
|
||||||
|
ClientConfig: ccfgSVC("addLabel"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
ReinvocationPolicy: &reinvokeIfNeeded,
|
||||||
|
}, {
|
||||||
|
Name: "removeLabel",
|
||||||
|
ClientConfig: ccfgSVC("removeLabel"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
ReinvocationPolicy: &reinvokeIfNeeded,
|
||||||
|
}},
|
||||||
|
AdditionalLabels: map[string]string{"remove": "me"},
|
||||||
|
ExpectAllow: true,
|
||||||
|
ExpectReinvokeWebhooks: map[string]bool{"addLabel": true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "match & never reinvoke policy",
|
||||||
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
|
Name: "addLabel",
|
||||||
|
ClientConfig: ccfgSVC("addLabel"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
ReinvocationPolicy: &reinvokeNever,
|
||||||
|
}},
|
||||||
|
ExpectAllow: true,
|
||||||
|
ExpectReinvokeWebhooks: map[string]bool{"addLabel": false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "match & never reinvoke policy (by default)",
|
||||||
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
|
Name: "addLabel",
|
||||||
|
ClientConfig: ccfgSVC("addLabel"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
}},
|
||||||
|
ExpectAllow: true,
|
||||||
|
ExpectReinvokeWebhooks: map[string]bool{"addLabel": false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "match & no reinvoke",
|
||||||
|
Webhooks: []registrationv1beta1.MutatingWebhook{{
|
||||||
|
Name: "noop",
|
||||||
|
ClientConfig: ccfgSVC("noop"),
|
||||||
|
Rules: matchEverythingRules,
|
||||||
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
|
}},
|
||||||
|
ExpectAllow: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CachedTest is a test case for the client manager.
|
// CachedTest is a test case for the client manager.
|
||||||
type CachedTest struct {
|
type CachedTest struct {
|
||||||
Name string
|
Name string
|
||||||
Webhooks []registrationv1beta1.Webhook
|
Webhooks []registrationv1beta1.ValidatingWebhook
|
||||||
ExpectAllow bool
|
ExpectAllow bool
|
||||||
ExpectCacheMiss bool
|
ExpectCacheMiss bool
|
||||||
}
|
}
|
||||||
|
|
@ -609,11 +810,12 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
|
||||||
return []CachedTest{
|
return []CachedTest{
|
||||||
{
|
{
|
||||||
Name: "uncached: service webhook, path 'allow'",
|
Name: "uncached: service webhook, path 'allow'",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "cache1",
|
Name: "cache1",
|
||||||
ClientConfig: ccfgSVC("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -622,11 +824,12 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "uncached: service webhook, path 'internalErr'",
|
Name: "uncached: service webhook, path 'internalErr'",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "cache2",
|
Name: "cache2",
|
||||||
ClientConfig: ccfgSVC("internalErr"),
|
ClientConfig: ccfgSVC("internalErr"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -635,11 +838,12 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "cached: service webhook, path 'allow'",
|
Name: "cached: service webhook, path 'allow'",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "cache3",
|
Name: "cache3",
|
||||||
ClientConfig: ccfgSVC("allow"),
|
ClientConfig: ccfgSVC("allow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -648,11 +852,12 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "uncached: url webhook, path 'allow'",
|
Name: "uncached: url webhook, path 'allow'",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "cache4",
|
Name: "cache4",
|
||||||
ClientConfig: ccfgURL("allow"),
|
ClientConfig: ccfgURL("allow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
@ -661,11 +866,12 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "cached: service webhook, path 'allow'",
|
Name: "cached: service webhook, path 'allow'",
|
||||||
Webhooks: []registrationv1beta1.Webhook{{
|
Webhooks: []registrationv1beta1.ValidatingWebhook{{
|
||||||
Name: "cache5",
|
Name: "cache5",
|
||||||
ClientConfig: ccfgURL("allow"),
|
ClientConfig: ccfgURL("allow"),
|
||||||
Rules: newMatchEverythingRules(),
|
Rules: newMatchEverythingRules(),
|
||||||
NamespaceSelector: &metav1.LabelSelector{},
|
NamespaceSelector: &metav1.LabelSelector{},
|
||||||
|
ObjectSelector: &metav1.LabelSelector{},
|
||||||
FailurePolicy: &policyIgnore,
|
FailurePolicy: &policyIgnore,
|
||||||
AdmissionReviewVersions: []string{"v1beta1"},
|
AdmissionReviewVersions: []string{"v1beta1"},
|
||||||
}},
|
}},
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,17 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
case "/shouldNotBeCalled":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
|
||||||
|
Response: &v1beta1.AdmissionResponse{
|
||||||
|
Allowed: false,
|
||||||
|
Result: &metav1.Status{
|
||||||
|
Message: "doesn't expect labels to match object selector",
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
case "/allow":
|
case "/allow":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
|
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
|
||||||
|
|
@ -138,6 +149,13 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
case "/noop":
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
|
||||||
|
Response: &v1beta1.AdmissionResponse{
|
||||||
|
Allowed: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,38 +17,39 @@ limitations under the License.
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"k8s.io/api/admissionregistration/v1beta1"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookClientConfigForWebhook construct a webhook.ClientConfig using a v1beta1.Webhook API object.
|
// HookClientConfigForWebhook construct a webhookutil.ClientConfig using a WebhookAccessor to access
|
||||||
// webhook.ClientConfig is used to create a HookClient and the purpose of the config struct is to
|
// v1beta1.MutatingWebhook and v1beta1.ValidatingWebhook API objects. webhookutil.ClientConfig is used
|
||||||
// share that with other packages that need to create a HookClient.
|
// to create a HookClient and the purpose of the config struct is to share that with other packages
|
||||||
func HookClientConfigForWebhook(w *v1beta1.Webhook) webhook.ClientConfig {
|
// that need to create a HookClient.
|
||||||
ret := webhook.ClientConfig{Name: w.Name, CABundle: w.ClientConfig.CABundle}
|
func HookClientConfigForWebhook(w webhook.WebhookAccessor) webhookutil.ClientConfig {
|
||||||
if w.ClientConfig.URL != nil {
|
ret := webhookutil.ClientConfig{Name: w.GetName(), CABundle: w.GetClientConfig().CABundle}
|
||||||
ret.URL = *w.ClientConfig.URL
|
if w.GetClientConfig().URL != nil {
|
||||||
|
ret.URL = *w.GetClientConfig().URL
|
||||||
}
|
}
|
||||||
if w.ClientConfig.Service != nil {
|
if w.GetClientConfig().Service != nil {
|
||||||
ret.Service = &webhook.ClientConfigService{
|
ret.Service = &webhookutil.ClientConfigService{
|
||||||
Name: w.ClientConfig.Service.Name,
|
Name: w.GetClientConfig().Service.Name,
|
||||||
Namespace: w.ClientConfig.Service.Namespace,
|
Namespace: w.GetClientConfig().Service.Namespace,
|
||||||
}
|
}
|
||||||
if w.ClientConfig.Service.Port != nil {
|
if w.GetClientConfig().Service.Port != nil {
|
||||||
ret.Service.Port = *w.ClientConfig.Service.Port
|
ret.Service.Port = *w.GetClientConfig().Service.Port
|
||||||
} else {
|
} else {
|
||||||
ret.Service.Port = 443
|
ret.Service.Port = 443
|
||||||
}
|
}
|
||||||
if w.ClientConfig.Service.Path != nil {
|
if w.GetClientConfig().Service.Path != nil {
|
||||||
ret.Service.Path = *w.ClientConfig.Service.Path
|
ret.Service.Path = *w.GetClientConfig().Service.Path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasAdmissionReviewVersion check whether a version is accepted by a given webhook.
|
// HasAdmissionReviewVersion check whether a version is accepted by a given webhook.
|
||||||
func HasAdmissionReviewVersion(a string, w *v1beta1.Webhook) bool {
|
func HasAdmissionReviewVersion(a string, w webhook.WebhookAccessor) bool {
|
||||||
for _, b := range w.AdmissionReviewVersions {
|
for _, b := range w.GetAdmissionReviewVersions() {
|
||||||
if b == a {
|
if b == a {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/klog"
|
|
||||||
|
|
||||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||||
"k8s.io/api/admissionregistration/v1beta1"
|
"k8s.io/api/admissionregistration/v1beta1"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
|
@ -31,36 +29,55 @@ import (
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||||
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
|
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/util"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/util"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||||
|
"k8s.io/klog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type validatingDispatcher struct {
|
type validatingDispatcher struct {
|
||||||
cm *webhook.ClientManager
|
cm *webhookutil.ClientManager
|
||||||
|
plugin *Plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
func newValidatingDispatcher(cm *webhook.ClientManager) generic.Dispatcher {
|
func newValidatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher {
|
||||||
return &validatingDispatcher{cm}
|
return func(cm *webhookutil.ClientManager) generic.Dispatcher {
|
||||||
|
return &validatingDispatcher{cm, p}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ generic.Dispatcher = &validatingDispatcher{}
|
var _ generic.Dispatcher = &validatingDispatcher{}
|
||||||
|
|
||||||
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, relevantHooks []*generic.WebhookInvocation) error {
|
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
|
||||||
|
var relevantHooks []*generic.WebhookInvocation
|
||||||
// Construct all the versions we need to call our webhooks
|
// Construct all the versions we need to call our webhooks
|
||||||
versionedAttrs := map[schema.GroupVersionKind]*generic.VersionedAttributes{}
|
versionedAttrs := map[schema.GroupVersionKind]*generic.VersionedAttributes{}
|
||||||
for _, call := range relevantHooks {
|
for _, hook := range hooks {
|
||||||
// If we already have this version, continue
|
invocation, statusError := d.plugin.ShouldCallHook(hook, attr, o)
|
||||||
if _, ok := versionedAttrs[call.Kind]; ok {
|
if statusError != nil {
|
||||||
|
return statusError
|
||||||
|
}
|
||||||
|
if invocation == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versionedAttr, err := generic.NewVersionedAttributes(attr, call.Kind, o)
|
relevantHooks = append(relevantHooks, invocation)
|
||||||
|
// If we already have this version, continue
|
||||||
|
if _, ok := versionedAttrs[invocation.Kind]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
versionedAttr, err := generic.NewVersionedAttributes(attr, invocation.Kind, o)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return apierrors.NewInternalError(err)
|
return apierrors.NewInternalError(err)
|
||||||
}
|
}
|
||||||
versionedAttrs[call.Kind] = versionedAttr
|
versionedAttrs[invocation.Kind] = versionedAttr
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(relevantHooks) == 0 {
|
||||||
|
// no matching hooks
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
|
@ -69,18 +86,21 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
|
||||||
for i := range relevantHooks {
|
for i := range relevantHooks {
|
||||||
go func(invocation *generic.WebhookInvocation) {
|
go func(invocation *generic.WebhookInvocation) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
hook := invocation.Webhook
|
hook, ok := invocation.Webhook.GetValidatingWebhook()
|
||||||
|
if !ok {
|
||||||
|
utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1beta1.ValidatingWebhook, but got %T", hook))
|
||||||
|
return
|
||||||
|
}
|
||||||
versionedAttr := versionedAttrs[invocation.Kind]
|
versionedAttr := versionedAttrs[invocation.Kind]
|
||||||
|
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
err := d.callHook(ctx, invocation, versionedAttr)
|
err := d.callHook(ctx, hook, invocation, versionedAttr)
|
||||||
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "validating", hook.Name)
|
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "validating", hook.Name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
|
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
|
||||||
if callErr, ok := err.(*webhook.ErrCallingWebhook); ok {
|
if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
|
||||||
if ignoreClientCallFailures {
|
if ignoreClientCallFailures {
|
||||||
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
|
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
|
||||||
utilruntime.HandleError(callErr)
|
utilruntime.HandleError(callErr)
|
||||||
|
|
@ -115,11 +135,10 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
|
||||||
return errs[0]
|
return errs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error {
|
func (d *validatingDispatcher) callHook(ctx context.Context, h *v1beta1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error {
|
||||||
h := invocation.Webhook
|
|
||||||
if attr.Attributes.IsDryRun() {
|
if attr.Attributes.IsDryRun() {
|
||||||
if h.SideEffects == nil {
|
if h.SideEffects == nil {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
|
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
|
||||||
}
|
}
|
||||||
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
|
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
|
||||||
return webhookerrors.NewDryRunUnsupportedErr(h.Name)
|
return webhookerrors.NewDryRunUnsupportedErr(h.Name)
|
||||||
|
|
@ -128,15 +147,15 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic
|
||||||
|
|
||||||
// Currently dispatcher only supports `v1beta1` AdmissionReview
|
// Currently dispatcher only supports `v1beta1` AdmissionReview
|
||||||
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
|
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
|
||||||
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, h) {
|
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReviewRequest")}
|
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReviewRequest")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the webhook request
|
// Make the webhook request
|
||||||
request := request.CreateAdmissionReview(attr, invocation)
|
request := request.CreateAdmissionReview(attr, invocation)
|
||||||
client, err := d.cm.HookClient(util.HookClientConfigForWebhook(h))
|
client, err := d.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||||
}
|
}
|
||||||
response := &admissionv1beta1.AdmissionReview{}
|
response := &admissionv1beta1.AdmissionReview{}
|
||||||
r := client.Post().Context(ctx).Body(&request)
|
r := client.Post().Context(ctx).Body(&request)
|
||||||
|
|
@ -144,11 +163,11 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic
|
||||||
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
|
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
|
||||||
}
|
}
|
||||||
if err := r.Do().Into(response); err != nil {
|
if err := r.Do().Into(response); err != nil {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.Response == nil {
|
if response.Response == nil {
|
||||||
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
|
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
|
||||||
}
|
}
|
||||||
for k, v := range response.Response.AuditAnnotations {
|
for k, v := range response.Response.AuditAnnotations {
|
||||||
key := h.Name + "/" + k
|
key := h.Name + "/" + k
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,13 @@ var _ admission.ValidationInterface = &Plugin{}
|
||||||
// NewValidatingAdmissionWebhook returns a generic admission webhook plugin.
|
// NewValidatingAdmissionWebhook returns a generic admission webhook plugin.
|
||||||
func NewValidatingAdmissionWebhook(configFile io.Reader) (*Plugin, error) {
|
func NewValidatingAdmissionWebhook(configFile io.Reader) (*Plugin, error) {
|
||||||
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
|
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
|
||||||
webhook, err := generic.NewWebhook(handler, configFile, configuration.NewValidatingWebhookConfigurationManager, newValidatingDispatcher)
|
p := &Plugin{}
|
||||||
|
var err error
|
||||||
|
p.Webhook, err = generic.NewWebhook(handler, configFile, configuration.NewValidatingWebhookConfigurationManager, newValidatingDispatcher(p))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Plugin{webhook}, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate makes an admission decision based on the request attributes.
|
// Validate makes an admission decision based on the request attributes.
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func TestValidate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ns := "webhook-test"
|
ns := "webhook-test"
|
||||||
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh)
|
client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
|
||||||
|
|
||||||
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
|
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
|
||||||
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
|
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
|
||||||
|
|
@ -116,7 +116,7 @@ func TestValidateCachedClient(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
|
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
|
||||||
ns := "webhook-test"
|
ns := "webhook-test"
|
||||||
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh)
|
client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
|
||||||
|
|
||||||
// override the webhook source. The client cache will stay the same.
|
// override the webhook source. The client cache will stay the same.
|
||||||
cacheMisses := new(int32)
|
cacheMisses := new(int32)
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ func (ps *Plugins) NewFromPlugins(pluginNames []string, configProvider ConfigPro
|
||||||
if len(validationPlugins) != 0 {
|
if len(validationPlugins) != 0 {
|
||||||
klog.Infof("Loaded %d validating admission controller(s) successfully in the following order: %s.", len(validationPlugins), strings.Join(validationPlugins, ","))
|
klog.Infof("Loaded %d validating admission controller(s) successfully in the following order: %s.", len(validationPlugins), strings.Join(validationPlugins, ","))
|
||||||
}
|
}
|
||||||
return chainAdmissionHandler(handlers), nil
|
return newReinvocationHandler(chainAdmissionHandler(handlers)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitPlugin creates an instance of the named interface.
|
// InitPlugin creates an instance of the named interface.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 admission
|
||||||
|
|
||||||
|
// newReinvocationHandler creates a handler that wraps the provided admission chain and reinvokes it
|
||||||
|
// if needed according to re-invocation policy of the webhooks.
|
||||||
|
func newReinvocationHandler(admissionChain Interface) Interface {
|
||||||
|
return &reinvoker{admissionChain}
|
||||||
|
}
|
||||||
|
|
||||||
|
type reinvoker struct {
|
||||||
|
admissionChain Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admit performs an admission control check using the wrapped admission chain, reinvoking the
|
||||||
|
// admission chain if needed according to the reinvocation policy. Plugins are expected to check
|
||||||
|
// the admission attributes' reinvocation context against their reinvocation policy to decide if
|
||||||
|
// they should re-run, and to update the reinvocation context if they perform any mutations.
|
||||||
|
func (r *reinvoker) Admit(a Attributes, o ObjectInterfaces) error {
|
||||||
|
if mutator, ok := r.admissionChain.(MutationInterface); ok {
|
||||||
|
err := mutator.Admit(a, o)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s := a.GetReinvocationContext()
|
||||||
|
if s.ShouldReinvoke() {
|
||||||
|
s.SetIsReinvoke()
|
||||||
|
// Calling admit a second time will reinvoke all in-tree plugins
|
||||||
|
// as well as any webhook plugins that need to be reinvoked based on the
|
||||||
|
// reinvocation policy.
|
||||||
|
return mutator.Admit(a, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate performs an admission control check using the wrapped admission chain, and returns immediately on first error.
|
||||||
|
func (r *reinvoker) Validate(a Attributes, o ObjectInterfaces) error {
|
||||||
|
if validator, ok := r.admissionChain.(ValidationInterface); ok {
|
||||||
|
return validator.Validate(a, o)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles will return true if any of the admission chain handlers handle the given operation.
|
||||||
|
func (r *reinvoker) Handles(operation Operation) bool {
|
||||||
|
return r.admissionChain.Handles(operation)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue