Merge pull request #55282 from mbohlool/webhooks

Automatic merge from submit-queue (batch tested with PRs 55268, 55282, 55419, 48340, 54829). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

 Add MutatingWebhookConfiguration type

As part of Mutating Webhook support, this PR adds the configuration for Mutating webhooks. It also renames existing ReadOnly webhook configurations from ExternalAdmissionHookConfiguration to ValidatingWebhookConfiguration. As part of the process some sub-types are also renamed.

Lastly, the mutating webhook configurations are sorted by name to make the serial executing of them deterministic.

ref: https://github.com/kubernetes/features/issues/492

Kubernetes-commit: 61f210859d9c4bd64af254ba696f6f693596ced9
This commit is contained in:
Kubernetes Publisher 2017-11-09 21:39:23 -08:00
commit d2374206e6
9 changed files with 488 additions and 344 deletions

458
Godeps/Godeps.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,8 +10,9 @@ go_test(
name = "go_default_test",
srcs = [
"configuration_manager_test.go",
"external_admission_hook_manager_test.go",
"initializer_manager_test.go",
"mutating_webhook_manager_test.go",
"validating_webhook_manager_test.go",
],
importpath = "k8s.io/apiserver/pkg/admission/configuration",
library = ":go_default_library",
@ -29,8 +30,9 @@ go_library(
name = "go_default_library",
srcs = [
"configuration_manager.go",
"external_admission_hook_manager.go",
"initializer_manager.go",
"mutating_webhook_manager.go",
"validating_webhook_manager.go",
],
importpath = "k8s.io/apiserver/pkg/admission/configuration",
deps = [

View File

@ -1,83 +0,0 @@
/*
Copyright 2017 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 configuration
import (
"fmt"
"reflect"
"github.com/golang/glog"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
type ExternalAdmissionHookConfigurationLister interface {
List(opts metav1.ListOptions) (*v1alpha1.ExternalAdmissionHookConfigurationList, error)
}
type ExternalAdmissionHookConfigurationManager struct {
*poller
}
func NewExternalAdmissionHookConfigurationManager(c ExternalAdmissionHookConfigurationLister) *ExternalAdmissionHookConfigurationManager {
getFn := func() (runtime.Object, error) {
list, err := c.List(metav1.ListOptions{})
if err != nil {
if errors.IsNotFound(err) || errors.IsForbidden(err) {
glog.V(5).Infof("ExternalAdmissionHookConfiguration are disabled due to an error: %v", err)
return nil, ErrDisabled
}
return nil, err
}
return mergeExternalAdmissionHookConfigurations(list), nil
}
return &ExternalAdmissionHookConfigurationManager{
newPoller(getFn),
}
}
// ExternalAdmissionHooks returns the merged ExternalAdmissionHookConfiguration.
func (im *ExternalAdmissionHookConfigurationManager) ExternalAdmissionHooks() (*v1alpha1.ExternalAdmissionHookConfiguration, error) {
configuration, err := im.poller.configuration()
if err != nil {
return nil, err
}
externalAdmissionHookConfiguration, ok := configuration.(*v1alpha1.ExternalAdmissionHookConfiguration)
if !ok {
return nil, fmt.Errorf("expected type %v, got type %v", reflect.TypeOf(externalAdmissionHookConfiguration), reflect.TypeOf(configuration))
}
return externalAdmissionHookConfiguration, nil
}
func (im *ExternalAdmissionHookConfigurationManager) Run(stopCh <-chan struct{}) {
im.poller.Run(stopCh)
}
func mergeExternalAdmissionHookConfigurations(
list *v1alpha1.ExternalAdmissionHookConfigurationList,
) *v1alpha1.ExternalAdmissionHookConfiguration {
configurations := list.Items
var ret v1alpha1.ExternalAdmissionHookConfiguration
for _, c := range configurations {
ret.ExternalAdmissionHooks = append(ret.ExternalAdmissionHooks, c.ExternalAdmissionHooks...)
}
return &ret
}

View File

@ -0,0 +1,101 @@
/*
Copyright 2017 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 configuration
import (
"fmt"
"reflect"
"sort"
"github.com/golang/glog"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
type MutatingWebhookConfigurationLister interface {
List(opts metav1.ListOptions) (*v1alpha1.MutatingWebhookConfigurationList, error)
}
// MutatingWebhookConfigurationManager collects the mutating webhook objects so that they can be called.
type MutatingWebhookConfigurationManager struct {
*poller
}
func NewMutatingWebhookConfigurationManager(c MutatingWebhookConfigurationLister) *MutatingWebhookConfigurationManager {
getFn := func() (runtime.Object, error) {
list, err := c.List(metav1.ListOptions{})
if err != nil {
if errors.IsNotFound(err) || errors.IsForbidden(err) {
glog.V(5).Infof("MutatingWebhookConfiguration are disabled due to an error: %v", err)
return nil, ErrDisabled
}
return nil, err
}
return mergeMutatingWebhookConfigurations(list), nil
}
return &MutatingWebhookConfigurationManager{
newPoller(getFn),
}
}
// Webhooks returns the merged MutatingWebhookConfiguration.
func (im *MutatingWebhookConfigurationManager) Webhooks() (*v1alpha1.MutatingWebhookConfiguration, error) {
configuration, err := im.poller.configuration()
if err != nil {
return nil, err
}
mutatingWebhookConfiguration, ok := configuration.(*v1alpha1.MutatingWebhookConfiguration)
if !ok {
return nil, fmt.Errorf("expected type %v, got type %v", reflect.TypeOf(mutatingWebhookConfiguration), reflect.TypeOf(configuration))
}
return mutatingWebhookConfiguration, nil
}
func (im *MutatingWebhookConfigurationManager) Run(stopCh <-chan struct{}) {
im.poller.Run(stopCh)
}
func mergeMutatingWebhookConfigurations(
list *v1alpha1.MutatingWebhookConfigurationList,
) *v1alpha1.MutatingWebhookConfiguration {
configurations := append([]v1alpha1.MutatingWebhookConfiguration{}, list.Items...)
var ret v1alpha1.MutatingWebhookConfiguration
// 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.Sort(byName(configurations))
for _, c := range configurations {
ret.Webhooks = append(ret.Webhooks, c.Webhooks...)
}
return &ret
}
// byName sorts MutatingWebhookConfiguration by name. These objects are all in
// cluster namespace (aka no namespace) thus they all have unique names.
type byName []v1alpha1.MutatingWebhookConfiguration
func (x byName) Len() int { return len(x) }
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byName) Less(i, j int) bool {
return x[i].ObjectMeta.Name < x[j].ObjectMeta.Name
}

View File

@ -0,0 +1,40 @@
/*
Copyright 2017 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 configuration
import (
"testing"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type disabledMutatingWebhookConfigLister struct{}
func (l *disabledMutatingWebhookConfigLister) List(options metav1.ListOptions) (*v1alpha1.MutatingWebhookConfigurationList, error) {
return nil, errors.NewNotFound(schema.GroupResource{Group: "admissionregistration", Resource: "MutatingWebhookConfigurations"}, "")
}
func TestMutatingWebhookConfigDisabled(t *testing.T) {
manager := NewMutatingWebhookConfigurationManager(&disabledMutatingWebhookConfigLister{})
manager.sync()
_, err := manager.Webhooks()
if err.Error() != ErrDisabled.Error() {
t.Errorf("expected %v, got %v", ErrDisabled, err)
}
}

View File

@ -0,0 +1,84 @@
/*
Copyright 2017 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 configuration
import (
"fmt"
"reflect"
"github.com/golang/glog"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
type ValidatingWebhookConfigurationLister interface {
List(opts metav1.ListOptions) (*v1alpha1.ValidatingWebhookConfigurationList, error)
}
// ValidatingWebhookConfigurationManager collects the validating webhook objects so that they can be called.
type ValidatingWebhookConfigurationManager struct {
*poller
}
func NewValidatingWebhookConfigurationManager(c ValidatingWebhookConfigurationLister) *ValidatingWebhookConfigurationManager {
getFn := func() (runtime.Object, error) {
list, err := c.List(metav1.ListOptions{})
if err != nil {
if errors.IsNotFound(err) || errors.IsForbidden(err) {
glog.V(5).Infof("ValidatingWebhookConfiguration are disabled due to an error: %v", err)
return nil, ErrDisabled
}
return nil, err
}
return mergeValidatingWebhookConfigurations(list), nil
}
return &ValidatingWebhookConfigurationManager{
newPoller(getFn),
}
}
// Webhooks returns the merged ValidatingWebhookConfiguration.
func (im *ValidatingWebhookConfigurationManager) Webhooks() (*v1alpha1.ValidatingWebhookConfiguration, error) {
configuration, err := im.poller.configuration()
if err != nil {
return nil, err
}
validatingWebhookConfiguration, ok := configuration.(*v1alpha1.ValidatingWebhookConfiguration)
if !ok {
return nil, fmt.Errorf("expected type %v, got type %v", reflect.TypeOf(validatingWebhookConfiguration), reflect.TypeOf(configuration))
}
return validatingWebhookConfiguration, nil
}
func (im *ValidatingWebhookConfigurationManager) Run(stopCh <-chan struct{}) {
im.poller.Run(stopCh)
}
func mergeValidatingWebhookConfigurations(
list *v1alpha1.ValidatingWebhookConfigurationList,
) *v1alpha1.ValidatingWebhookConfiguration {
configurations := list.Items
var ret v1alpha1.ValidatingWebhookConfiguration
for _, c := range configurations {
ret.Webhooks = append(ret.Webhooks, c.Webhooks...)
}
return &ret
}

View File

@ -25,15 +25,15 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
type disabledWebhookConfigLister struct{}
type disabledValidatingWebhookConfigLister struct{}
func (l *disabledWebhookConfigLister) List(options metav1.ListOptions) (*v1alpha1.ExternalAdmissionHookConfigurationList, error) {
return nil, errors.NewNotFound(schema.GroupResource{Group: "admissionregistration", Resource: "externalAdmissionHookConfigurations"}, "")
func (l *disabledValidatingWebhookConfigLister) List(options metav1.ListOptions) (*v1alpha1.ValidatingWebhookConfigurationList, error) {
return nil, errors.NewNotFound(schema.GroupResource{Group: "admissionregistration", Resource: "ValidatingWebhookConfigurations"}, "")
}
func TestWebhookConfigDisabled(t *testing.T) {
manager := NewExternalAdmissionHookConfigurationManager(&disabledWebhookConfigLister{})
manager := NewValidatingWebhookConfigurationManager(&disabledValidatingWebhookConfigLister{})
manager.sync()
_, err := manager.ExternalAdmissionHooks()
_, err := manager.Webhooks()
if err.Error() != ErrDisabled.Error() {
t.Errorf("expected %v, got %v", ErrDisabled, err)
}

View File

@ -75,7 +75,7 @@ func Register(plugins *admission.Plugins) {
// WebhookSource can list dynamic webhook plugins.
type WebhookSource interface {
Run(stopCh <-chan struct{})
ExternalAdmissionHooks() (*v1alpha1.ExternalAdmissionHookConfiguration, error)
Webhooks() (*v1alpha1.ValidatingWebhookConfiguration, error)
}
// NewGenericAdmissionWebhook returns a generic admission webhook plugin.
@ -153,7 +153,7 @@ func (a *GenericAdmissionWebhook) SetScheme(scheme *runtime.Scheme) {
// WantsExternalKubeClientSet defines a function which sets external ClientSet for admission plugins that need it
func (a *GenericAdmissionWebhook) SetExternalKubeClientSet(client clientset.Interface) {
a.hookSource = configuration.NewExternalAdmissionHookConfigurationManager(client.AdmissionregistrationV1alpha1().ExternalAdmissionHookConfigurations())
a.hookSource = configuration.NewValidatingWebhookConfigurationManager(client.AdmissionregistrationV1alpha1().ValidatingWebhookConfigurations())
}
// ValidateInitialization implements the InitializationValidator interface.
@ -168,19 +168,19 @@ func (a *GenericAdmissionWebhook) ValidateInitialization() error {
return nil
}
func (a *GenericAdmissionWebhook) loadConfiguration(attr admission.Attributes) (*v1alpha1.ExternalAdmissionHookConfiguration, error) {
hookConfig, err := a.hookSource.ExternalAdmissionHooks()
// if ExternalAdmissionHook configuration is disabled, fail open
func (a *GenericAdmissionWebhook) loadConfiguration(attr admission.Attributes) (*v1alpha1.ValidatingWebhookConfiguration, error) {
hookConfig, err := a.hookSource.Webhooks()
// if Webhook configuration is disabled, fail open
if err == configuration.ErrDisabled {
return &v1alpha1.ExternalAdmissionHookConfiguration{}, nil
return &v1alpha1.ValidatingWebhookConfiguration{}, nil
}
if err != nil {
e := apierrors.NewServerTimeout(attr.GetResource().GroupResource(), string(attr.GetOperation()), 1)
e.ErrStatus.Message = fmt.Sprintf("Unable to refresh the ExternalAdmissionHook configuration: %v", err)
e.ErrStatus.Message = fmt.Sprintf("Unable to refresh the Webhook configuration: %v", err)
e.ErrStatus.Reason = "LoadingConfiguration"
e.ErrStatus.Details.Causes = append(e.ErrStatus.Details.Causes, metav1.StatusCause{
Type: "ExternalAdmissionHookConfigurationFailure",
Message: "An error has occurred while refreshing the externalAdmissionHook configuration, no resources can be created/updated/deleted/connected until a refresh succeeds.",
Type: "ValidatingWebhookConfigurationFailure",
Message: "An error has occurred while refreshing the ValidatingWebhook configuration, no resources can be created/updated/deleted/connected until a refresh succeeds.",
})
return nil, e
}
@ -193,14 +193,14 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
if err != nil {
return err
}
hooks := hookConfig.ExternalAdmissionHooks
hooks := hookConfig.Webhooks
ctx := context.TODO()
errCh := make(chan error, len(hooks))
wg := sync.WaitGroup{}
wg.Add(len(hooks))
for i := range hooks {
go func(hook *v1alpha1.ExternalAdmissionHook) {
go func(hook *v1alpha1.Webhook) {
defer wg.Done()
err := a.callHook(ctx, hook, attr)
@ -245,7 +245,7 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
return errs[0]
}
func (a *GenericAdmissionWebhook) callHook(ctx context.Context, h *v1alpha1.ExternalAdmissionHook, attr admission.Attributes) error {
func (a *GenericAdmissionWebhook) callHook(ctx context.Context, h *v1alpha1.Webhook, attr admission.Attributes) error {
matches := false
for _, r := range h.Rules {
m := RuleMatcher{Rule: r, Attr: attr}
@ -299,7 +299,7 @@ func toStatusErr(name string, result *metav1.Status) *apierrors.StatusError {
}
}
func (a *GenericAdmissionWebhook) hookClient(h *v1alpha1.ExternalAdmissionHook) (*rest.RESTClient, error) {
func (a *GenericAdmissionWebhook) hookClient(h *v1alpha1.Webhook) (*rest.RESTClient, error) {
serverName := h.ClientConfig.Service.Name + "." + h.ClientConfig.Service.Namespace + ".svc"
u, err := a.serviceResolver.ResolveEndpoint(h.ClientConfig.Service.Namespace, h.ClientConfig.Service.Name)
if err != nil {

View File

@ -39,15 +39,15 @@ import (
)
type fakeHookSource struct {
hooks []registrationv1alpha1.ExternalAdmissionHook
hooks []registrationv1alpha1.Webhook
err error
}
func (f *fakeHookSource) ExternalAdmissionHooks() (*registrationv1alpha1.ExternalAdmissionHookConfiguration, error) {
func (f *fakeHookSource) Webhooks() (*registrationv1alpha1.ValidatingWebhookConfiguration, error) {
if f.err != nil {
return nil, f.err
}
return &registrationv1alpha1.ExternalAdmissionHookConfiguration{ExternalAdmissionHooks: f.hooks}, nil
return &registrationv1alpha1.ValidatingWebhookConfiguration{Webhooks: f.hooks}, nil
}
func (f *fakeHookSource) Run(stopCh <-chan struct{}) {}
@ -137,8 +137,8 @@ func TestAdmit(t *testing.T) {
expectAllow bool
errorContains string
}
ccfg := func(urlPath string) registrationv1alpha1.AdmissionHookClientConfig {
return registrationv1alpha1.AdmissionHookClientConfig{
ccfg := func(urlPath string) registrationv1alpha1.WebhookClientConfig {
return registrationv1alpha1.WebhookClientConfig{
Service: registrationv1alpha1.ServiceReference{
Name: "webhook-test",
Namespace: "default",
@ -163,7 +163,7 @@ func TestAdmit(t *testing.T) {
table := map[string]test{
"no match": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
hooks: []registrationv1alpha1.Webhook{{
Name: "nomatch",
ClientConfig: ccfg("disallow"),
Rules: []registrationv1alpha1.RuleWithOperations{{
@ -175,7 +175,7 @@ func TestAdmit(t *testing.T) {
},
"match & allow": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
hooks: []registrationv1alpha1.Webhook{{
Name: "allow",
ClientConfig: ccfg("allow"),
Rules: matchEverythingRules,
@ -185,7 +185,7 @@ func TestAdmit(t *testing.T) {
},
"match & disallow": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallow",
ClientConfig: ccfg("disallow"),
Rules: matchEverythingRules,
@ -195,7 +195,7 @@ func TestAdmit(t *testing.T) {
},
"match & disallow ii": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
hooks: []registrationv1alpha1.Webhook{{
Name: "disallowReason",
ClientConfig: ccfg("disallowReason"),
Rules: matchEverythingRules,
@ -205,7 +205,7 @@ func TestAdmit(t *testing.T) {
},
"match & fail (but allow because fail open)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
hooks: []registrationv1alpha1.Webhook{{
Name: "internalErr A",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
@ -226,7 +226,7 @@ func TestAdmit(t *testing.T) {
},
"match & fail (but disallow because fail closed on nil)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
hooks: []registrationv1alpha1.Webhook{{
Name: "internalErr A",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,
@ -244,7 +244,7 @@ func TestAdmit(t *testing.T) {
},
"match & fail (but fail because fail closed)": {
hookSource: fakeHookSource{
hooks: []registrationv1alpha1.ExternalAdmissionHook{{
hooks: []registrationv1alpha1.Webhook{{
Name: "internalErr A",
ClientConfig: ccfg("internalErr"),
Rules: matchEverythingRules,