object matcher

This commit is contained in:
Chao Xu 2019-05-29 15:56:52 -07:00
parent 4f5c47fb99
commit 6cf499db6c
14 changed files with 397 additions and 42 deletions

View File

@ -40,6 +40,8 @@ type WebhookAccessor interface {
GetMatchPolicy() *v1beta1.MatchPolicyType GetMatchPolicy() *v1beta1.MatchPolicyType
// GetNamespaceSelector gets the webhook NamespaceSelector field. // GetNamespaceSelector gets the webhook NamespaceSelector field.
GetNamespaceSelector() *metav1.LabelSelector GetNamespaceSelector() *metav1.LabelSelector
// GetObjectSelector gets the webhook ObjectSelector field.
GetObjectSelector() *metav1.LabelSelector
// GetSideEffects gets the webhook SideEffects field. // GetSideEffects gets the webhook SideEffects field.
GetSideEffects() *v1beta1.SideEffectClass GetSideEffects() *v1beta1.SideEffectClass
// GetTimeoutSeconds gets the webhook TimeoutSeconds field. // GetTimeoutSeconds gets the webhook TimeoutSeconds field.
@ -84,6 +86,9 @@ func (m mutatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
func (m mutatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector { func (m mutatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
return m.NamespaceSelector return m.NamespaceSelector
} }
func (m mutatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
return m.ObjectSelector
}
func (m mutatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass { func (m mutatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
return m.SideEffects return m.SideEffects
} }
@ -133,6 +138,9 @@ func (v validatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
func (v validatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector { func (v validatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
return v.NamespaceSelector return v.NamespaceSelector
} }
func (v validatingWebhookAccessor) GetObjectSelector() *metav1.LabelSelector {
return v.ObjectSelector
}
func (v validatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass { func (v validatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
return v.SideEffects return v.SideEffects
} }

View File

@ -21,6 +21,7 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/object:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library", "//staging/src/k8s.io/apiserver/pkg/util/webhook:go_default_library",
"//staging/src/k8s.io/client-go/informers:go_default_library", "//staging/src/k8s.io/client-go/informers:go_default_library",
@ -59,6 +60,7 @@ go_test(
"//staging/src/k8s.io/apiserver/pkg/admission:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/object:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library",

View File

@ -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,6 +48,14 @@ 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 {
@ -59,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
} }

View File

@ -30,6 +30,7 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook" "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"
webhookutil "k8s.io/apiserver/pkg/util/webhook" webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
@ -45,6 +46,7 @@ type Webhook struct {
hookSource Source hookSource Source
clientManager *webhookutil.ClientManager clientManager *webhookutil.ClientManager
namespaceMatcher *namespace.Matcher namespaceMatcher *namespace.Matcher
objectMatcher *object.Matcher
dispatcher Dispatcher dispatcher Dispatcher
} }
@ -80,6 +82,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
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
} }
@ -127,9 +130,9 @@ 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 webhook.WebhookAccessor, 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.GetRules() { for _, r := range h.GetRules() {
@ -184,6 +187,11 @@ func (a *Webhook) shouldCallHook(h webhook.WebhookAccessor, attr admission.Attri
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
} }
@ -206,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)
} }

View File

@ -28,10 +28,11 @@ import (
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook" "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
@ -82,6 +83,7 @@ func TestShouldCallHook(t *testing.T) {
name: "invalid kind lookup", name: "invalid kind lookup",
webhook: &v1beta1.ValidatingWebhook{ 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{"*"},
@ -95,6 +97,7 @@ func TestShouldCallHook(t *testing.T) {
name: "wildcard rule, match as requested", name: "wildcard rule, match as requested",
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -109,6 +112,7 @@ func TestShouldCallHook(t *testing.T) {
name: "specific rules, prefer exact match", name: "specific rules, prefer exact match",
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -129,6 +133,7 @@ func TestShouldCallHook(t *testing.T) {
name: "specific rules, match miss", name: "specific rules, match miss",
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -144,6 +149,7 @@ func TestShouldCallHook(t *testing.T) {
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -159,6 +165,7 @@ func TestShouldCallHook(t *testing.T) {
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -177,6 +184,7 @@ func TestShouldCallHook(t *testing.T) {
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -195,6 +203,7 @@ func TestShouldCallHook(t *testing.T) {
name: "specific rules, subresource prefer exact match", name: "specific rules, subresource prefer exact match",
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -215,6 +224,7 @@ func TestShouldCallHook(t *testing.T) {
name: "specific rules, subresource match miss", name: "specific rules, subresource match miss",
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -230,6 +240,7 @@ func TestShouldCallHook(t *testing.T) {
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -245,6 +256,7 @@ func TestShouldCallHook(t *testing.T) {
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -263,6 +275,7 @@ func TestShouldCallHook(t *testing.T) {
webhook: &v1beta1.ValidatingWebhook{ 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},
@ -280,7 +293,7 @@ func TestShouldCallHook(t *testing.T) {
for i, 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(webhook.NewValidatingWebhookAccessor(fmt.Sprintf("webhook-%d", i), 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)

View File

@ -36,6 +36,7 @@ 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"
@ -56,7 +57,7 @@ func newMutatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generi
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() reinvokeCtx := attr.GetReinvocationContext()
var webhookReinvokeCtx *webhookReinvokeContext var webhookReinvokeCtx *webhookReinvokeContext
if v := reinvokeCtx.Value(PluginName); v != nil { if v := reinvokeCtx.Value(PluginName); v != nil {
@ -75,14 +76,31 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject()) webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject())
}() }()
var versionedAttr *generic.VersionedAttributes var versionedAttr *generic.VersionedAttributes
for _, invocation := range relevantHooks { for _, hook := range hooks {
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() hook, ok := invocation.Webhook.GetMutatingWebhook()
if !ok { if !ok {
return fmt.Errorf("mutating webhook dispatch requires v1beta1.MutatingWebhook, but got %T", hook) 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()) { if reinvokeCtx.IsReinvoke() && !webhookReinvokeCtx.ShouldReinvokeWebhook(invocation.Webhook.GetUID()) {
continue continue
} }
if versionedAttr == nil { if versionedAttr == nil {
// First webhook, create versioned attributes // First webhook, create versioned attributes
var err error var err error

View File

@ -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"

View File

@ -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
}

View File

@ -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")
}
})
}
}

View File

@ -245,7 +245,7 @@ func ConvertToMutatingTestCases(tests []ValidatingTest) []MutatingTest {
func ConvertToMutatingWebhooks(webhooks []registrationv1beta1.ValidatingWebhook) []registrationv1beta1.MutatingWebhook { func ConvertToMutatingWebhooks(webhooks []registrationv1beta1.ValidatingWebhook) []registrationv1beta1.MutatingWebhook {
mutating := make([]registrationv1beta1.MutatingWebhook, len(webhooks)) mutating := make([]registrationv1beta1.MutatingWebhook, len(webhooks))
for i, h := range webhooks { for i, h := range webhooks {
mutating[i] = registrationv1beta1.MutatingWebhook{h.Name, h.ClientConfig, h.Rules, h.FailurePolicy, h.MatchPolicy, h.NamespaceSelector, h.SideEffects, h.TimeoutSeconds, h.AdmissionReviewVersions, nil} 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 return mutating
} }
@ -552,6 +552,30 @@ func NewNonMutatingTestCases(url *url.URL) []ValidatingTest {
}}, }},
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.
} }
@ -642,6 +666,36 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
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.
{ {
@ -651,6 +705,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
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"},
ReinvocationPolicy: &reinvokeIfNeeded, ReinvocationPolicy: &reinvokeIfNeeded,
}, { }, {
@ -658,6 +713,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
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"},
ReinvocationPolicy: &reinvokeIfNeeded, ReinvocationPolicy: &reinvokeIfNeeded,
}}, }},
@ -672,6 +728,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
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"},
ReinvocationPolicy: &reinvokeNever, ReinvocationPolicy: &reinvokeNever,
}}, }},
@ -685,6 +742,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
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,
@ -697,6 +755,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
ClientConfig: ccfgSVC("noop"), ClientConfig: ccfgSVC("noop"),
Rules: matchEverythingRules, Rules: matchEverythingRules,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
AdmissionReviewVersions: []string{"v1beta1"}, AdmissionReviewVersions: []string{"v1beta1"},
}}, }},
ExpectAllow: true, ExpectAllow: true,

View File

@ -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{

View File

@ -29,6 +29,7 @@ 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"
@ -39,27 +40,44 @@ import (
type validatingDispatcher struct { type validatingDispatcher struct {
cm *webhookutil.ClientManager cm *webhookutil.ClientManager
plugin *Plugin
} }
func newValidatingDispatcher(cm *webhookutil.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{}

View File

@ -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.

View File

@ -54,6 +54,7 @@ func TestWebhookReinvocationPolicy(t *testing.T) {
type testWebhook struct { type testWebhook struct {
path string path string
policy *registrationv1beta1.ReinvocationPolicyType policy *registrationv1beta1.ReinvocationPolicyType
objectSelector *metav1.LabelSelector
} }
testCases := []struct { testCases := []struct {
@ -110,6 +111,16 @@ func TestWebhookReinvocationPolicy(t *testing.T) {
expectLabels: map[string]string{"x": "true", "fight": "false"}, expectLabels: map[string]string{"x": "true", "fight": "false"},
expectInvocations: map[string]int{"/settrue": 2, "/setfalse": 2}, expectInvocations: map[string]int{"/settrue": 2, "/setfalse": 2},
}, },
{ // in-tree (mutation), webhook A is SKIPPED due to objectSelector not matching, webhook B (mutation), reinvoke in-tree (no-mutation), webhook A is SKIPPED even though the labels match now, because it's not called in the first round. No reinvocation of webhook B required
name: "no reinvocation of webhook B when in-tree or prior webhook mutations",
initialPriorityClass: "low-priority", // trigger initial in-tree mutation
webhooks: []testWebhook{
{path: "/conditionaladdlabel", policy: &reinvokeIfNeeded, objectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "true"}}},
{path: "/addlabel", policy: &reinvokeIfNeeded},
},
expectLabels: map[string]string{"x": "true", "a": "true"},
expectInvocations: map[string]int{"/addlabel": 1, "/conditionaladdlabel": 0},
},
{ {
name: "invalid priority class set by webhook should result in error from in-tree priority plugin", name: "invalid priority class set by webhook should result in error from in-tree priority plugin",
webhooks: []testWebhook{ webhooks: []testWebhook{
@ -193,7 +204,7 @@ func TestWebhookReinvocationPolicy(t *testing.T) {
} }
for i, webhook := range tt.webhooks { for i, webhook := range tt.webhooks {
defer registerWebhook(t, client, fmt.Sprintf("admission.integration.test%d", i), webhookServer.URL+webhook.path, webhook.policy)() defer registerWebhook(t, client, fmt.Sprintf("admission.integration.test%d", i), webhookServer.URL+webhook.path, webhook.policy, webhook.objectSelector)()
} }
pod := &corev1.Pod{ pod := &corev1.Pod{
@ -248,7 +259,7 @@ func TestWebhookReinvocationPolicy(t *testing.T) {
} }
} }
func registerWebhook(t *testing.T, client clientset.Interface, name, endpoint string, reinvocationPolicy *registrationv1beta1.ReinvocationPolicyType) func() { func registerWebhook(t *testing.T, client clientset.Interface, name, endpoint string, reinvocationPolicy *registrationv1beta1.ReinvocationPolicyType, objectSelector *metav1.LabelSelector) func() {
fail := admissionv1beta1.Fail fail := admissionv1beta1.Fail
hook, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{ hook, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: name}, ObjectMeta: metav1.ObjectMeta{Name: name},
@ -262,6 +273,7 @@ func registerWebhook(t *testing.T, client clientset.Interface, name, endpoint st
Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll}, Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
Rule: admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}}, Rule: admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
}}, }},
ObjectSelector: objectSelector,
FailurePolicy: &fail, FailurePolicy: &fail,
ReinvocationPolicy: reinvocationPolicy, ReinvocationPolicy: reinvocationPolicy,
AdmissionReviewVersions: []string{"v1beta1"}, AdmissionReviewVersions: []string{"v1beta1"},