mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 18:00:08 +00:00
object matcher
This commit is contained in:
parent
4f5c47fb99
commit
6cf499db6c
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
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,
|
||||||
|
@ -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{
|
||||||
|
@ -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{}
|
||||||
|
@ -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.
|
||||||
|
@ -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"},
|
||||||
|
Loading…
Reference in New Issue
Block a user