split admissionregistration.v1beta1/Webhook into MutatingWebhook and ValidatingWebhook

Kubernetes-commit: 55ecc45455f191c404e355097bf1beae9c42f895
This commit is contained in:
Joe Betz 2019-05-29 21:30:45 -07:00 committed by Kubernetes Publisher
parent da0bf83195
commit b2b1ef14ec
16 changed files with 382 additions and 169 deletions

View File

@ -24,6 +24,7 @@ import (
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
@ -48,7 +49,7 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g
}
// Start with an empty list
manager.configuration.Store(&v1beta1.MutatingWebhookConfiguration{})
manager.configuration.Store([]webhook.WebhookAccessor{})
// On any change, rebuild the config
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
@ -61,8 +62,8 @@ func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) g
}
// Webhooks returns the merged MutatingWebhookConfiguration.
func (m *mutatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook {
return m.configuration.Load().(*v1beta1.MutatingWebhookConfiguration).Webhooks
func (m *mutatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor {
return m.configuration.Load().([]webhook.WebhookAccessor)
}
func (m *mutatingWebhookConfigurationManager) HasSynced() bool {
@ -78,16 +79,18 @@ func (m *mutatingWebhookConfigurationManager) updateConfiguration() {
m.configuration.Store(mergeMutatingWebhookConfigurations(configurations))
}
func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhookConfiguration) *v1beta1.MutatingWebhookConfiguration {
var ret v1beta1.MutatingWebhookConfiguration
func mergeMutatingWebhookConfigurations(configurations []*v1beta1.MutatingWebhookConfiguration) []webhook.WebhookAccessor {
// The internal order of webhooks for each configuration is provided by the user
// but configurations themselves can be in any order. As we are going to run these
// webhooks in serial, they are sorted here to have a deterministic order.
sort.SliceStable(configurations, MutatingWebhookConfigurationSorter(configurations).ByName)
accessors := []webhook.WebhookAccessor{}
for _, c := range configurations {
ret.Webhooks = append(ret.Webhooks, c.Webhooks...)
for i := range c.Webhooks {
accessors = append(accessors, webhook.NewMutatingWebhookAccessor(&c.Webhooks[i]))
}
}
return &ret
return accessors
}
type MutatingWebhookConfigurationSorter []*v1beta1.MutatingWebhookConfiguration

View File

@ -45,7 +45,7 @@ func TestGetMutatingWebhookConfig(t *testing.T) {
webhookConfiguration := &v1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}},
Webhooks: []v1beta1.MutatingWebhook{{Name: "webhook1.1"}},
}
mutatingInformer := informerFactory.Admissionregistration().V1beta1().MutatingWebhookConfigurations()
@ -57,7 +57,14 @@ func TestGetMutatingWebhookConfig(t *testing.T) {
if len(configurations) == 0 {
t.Errorf("expected non empty webhooks")
}
if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) {
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations)
for i := range configurations {
h, ok := configurations[i].GetMutatingWebhook()
if !ok {
t.Errorf("Expected mutating webhook")
continue
}
if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) {
t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h)
}
}
}

View File

@ -24,6 +24,7 @@ import (
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
@ -48,7 +49,7 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory)
}
// Start with an empty list
manager.configuration.Store(&v1beta1.ValidatingWebhookConfiguration{})
manager.configuration.Store([]webhook.WebhookAccessor{})
// On any change, rebuild the config
informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
@ -61,8 +62,8 @@ func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory)
}
// Webhooks returns the merged ValidatingWebhookConfiguration.
func (v *validatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook {
return v.configuration.Load().(*v1beta1.ValidatingWebhookConfiguration).Webhooks
func (v *validatingWebhookConfigurationManager) Webhooks() []webhook.WebhookAccessor {
return v.configuration.Load().([]webhook.WebhookAccessor)
}
// HasSynced returns true if the shared informers have synced.
@ -79,15 +80,15 @@ func (v *validatingWebhookConfigurationManager) updateConfiguration() {
v.configuration.Store(mergeValidatingWebhookConfigurations(configurations))
}
func mergeValidatingWebhookConfigurations(
configurations []*v1beta1.ValidatingWebhookConfiguration,
) *v1beta1.ValidatingWebhookConfiguration {
func mergeValidatingWebhookConfigurations(configurations []*v1beta1.ValidatingWebhookConfiguration) []webhook.WebhookAccessor {
sort.SliceStable(configurations, ValidatingWebhookConfigurationSorter(configurations).ByName)
var ret v1beta1.ValidatingWebhookConfiguration
accessors := []webhook.WebhookAccessor{}
for _, c := range configurations {
ret.Webhooks = append(ret.Webhooks, c.Webhooks...)
for i := range c.Webhooks {
accessors = append(accessors, webhook.NewValidatingWebhookAccessor(&c.Webhooks[i]))
}
}
return &ret
return accessors
}
type ValidatingWebhookConfigurationSorter []*v1beta1.ValidatingWebhookConfiguration

View File

@ -46,7 +46,7 @@ func TestGetValidatingWebhookConfig(t *testing.T) {
webhookConfiguration := &v1beta1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}},
Webhooks: []v1beta1.ValidatingWebhook{{Name: "webhook1.1"}},
}
validatingInformer := informerFactory.Admissionregistration().V1beta1().ValidatingWebhookConfigurations()
@ -59,7 +59,14 @@ func TestGetValidatingWebhookConfig(t *testing.T) {
if len(configurations) == 0 {
t.Errorf("expected non empty webhooks")
}
if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) {
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations)
for i := range configurations {
h, ok := configurations[i].GetValidatingWebhook()
if !ok {
t.Errorf("Expected validating webhook")
continue
}
if !reflect.DeepEqual(h, &webhookConfiguration.Webhooks[i]) {
t.Errorf("Expected\n%#v\ngot\n%#v", &webhookConfiguration.Webhooks[i], h)
}
}
}

