mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 12:15:52 +00:00
object matcher
This commit is contained in:
parent
4f5c47fb99
commit
6cf499db6c
@ -40,6 +40,8 @@ type WebhookAccessor interface {
|
||||
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.
|
||||
@ -84,6 +86,9 @@ func (m mutatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
|
||||
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
|
||||
}
|
||||
@ -133,6 +138,9 @@ func (v validatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
|
||||
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
|
||||
}
|
||||
|
@ -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/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/object: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/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/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/object: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/example2/v1:go_default_library",
|
||||
|
@ -35,7 +35,7 @@ type Source interface {
|
||||
// variants of the object and old object.
|
||||
type VersionedAttributes struct {
|
||||
// Attributes holds the original admission attributes
|
||||
Attributes admission.Attributes
|
||||
admission.Attributes
|
||||
// VersionedOldObject holds Attributes.OldObject (if non-nil), converted to VersionedKind.
|
||||
// It must never be mutated.
|
||||
VersionedOldObject runtime.Object
|
||||
@ -48,6 +48,14 @@ type VersionedAttributes struct {
|
||||
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,
|
||||
// and the kind that should be sent to the webhook.
|
||||
type WebhookInvocation struct {
|
||||
@ -59,6 +67,9 @@ type WebhookInvocation struct {
|
||||
|
||||
// Dispatcher dispatches webhook call to a list of webhooks with admission attributes as argument.
|
||||
type Dispatcher interface {
|
||||
// Dispatch a request to the webhooks using the given webhooks. A non-nil error means the request is rejected.
|
||||
Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []*WebhookInvocation) error
|
||||
// Dispatch a request to the webhooks. Dispatcher may choose not to
|
||||
// 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
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/object"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/rules"
|
||||
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/informers"
|
||||
@ -45,6 +46,7 @@ type Webhook struct {
|
||||
hookSource Source
|
||||
clientManager *webhookutil.ClientManager
|
||||
namespaceMatcher *namespace.Matcher
|
||||
objectMatcher *object.Matcher
|
||||
dispatcher Dispatcher
|
||||
}
|
||||
|
||||
@ -80,6 +82,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
|
||||
sourceFactory: sourceFactory,
|
||||
clientManager: &cm,
|
||||
namespaceMatcher: &namespace.Matcher{},
|
||||
objectMatcher: &object.Matcher{},
|
||||
dispatcher: dispatcherFactory(&cm),
|
||||
}, nil
|
||||
}
|
||||
@ -127,9 +130,9 @@ func (a *Webhook) ValidateInitialization() error {
|
||||
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.
|
||||
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 invocation *WebhookInvocation
|
||||
for _, r := range h.GetRules() {
|
||||
@ -184,6 +187,11 @@ func (a *Webhook) shouldCallHook(h webhook.WebhookAccessor, attr admission.Attri
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matches, err = a.objectMatcher.MatchObjectSelector(h, attr)
|
||||
if !matches || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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.
|
||||
ctx := context.TODO()
|
||||
|
||||
var relevantHooks []*WebhookInvocation
|
||||
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)
|
||||
return a.dispatcher.Dispatch(ctx, attr, o, hooks)
|
||||
}
|
||||
|
@ -28,10 +28,11 @@ import (
|
||||
"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/object"
|
||||
)
|
||||
|
||||
func TestShouldCallHook(t *testing.T) {
|
||||
a := &Webhook{namespaceMatcher: &namespace.Matcher{}}
|
||||
a := &Webhook{namespaceMatcher: &namespace.Matcher{}, objectMatcher: &object.Matcher{}}
|
||||
|
||||
allScopes := v1beta1.AllScopes
|
||||
exactMatch := v1beta1.Exact
|
||||
@ -82,6 +83,7 @@ func TestShouldCallHook(t *testing.T) {
|
||||
name: "invalid kind lookup",
|
||||
webhook: &v1beta1.ValidatingWebhook{
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
MatchPolicy: &equivalentMatch,
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
@ -95,6 +97,7 @@ func TestShouldCallHook(t *testing.T) {
|
||||
name: "wildcard rule, match as requested",
|
||||
webhook: &v1beta1.ValidatingWebhook{
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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",
|
||||
webhook: &v1beta1.ValidatingWebhook{
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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",
|
||||
webhook: &v1beta1.ValidatingWebhook{
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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{
|
||||
MatchPolicy: &exactMatch,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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{
|
||||
MatchPolicy: &equivalentMatch,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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{
|
||||
MatchPolicy: &equivalentMatch,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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",
|
||||
webhook: &v1beta1.ValidatingWebhook{
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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",
|
||||
webhook: &v1beta1.ValidatingWebhook{
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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{
|
||||
MatchPolicy: &exactMatch,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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{
|
||||
MatchPolicy: &equivalentMatch,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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{
|
||||
MatchPolicy: &equivalentMatch,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
Rules: []v1beta1.RuleWithOperations{{
|
||||
Operations: []v1beta1.OperationType{"*"},
|
||||
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 {
|
||||
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 len(testcase.expectErr) == 0 {
|
||||
t.Fatal(err)
|
||||
|
@ -36,6 +36,7 @@ import (
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"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{}
|
||||
|
||||
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 {
|
||||
@ -75,14 +76,31 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
|
||||
webhookReinvokeCtx.SetLastWebhookInvocationOutput(attr.GetObject())
|
||||
}()
|
||||
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()
|
||||
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 {
|
||||
// First webhook, create versioned attributes
|
||||
var err error
|
||||
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -245,7 +245,7 @@ func ConvertToMutatingTestCases(tests []ValidatingTest) []MutatingTest {
|
||||
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.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
|
||||
}
|
||||
@ -552,6 +552,30 @@ func NewNonMutatingTestCases(url *url.URL) []ValidatingTest {
|
||||
}},
|
||||
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
|
||||
// connection is different.
|
||||
}
|
||||
@ -642,6 +666,36 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
|
||||
ExpectStatusCode: http.StatusBadRequest,
|
||||
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
|
||||
// connection is different.
|
||||
{
|
||||
@ -651,6 +705,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
|
||||
ClientConfig: ccfgSVC("addLabel"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
ReinvocationPolicy: &reinvokeIfNeeded,
|
||||
}, {
|
||||
@ -658,6 +713,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
|
||||
ClientConfig: ccfgSVC("removeLabel"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
ReinvocationPolicy: &reinvokeIfNeeded,
|
||||
}},
|
||||
@ -672,6 +728,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
|
||||
ClientConfig: ccfgSVC("addLabel"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
ReinvocationPolicy: &reinvokeNever,
|
||||
}},
|
||||
@ -685,6 +742,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
|
||||
ClientConfig: ccfgSVC("addLabel"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
@ -697,6 +755,7 @@ func NewMutatingTestCases(url *url.URL) []MutatingTest {
|
||||
ClientConfig: ccfgSVC("noop"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
|
@ -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":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
|
||||
|
@ -29,6 +29,7 @@ import (
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
|
||||
@ -38,28 +39,45 @@ import (
|
||||
)
|
||||
|
||||
type validatingDispatcher struct {
|
||||
cm *webhookutil.ClientManager
|
||||
cm *webhookutil.ClientManager
|
||||
plugin *Plugin
|
||||
}
|
||||
|
||||
func newValidatingDispatcher(cm *webhookutil.ClientManager) generic.Dispatcher {
|
||||
return &validatingDispatcher{cm}
|
||||
func newValidatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher {
|
||||
return func(cm *webhookutil.ClientManager) generic.Dispatcher {
|
||||
return &validatingDispatcher{cm, p}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
versionedAttrs := map[schema.GroupVersionKind]*generic.VersionedAttributes{}
|
||||
for _, call := range relevantHooks {
|
||||
// If we already have this version, continue
|
||||
if _, ok := versionedAttrs[call.Kind]; ok {
|
||||
for _, hook := range hooks {
|
||||
invocation, statusError := d.plugin.ShouldCallHook(hook, attr, o)
|
||||
if statusError != nil {
|
||||
return statusError
|
||||
}
|
||||
if invocation == nil {
|
||||
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 {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
versionedAttrs[call.Kind] = versionedAttr
|
||||
versionedAttrs[invocation.Kind] = versionedAttr
|
||||
}
|
||||
|
||||
if len(relevantHooks) == 0 {
|
||||
// no matching hooks
|
||||
return nil
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
@ -51,11 +51,13 @@ var _ admission.ValidationInterface = &Plugin{}
|
||||
// NewValidatingAdmissionWebhook returns a generic admission webhook plugin.
|
||||
func NewValidatingAdmissionWebhook(configFile io.Reader) (*Plugin, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return &Plugin{webhook}, nil
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Validate makes an admission decision based on the request attributes.
|
||||
|
@ -52,8 +52,9 @@ func TestWebhookReinvocationPolicy(t *testing.T) {
|
||||
reinvokeIfNeeded := registrationv1beta1.IfNeededReinvocationPolicy
|
||||
|
||||
type testWebhook struct {
|
||||
path string
|
||||
policy *registrationv1beta1.ReinvocationPolicyType
|
||||
path string
|
||||
policy *registrationv1beta1.ReinvocationPolicyType
|
||||
objectSelector *metav1.LabelSelector
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
@ -110,6 +111,16 @@ func TestWebhookReinvocationPolicy(t *testing.T) {
|
||||
expectLabels: map[string]string{"x": "true", "fight": "false"},
|
||||
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",
|
||||
webhooks: []testWebhook{
|
||||
@ -193,7 +204,7 @@ func TestWebhookReinvocationPolicy(t *testing.T) {
|
||||
}
|
||||
|
||||
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{
|
||||
@ -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
|
||||
hook, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&admissionv1beta1.MutatingWebhookConfiguration{
|
||||
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},
|
||||
Rule: admissionv1beta1.Rule{APIGroups: []string{"*"}, APIVersions: []string{"*"}, Resources: []string{"*/*"}},
|
||||
}},
|
||||
ObjectSelector: objectSelector,
|
||||
FailurePolicy: &fail,
|
||||
ReinvocationPolicy: reinvocationPolicy,
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
|
Loading…
Reference in New Issue
Block a user