View File

@ -0,0 +1,139 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webhook
import (
"k8s.io/api/admissionregistration/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// WebhookAccessor provides a common interface to both mutating and validating webhook types.
type WebhookAccessor interface {
// GetName gets the webhook Name field.
GetName() string
// GetClientConfig gets the webhook ClientConfig field.
GetClientConfig() v1beta1.WebhookClientConfig
// GetRules gets the webhook Rules field.
GetRules() []v1beta1.RuleWithOperations
// GetFailurePolicy gets the webhook FailurePolicy field.
GetFailurePolicy() *v1beta1.FailurePolicyType
// GetMatchPolicy gets the webhook MatchPolicy field.
GetMatchPolicy() *v1beta1.MatchPolicyType
// GetNamespaceSelector gets the webhook NamespaceSelector field.
GetNamespaceSelector() *metav1.LabelSelector
// GetSideEffects gets the webhook SideEffects field.
GetSideEffects() *v1beta1.SideEffectClass
// GetTimeoutSeconds gets the webhook TimeoutSeconds field.
GetTimeoutSeconds() *int32
// GetAdmissionReviewVersions gets the webhook AdmissionReviewVersions field.
GetAdmissionReviewVersions() []string
// GetMutatingWebhook if the accessor contains a MutatingWebhook, returns it and true, else returns false.
GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool)
// GetValidatingWebhook if the accessor contains a ValidatingWebhook, returns it and true, else returns false.
GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool)
}
// NewMutatingWebhookAccessor creates an accessor for a MutatingWebhook.
func NewMutatingWebhookAccessor(h *v1beta1.MutatingWebhook) WebhookAccessor {
return mutatingWebhookAccessor{h}
}
type mutatingWebhookAccessor struct {
*v1beta1.MutatingWebhook
}
func (m mutatingWebhookAccessor) GetName() string {
return m.Name
}
func (m mutatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig {
return m.ClientConfig
}
func (m mutatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations {
return m.Rules
}
func (m mutatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType {
return m.FailurePolicy
}
func (m mutatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
return m.MatchPolicy
}
func (m mutatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
return m.NamespaceSelector
}
func (m mutatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
return m.SideEffects
}
func (m mutatingWebhookAccessor) GetTimeoutSeconds() *int32 {
return m.TimeoutSeconds
}
func (m mutatingWebhookAccessor) GetAdmissionReviewVersions() []string {
return m.AdmissionReviewVersions
}
func (m mutatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) {
return m.MutatingWebhook, true
}
func (m mutatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) {
return nil, false
}
// NewValidatingWebhookAccessor creates an accessor for a ValidatingWebhook.
func NewValidatingWebhookAccessor(h *v1beta1.ValidatingWebhook) WebhookAccessor {
return validatingWebhookAccessor{h}
}
type validatingWebhookAccessor struct {
*v1beta1.ValidatingWebhook
}
func (v validatingWebhookAccessor) GetName() string {
return v.Name
}
func (v validatingWebhookAccessor) GetClientConfig() v1beta1.WebhookClientConfig {
return v.ClientConfig
}
func (v validatingWebhookAccessor) GetRules() []v1beta1.RuleWithOperations {
return v.Rules
}
func (v validatingWebhookAccessor) GetFailurePolicy() *v1beta1.FailurePolicyType {
return v.FailurePolicy
}
func (v validatingWebhookAccessor) GetMatchPolicy() *v1beta1.MatchPolicyType {
return v.MatchPolicy
}
func (v validatingWebhookAccessor) GetNamespaceSelector() *metav1.LabelSelector {
return v.NamespaceSelector
}
func (v validatingWebhookAccessor) GetSideEffects() *v1beta1.SideEffectClass {
return v.SideEffects
}
func (v validatingWebhookAccessor) GetTimeoutSeconds() *int32 {
return v.TimeoutSeconds
}
func (v validatingWebhookAccessor) GetAdmissionReviewVersions() []string {
return v.AdmissionReviewVersions
}
func (v validatingWebhookAccessor) GetMutatingWebhook() (*v1beta1.MutatingWebhook, bool) {
return nil, false
}
func (v validatingWebhookAccessor) GetValidatingWebhook() (*v1beta1.ValidatingWebhook, bool) {
return v.ValidatingWebhook, true
}

View File

@ -19,15 +19,15 @@ package generic
import (
"context"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
)
// Source can list dynamic webhook plugins.
type Source interface {
Webhooks() []v1beta1.Webhook
Webhooks() []webhook.WebhookAccessor
HasSynced() bool
}
@ -51,8 +51,7 @@ type VersionedAttributes struct {
// 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 {
Webhook *v1beta1.Webhook
Webhook webhook.WebhookAccessor
Resource schema.GroupVersionResource
Subresource string
Kind schema.GroupVersionKind

View File

@ -27,10 +27,11 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/rules"
"k8s.io/apiserver/pkg/util/webhook"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
)
@ -42,7 +43,7 @@ type Webhook struct {
sourceFactory sourceFactory
hookSource Source
clientManager *webhook.ClientManager
clientManager *webhookutil.ClientManager
namespaceMatcher *namespace.Matcher
dispatcher Dispatcher
}
@ -53,7 +54,7 @@ var (
)
type sourceFactory func(f informers.SharedInformerFactory) Source
type dispatcherFactory func(cm *webhook.ClientManager) Dispatcher
type dispatcherFactory func(cm *webhookutil.ClientManager) Dispatcher
// NewWebhook creates a new generic admission webhook.
func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) {
@ -62,17 +63,17 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
return nil, err
}
cm, err := webhook.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme)
cm, err := webhookutil.NewClientManager(admissionv1beta1.SchemeGroupVersion, admissionv1beta1.AddToScheme)
if err != nil {
return nil, err
}
authInfoResolver, err := webhook.NewDefaultAuthenticationInfoResolver(kubeconfigFile)
authInfoResolver, err := webhookutil.NewDefaultAuthenticationInfoResolver(kubeconfigFile)
if err != nil {
return nil, err
}
// Set defaults which may be overridden later.
cm.SetAuthenticationInfoResolver(authInfoResolver)
cm.SetServiceResolver(webhook.NewDefaultServiceResolver())
cm.SetServiceResolver(webhookutil.NewDefaultServiceResolver())
return &Webhook{
Handler: handler,
@ -86,13 +87,13 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
// SetAuthenticationInfoResolverWrapper sets the
// AuthenticationInfoResolverWrapper.
// TODO find a better way wire this, but keep this pull small for now.
func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhook.AuthenticationInfoResolverWrapper) {
func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper webhookutil.AuthenticationInfoResolverWrapper) {
a.clientManager.SetAuthenticationInfoResolverWrapper(wrapper)
}
// SetServiceResolver sets a service resolver for the webhook admission plugin.
// Passing a nil resolver does not have an effect, instead a default one will be used.
func (a *Webhook) SetServiceResolver(sr webhook.ServiceResolver) {
func (a *Webhook) SetServiceResolver(sr webhookutil.ServiceResolver) {
a.clientManager.SetServiceResolver(sr)
}
@ -128,10 +129,10 @@ func (a *Webhook) ValidateInitialization() error {
// 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 *v1beta1.Webhook, attr admission.Attributes, o admission.ObjectInterfaces) (*WebhookInvocation, *apierrors.StatusError) {
func (a *Webhook) shouldCallHook(h webhook.WebhookAccessor, attr admission.Attributes, o admission.ObjectInterfaces) (*WebhookInvocation, *apierrors.StatusError) {
var err *apierrors.StatusError
var invocation *WebhookInvocation
for _, r := range h.Rules {
for _, r := range h.GetRules() {
m := rules.Matcher{Rule: r, Attr: attr}
if m.Matches() {
invocation = &WebhookInvocation{
@ -143,12 +144,12 @@ func (a *Webhook) shouldCallHook(h *v1beta1.Webhook, attr admission.Attributes,
break
}
}
if invocation == nil && h.MatchPolicy != nil && *h.MatchPolicy == v1beta1.Equivalent {
if invocation == nil && h.GetMatchPolicy() != nil && *h.GetMatchPolicy() == v1beta1.Equivalent {
attrWithOverride := &attrWithResourceOverride{Attributes: attr}
equivalents := o.GetEquivalentResourceMapper().EquivalentResourcesFor(attr.GetResource(), attr.GetSubresource())
// honor earlier rules first
OuterLoop:
for _, r := range h.Rules {
for _, r := range h.GetRules() {
// see if the rule matches any of the equivalent resources
for _, equivalent := range equivalents {
if equivalent == attr.GetResource() {
@ -207,7 +208,7 @@ func (a *Webhook) Dispatch(attr admission.Attributes, o admission.ObjectInterfac
var relevantHooks []*WebhookInvocation
for i := range hooks {
invocation, err := a.shouldCallHook(&hooks[i], attr, o)
invocation, err := a.shouldCallHook(hooks[i], attr, o)
if err != nil {
return err
}

View File

@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook/namespace"
)
@ -61,7 +62,7 @@ func TestShouldCallHook(t *testing.T) {
testcases := []struct {
name string
webhook *v1beta1.Webhook
webhook *v1beta1.ValidatingWebhook
attrs admission.Attributes
expectCall bool
@ -72,13 +73,13 @@ func TestShouldCallHook(t *testing.T) {
}{
{
name: "no rules (just write)",
webhook: &v1beta1.Webhook{Rules: []v1beta1.RuleWithOperations{}},
webhook: &v1beta1.ValidatingWebhook{Rules: []v1beta1.RuleWithOperations{}},
attrs: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{"apps", "v1", "Deployment"}, "ns", "name", schema.GroupVersionResource{"apps", "v1", "deployments"}, "", admission.Create, &metav1.CreateOptions{}, false, nil),
expectCall: false,
},
{
name: "invalid kind lookup",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{},
MatchPolicy: &equivalentMatch,
Rules: []v1beta1.RuleWithOperations{{
@ -91,7 +92,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "wildcard rule, match as requested",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"},
@ -105,7 +106,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "specific rules, prefer exact match",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"},
@ -125,7 +126,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "specific rules, match miss",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"},
@ -139,7 +140,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "specific rules, exact match miss",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &exactMatch,
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
@ -154,7 +155,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "specific rules, equivalent match, prefer extensions",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
@ -172,7 +173,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "specific rules, equivalent match, prefer apps",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
@ -191,7 +192,7 @@ func TestShouldCallHook(t *testing.T) {
{
name: "specific rules, subresource prefer exact match",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"},
@ -211,7 +212,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "specific rules, subresource match miss",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
Operations: []v1beta1.OperationType{"*"},
@ -225,7 +226,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "specific rules, subresource exact match miss",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &exactMatch,
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
@ -240,7 +241,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "specific rules, subresource equivalent match, prefer extensions",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
@ -258,7 +259,7 @@ func TestShouldCallHook(t *testing.T) {
},
{
name: "specific rules, subresource equivalent match, prefer apps",
webhook: &v1beta1.Webhook{
webhook: &v1beta1.ValidatingWebhook{
MatchPolicy: &equivalentMatch,
NamespaceSelector: &metav1.LabelSelector{},
Rules: []v1beta1.RuleWithOperations{{
@ -278,7 +279,7 @@ func TestShouldCallHook(t *testing.T) {
for _, testcase := range testcases {
t.Run(testcase.name, func(t *testing.T) {
invocation, err := a.shouldCallHook(testcase.webhook, testcase.attrs, interfaces)
invocation, err := a.shouldCallHook(webhook.NewValidatingWebhookAccessor(testcase.webhook), testcase.attrs, interfaces)
if err != nil {
if len(testcase.expectErr) == 0 {
t.Fatal(err)

View File

@ -39,16 +39,16 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
"k8s.io/apiserver/pkg/admission/plugin/webhook/util"
"k8s.io/apiserver/pkg/util/webhook"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
)
type mutatingDispatcher struct {
cm *webhook.ClientManager
cm *webhookutil.ClientManager
plugin *Plugin
}
func newMutatingDispatcher(p *Plugin) func(cm *webhook.ClientManager) generic.Dispatcher {
return func(cm *webhook.ClientManager) generic.Dispatcher {
func newMutatingDispatcher(p *Plugin) func(cm *webhookutil.ClientManager) generic.Dispatcher {
return func(cm *webhookutil.ClientManager) generic.Dispatcher {
return &mutatingDispatcher{cm, p}
}
}
@ -58,7 +58,10 @@ var _ generic.Dispatcher = &mutatingDispatcher{}
func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, relevantHooks []*generic.WebhookInvocation) error {
var versionedAttr *generic.VersionedAttributes
for _, invocation := range relevantHooks {
hook := invocation.Webhook
hook, ok := invocation.Webhook.GetMutatingWebhook()
if !ok {
return fmt.Errorf("mutating webhook dispatch requires v1beta1.MutatingWebhook, but got %T", hook)
}
if versionedAttr == nil {
// First webhook, create versioned attributes
var err error
@ -73,14 +76,14 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
}
t := time.Now()
err := a.callAttrMutatingHook(ctx, invocation, versionedAttr, o)
err := a.callAttrMutatingHook(ctx, hook, invocation, versionedAttr, o)
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "admit", hook.Name)
if err == nil {
continue
}
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
if callErr, ok := err.(*webhook.ErrCallingWebhook); ok {
if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
if ignoreClientCallFailures {
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr)
@ -100,11 +103,11 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
}
// note that callAttrMutatingHook updates attr
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) error {
h := invocation.Webhook
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta1.MutatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes, o admission.ObjectInterfaces) error {
if attr.Attributes.IsDryRun() {
if h.SideEffects == nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
}
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
return webhookerrors.NewDryRunUnsupportedErr(h.Name)
@ -113,15 +116,15 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocatio
// Currently dispatcher only supports `v1beta1` AdmissionReview
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, h) {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReview")}
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReview")}
}
// Make the webhook request
request := request.CreateAdmissionReview(attr, invocation)
client, err := a.cm.HookClient(util.HookClientConfigForWebhook(h))
client, err := a.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook))
if err != nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
response := &admissionv1beta1.AdmissionReview{}
r := client.Post().Context(ctx).Body(&request)
@ -129,11 +132,11 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, invocatio
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
}
if err := r.Do().Into(response); err != nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
if response.Response == nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
}
for k, v := range response.Response.AuditAnnotations {

View File

@ -46,7 +46,7 @@ func TestAdmit(t *testing.T) {
defer close(stopCh)
testCases := append(webhooktesting.NewMutatingTestCases(serverURL),
webhooktesting.NewNonMutatingTestCases(serverURL)...)
webhooktesting.ConvertToMutatingTestCases(webhooktesting.NewNonMutatingTestCases(serverURL))...)
for _, tt := range testCases {
wh, err := NewMutatingWebhook(nil)
@ -56,7 +56,7 @@ func TestAdmit(t *testing.T) {
}
ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh)
client, informer := webhooktesting.NewFakeMutatingDataSource(ns, tt.Webhooks, stopCh)
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
@ -136,7 +136,7 @@ func TestAdmitCachedClient(t *testing.T) {
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh)
client, informer := webhooktesting.NewFakeMutatingDataSource(ns, webhooktesting.ConvertToMutatingWebhooks(tt.Webhooks), stopCh)
// override the webhook source. The client cache will stay the same.
cacheMisses := new(int32)

View File

@ -19,13 +19,13 @@ package namespace
import (
"fmt"
"k8s.io/api/admissionregistration/v1beta1"
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"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
clientset "k8s.io/client-go/kubernetes"
corelisters "k8s.io/client-go/listers/core/v1"
)
@ -86,7 +86,7 @@ func (m *Matcher) GetNamespaceLabels(attr admission.Attributes) (map[string]stri
// MatchNamespaceSelector decideds whether the request matches the
// namespaceSelctor of the webhook. Only when they match, the webhook is called.
func (m *Matcher) MatchNamespaceSelector(h *v1beta1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
func (m *Matcher) MatchNamespaceSelector(h webhook.WebhookAccessor, attr admission.Attributes) (bool, *apierrors.StatusError) {
namespaceName := attr.GetNamespace()
if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" {
// If the request is about a cluster scoped resource, and it is not a
@ -96,7 +96,7 @@ func (m *Matcher) MatchNamespaceSelector(h *v1beta1.Webhook, attr admission.Attr
return true, nil
}
// TODO: adding an LRU cache to cache the translation
selector, err := metav1.LabelSelectorAsSelector(h.NamespaceSelector)
selector, err := metav1.LabelSelectorAsSelector(h.GetNamespaceSelector())
if err != nil {
return false, apierrors.NewInternalError(err)
}

View File

@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
)
type fakeNamespaceLister struct {
@ -114,12 +115,12 @@ func TestGetNamespaceLabels(t *testing.T) {
}
func TestNotExemptClusterScopedResource(t *testing.T) {
hook := &registrationv1beta1.Webhook{
hook := &registrationv1beta1.ValidatingWebhook{
NamespaceSelector: &metav1.LabelSelector{},
}
attr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "mock-name", schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "", admission.Create, &metav1.CreateOptions{}, false, nil)
matcher := Matcher{}
matches, err := matcher.MatchNamespaceSelector(hook, attr)
matches, err := matcher.MatchNamespaceSelector(webhook.NewValidatingWebhookAccessor(hook), attr)
if err != nil {
t.Fatal(err)
}

View File

@ -49,8 +49,8 @@ var sideEffectsNone = registrationv1beta1.SideEffectClassNone
var sideEffectsSome = registrationv1beta1.SideEffectClassSome
var sideEffectsNoneOnDryRun = registrationv1beta1.SideEffectClassNoneOnDryRun
// NewFakeDataSource returns a mock client and informer returning the given webhooks.
func NewFakeDataSource(name string, webhooks []registrationv1beta1.Webhook, mutating bool, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
// NewFakeValidatingDataSource returns a mock client and informer returning the given webhooks.
func NewFakeValidatingDataSource(name string, webhooks []registrationv1beta1.ValidatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
var objs = []runtime.Object{
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
@ -61,21 +61,37 @@ func NewFakeDataSource(name string, webhooks []registrationv1beta1.Webhook, muta
},
},
}
if mutating {
objs = append(objs, &registrationv1beta1.MutatingWebhookConfiguration{
objs = append(objs, &registrationv1beta1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "test-webhooks",
},
Webhooks: webhooks,
})
client := fakeclientset.NewSimpleClientset(objs...)
informerFactory := informers.NewSharedInformerFactory(client, 0)
return client, informerFactory
}
// NewFakeMutatingDataSource returns a mock client and informer returning the given webhooks.
func NewFakeMutatingDataSource(name string, webhooks []registrationv1beta1.MutatingWebhook, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
var objs = []runtime.Object{
&corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-webhooks",
Name: name,
Labels: map[string]string{
"runlevel": "0",
},
},
Webhooks: webhooks,
})
} else {
objs = append(objs, &registrationv1beta1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "test-webhooks",
},
Webhooks: webhooks,
})
},
}
objs = append(objs, &registrationv1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: "test-webhooks",
},
Webhooks: webhooks,
})
client := fakeclientset.NewSimpleClientset(objs...)
informerFactory := informers.NewSharedInformerFactory(client, 0)
@ -181,10 +197,10 @@ func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1beta1.WebhookC
}
}
// Test is a webhook test case.
type Test struct {
// ValidatingTest is a validating webhook test case.
type ValidatingTest struct {
Name string
Webhooks []registrationv1beta1.Webhook
Webhooks []registrationv1beta1.ValidatingWebhook
Path string
IsCRD bool
IsDryRun bool
@ -196,19 +212,52 @@ type Test struct {
ExpectStatusCode int32
}
// MutatingTest is a mutating webhook test case.
type MutatingTest struct {
Name string
Webhooks []registrationv1beta1.MutatingWebhook
Path string
IsCRD bool
IsDryRun bool
AdditionalLabels map[string]string
ExpectLabels map[string]string
ExpectAllow bool
ErrorContains string
ExpectAnnotations map[string]string
ExpectStatusCode int32
}
// ConvertToMutatingTestCases converts a validating test case to a mutating one for test purposes.
func ConvertToMutatingTestCases(tests []ValidatingTest) []MutatingTest {
r := make([]MutatingTest, len(tests))
for i, t := range tests {
r[i] = MutatingTest{t.Name, ConvertToMutatingWebhooks(t.Webhooks), t.Path, t.IsCRD, t.IsDryRun, t.AdditionalLabels, t.ExpectLabels, t.ExpectAllow, t.ErrorContains, t.ExpectAnnotations, t.ExpectStatusCode}
}
return r
}
// ConvertToMutatingWebhooks converts a validating webhook to a mutating one for test purposes.
func ConvertToMutatingWebhooks(webhooks []registrationv1beta1.ValidatingWebhook) []registrationv1beta1.MutatingWebhook {
mutating := make([]registrationv1beta1.MutatingWebhook, len(webhooks))
for i, h := range webhooks {
mutating[i] = registrationv1beta1.MutatingWebhook{h.Name, h.ClientConfig, h.Rules, h.FailurePolicy, h.MatchPolicy, h.NamespaceSelector, h.SideEffects, h.TimeoutSeconds, h.AdmissionReviewVersions}
}
return mutating
}
// NewNonMutatingTestCases returns test cases with a given base url.
// All test cases in NewNonMutatingTestCases have no Patch set in
// AdmissionResponse. The test cases are used by both MutatingAdmissionWebhook
// and ValidatingAdmissionWebhook.
func NewNonMutatingTestCases(url *url.URL) []Test {
func NewNonMutatingTestCases(url *url.URL) []ValidatingTest {
policyFail := registrationv1beta1.Fail
policyIgnore := registrationv1beta1.Ignore
ccfgURL := urlConfigGenerator{url}.ccfgURL
return []Test{
return []ValidatingTest{
{
Name: "no match",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "nomatch",
ClientConfig: ccfgSVC("disallow"),
Rules: []registrationv1beta1.RuleWithOperations{{
@ -221,7 +270,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & allow",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "allow.example.com",
ClientConfig: ccfgSVC("allow"),
Rules: matchEverythingRules,
@ -233,7 +282,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & disallow",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "disallow",
ClientConfig: ccfgSVC("disallow"),
Rules: matchEverythingRules,
@ -245,7 +294,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & disallow ii",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "disallowReason",
ClientConfig: ccfgSVC("disallowReason"),
Rules: matchEverythingRules,
@ -257,7 +306,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & disallow & but allowed because namespaceSelector exempt the ns",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "disallow",
ClientConfig: ccfgSVC("disallow"),
Rules: newMatchEverythingRules(),
@ -275,7 +324,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & disallow & but allowed because namespaceSelector exempt the ns ii",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "disallow",
ClientConfig: ccfgSVC("disallow"),
Rules: newMatchEverythingRules(),
@ -292,7 +341,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & fail (but allow because fail open)",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "internalErr A",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
@ -319,7 +368,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & fail (but disallow because fail close on nil FailurePolicy)",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "internalErr A",
ClientConfig: ccfgSVC("internalErr"),
NamespaceSelector: &metav1.LabelSelector{},
@ -343,7 +392,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & fail (but fail because fail closed)",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "internalErr A",
ClientConfig: ccfgSVC("internalErr"),
Rules: matchEverythingRules,
@ -370,7 +419,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & allow (url)",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "allow.example.com",
ClientConfig: ccfgURL("allow"),
Rules: matchEverythingRules,
@ -382,7 +431,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & disallow (url)",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "disallow",
ClientConfig: ccfgURL("disallow"),
Rules: matchEverythingRules,
@ -393,7 +442,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
ErrorContains: "without explanation",
}, {
Name: "absent response and fail open",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "nilResponse",
ClientConfig: ccfgURL("nilResponse"),
FailurePolicy: &policyIgnore,
@ -405,7 +454,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "absent response and fail closed",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "nilResponse",
ClientConfig: ccfgURL("nilResponse"),
FailurePolicy: &policyFail,
@ -418,7 +467,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "no match dry run",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "nomatch",
ClientConfig: ccfgSVC("allow"),
Rules: []registrationv1beta1.RuleWithOperations{{
@ -433,7 +482,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match dry run side effects Unknown",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "allow",
ClientConfig: ccfgSVC("allow"),
Rules: matchEverythingRules,
@ -447,7 +496,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match dry run side effects None",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "allow",
ClientConfig: ccfgSVC("allow"),
Rules: matchEverythingRules,
@ -461,7 +510,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match dry run side effects Some",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "allow",
ClientConfig: ccfgSVC("allow"),
Rules: matchEverythingRules,
@ -475,7 +524,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match dry run side effects NoneOnDryRun",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "allow",
ClientConfig: ccfgSVC("allow"),
Rules: matchEverythingRules,
@ -489,7 +538,7 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
},
{
Name: "illegal annotation format",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "invalidAnnotation",
ClientConfig: ccfgURL("invalidAnnotation"),
Rules: matchEverythingRules,
@ -506,11 +555,11 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
// NewMutatingTestCases returns test cases with a given base url.
// All test cases in NewMutatingTestCases have Patch set in
// AdmissionResponse. The test cases are only used by both MutatingAdmissionWebhook.
func NewMutatingTestCases(url *url.URL) []Test {
return []Test{
func NewMutatingTestCases(url *url.URL) []MutatingTest {
return []MutatingTest{
{
Name: "match & remove label",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.MutatingWebhook{{
Name: "removelabel.example.com",
ClientConfig: ccfgSVC("removeLabel"),
Rules: matchEverythingRules,
@ -524,7 +573,7 @@ func NewMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & add label",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.MutatingWebhook{{
Name: "addLabel",
ClientConfig: ccfgSVC("addLabel"),
Rules: matchEverythingRules,
@ -536,7 +585,7 @@ func NewMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match CRD & add label",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.MutatingWebhook{{
Name: "addLabel",
ClientConfig: ccfgSVC("addLabel"),
Rules: matchEverythingRules,
@ -549,7 +598,7 @@ func NewMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match CRD & remove label",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.MutatingWebhook{{
Name: "removelabel.example.com",
ClientConfig: ccfgSVC("removeLabel"),
Rules: matchEverythingRules,
@ -564,7 +613,7 @@ func NewMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & invalid mutation",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.MutatingWebhook{{
Name: "invalidMutation",
ClientConfig: ccfgSVC("invalidMutation"),
Rules: matchEverythingRules,
@ -576,7 +625,7 @@ func NewMutatingTestCases(url *url.URL) []Test {
},
{
Name: "match & remove label dry run unsupported",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.MutatingWebhook{{
Name: "removeLabel",
ClientConfig: ccfgSVC("removeLabel"),
Rules: matchEverythingRules,
@ -596,7 +645,7 @@ func NewMutatingTestCases(url *url.URL) []Test {
// CachedTest is a test case for the client manager.
type CachedTest struct {
Name string
Webhooks []registrationv1beta1.Webhook
Webhooks []registrationv1beta1.ValidatingWebhook
ExpectAllow bool
ExpectCacheMiss bool
}
@ -609,7 +658,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
return []CachedTest{
{
Name: "uncached: service webhook, path 'allow'",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "cache1",
ClientConfig: ccfgSVC("allow"),
Rules: newMatchEverythingRules(),
@ -622,7 +671,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
},
{
Name: "uncached: service webhook, path 'internalErr'",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "cache2",
ClientConfig: ccfgSVC("internalErr"),
Rules: newMatchEverythingRules(),
@ -635,7 +684,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
},
{
Name: "cached: service webhook, path 'allow'",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "cache3",
ClientConfig: ccfgSVC("allow"),
Rules: newMatchEverythingRules(),
@ -648,7 +697,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
},
{
Name: "uncached: url webhook, path 'allow'",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "cache4",
ClientConfig: ccfgURL("allow"),
Rules: newMatchEverythingRules(),
@ -661,7 +710,7 @@ func NewCachedClientTestcases(url *url.URL) []CachedTest {
},
{
Name: "cached: service webhook, path 'allow'",
Webhooks: []registrationv1beta1.Webhook{{
Webhooks: []registrationv1beta1.ValidatingWebhook{{
Name: "cache5",
ClientConfig: ccfgURL("allow"),
Rules: newMatchEverythingRules(),

View File

@ -17,38 +17,39 @@ limitations under the License.
package util
import (
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/pkg/admission/plugin/webhook"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
)
// HookClientConfigForWebhook construct a webhook.ClientConfig using a v1beta1.Webhook API object.
// webhook.ClientConfig is used to create a HookClient and the purpose of the config struct is to
// share that with other packages that need to create a HookClient.
func HookClientConfigForWebhook(w *v1beta1.Webhook) webhook.ClientConfig {
ret := webhook.ClientConfig{Name: w.Name, CABundle: w.ClientConfig.CABundle}
if w.ClientConfig.URL != nil {
ret.URL = *w.ClientConfig.URL
// HookClientConfigForWebhook construct a webhookutil.ClientConfig using a WebhookAccessor to access
// v1beta1.MutatingWebhook and v1beta1.ValidatingWebhook API objects. webhookutil.ClientConfig is used
// to create a HookClient and the purpose of the config struct is to share that with other packages
// that need to create a HookClient.
func HookClientConfigForWebhook(w webhook.WebhookAccessor) webhookutil.ClientConfig {
ret := webhookutil.ClientConfig{Name: w.GetName(), CABundle: w.GetClientConfig().CABundle}
if w.GetClientConfig().URL != nil {
ret.URL = *w.GetClientConfig().URL
}
if w.ClientConfig.Service != nil {
ret.Service = &webhook.ClientConfigService{
Name: w.ClientConfig.Service.Name,
Namespace: w.ClientConfig.Service.Namespace,
if w.GetClientConfig().Service != nil {
ret.Service = &webhookutil.ClientConfigService{
Name: w.GetClientConfig().Service.Name,
Namespace: w.GetClientConfig().Service.Namespace,
}
if w.ClientConfig.Service.Port != nil {
ret.Service.Port = *w.ClientConfig.Service.Port
if w.GetClientConfig().Service.Port != nil {
ret.Service.Port = *w.GetClientConfig().Service.Port
} else {
ret.Service.Port = 443
}
if w.ClientConfig.Service.Path != nil {
ret.Service.Path = *w.ClientConfig.Service.Path
if w.GetClientConfig().Service.Path != nil {
ret.Service.Path = *w.GetClientConfig().Service.Path
}
}
return ret
}
// HasAdmissionReviewVersion check whether a version is accepted by a given webhook.
func HasAdmissionReviewVersion(a string, w *v1beta1.Webhook) bool {
for _, b := range w.AdmissionReviewVersions {
func HasAdmissionReviewVersion(a string, w webhook.WebhookAccessor) bool {
for _, b := range w.GetAdmissionReviewVersions() {
if b == a {
return true
}

View File

@ -22,8 +22,6 @@ import (
"sync"
"time"
"k8s.io/klog"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/api/admissionregistration/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -35,14 +33,15 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
"k8s.io/apiserver/pkg/admission/plugin/webhook/util"
"k8s.io/apiserver/pkg/util/webhook"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/klog"
)
type validatingDispatcher struct {
cm *webhook.ClientManager
cm *webhookutil.ClientManager
}
func newValidatingDispatcher(cm *webhook.ClientManager) generic.Dispatcher {
func newValidatingDispatcher(cm *webhookutil.ClientManager) generic.Dispatcher {
return &validatingDispatcher{cm}
}
@ -69,18 +68,21 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
for i := range relevantHooks {
go func(invocation *generic.WebhookInvocation) {
defer wg.Done()
hook := invocation.Webhook
hook, ok := invocation.Webhook.GetValidatingWebhook()
if !ok {
utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1beta1.ValidatingWebhook, but got %T", hook))
return
}
versionedAttr := versionedAttrs[invocation.Kind]
t := time.Now()
err := d.callHook(ctx, invocation, versionedAttr)
err := d.callHook(ctx, hook, invocation, versionedAttr)
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, versionedAttr.Attributes, "validating", hook.Name)
if err == nil {
return
}
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
if callErr, ok := err.(*webhook.ErrCallingWebhook); ok {
if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
if ignoreClientCallFailures {
klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr)
@ -115,11 +117,10 @@ func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attr
return errs[0]
}
func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error {
h := invocation.Webhook
func (d *validatingDispatcher) callHook(ctx context.Context, h *v1beta1.ValidatingWebhook, invocation *generic.WebhookInvocation, attr *generic.VersionedAttributes) error {
if attr.Attributes.IsDryRun() {
if h.SideEffects == nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook SideEffects is nil")}
}
if !(*h.SideEffects == v1beta1.SideEffectClassNone || *h.SideEffects == v1beta1.SideEffectClassNoneOnDryRun) {
return webhookerrors.NewDryRunUnsupportedErr(h.Name)
@ -128,15 +129,15 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic
// Currently dispatcher only supports `v1beta1` AdmissionReview
// TODO: Make the dispatcher capable of sending multiple AdmissionReview versions
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, h) {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReviewRequest")}
if !util.HasAdmissionReviewVersion(v1beta1.SchemeGroupVersion.Version, invocation.Webhook) {
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("webhook does not accept v1beta1 AdmissionReviewRequest")}
}
// Make the webhook request
request := request.CreateAdmissionReview(attr, invocation)
client, err := d.cm.HookClient(util.HookClientConfigForWebhook(h))
client, err := d.cm.HookClient(util.HookClientConfigForWebhook(invocation.Webhook))
if err != nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
response := &admissionv1beta1.AdmissionReview{}
r := client.Post().Context(ctx).Body(&request)
@ -144,11 +145,11 @@ func (d *validatingDispatcher) callHook(ctx context.Context, invocation *generic
r = r.Timeout(time.Duration(*h.TimeoutSeconds) * time.Second)
}
if err := r.Do().Into(response); err != nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
if response.Response == nil {
return &webhook.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
}
for k, v := range response.Response.AuditAnnotations {
key := h.Name + "/" + k

View File

@ -51,7 +51,7 @@ func TestValidate(t *testing.T) {
}
ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh)
client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
@ -116,7 +116,7 @@ func TestValidateCachedClient(t *testing.T) {
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh)
client, informer := webhooktesting.NewFakeValidatingDataSource(ns, tt.Webhooks, stopCh)
// override the webhook source. The client cache will stay the same.
cacheMisses := new(int32)