Unverified Commit 80bd7510 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #58381 from sttts/sttts-deduplicate-admission-webhooks

Automatic merge from submit-queue. 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>. admission/webhook: deduplicate mutating and validating webhook code This refactors along: webhook = generic-webhook + source + dispatcher TODOs: - [x] refactor unit tests
parents a8482ef5 4eb48436
......@@ -1171,6 +1171,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/generic",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......@@ -1191,10 +1195,6 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/apis/apiserver",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......
......@@ -1755,10 +1755,6 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/informers/admissionregistration/v1beta1",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/kubernetes",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......
......@@ -72,12 +72,14 @@ filegroup(
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/config:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/initializer:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating:all-srcs",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned:all-srcs",
],
......
......@@ -20,12 +20,11 @@ go_test(
"//vendor/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/client-go/listers/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
],
)
......@@ -48,7 +47,8 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/client-go/informers/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/listers/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library",
],
......
......@@ -24,21 +24,27 @@ import (
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
admissionregistrationinformers "k8s.io/client-go/informers/admissionregistration/v1beta1"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
"k8s.io/client-go/tools/cache"
)
// MutatingWebhookConfigurationManager collects the mutating webhook objects so that they can be called.
type MutatingWebhookConfigurationManager struct {
// mutatingWebhookConfigurationManager collects the mutating webhook objects so that they can be called.
type mutatingWebhookConfigurationManager struct {
configuration *atomic.Value
lister admissionregistrationlisters.MutatingWebhookConfigurationLister
hasSynced func() bool
}
func NewMutatingWebhookConfigurationManager(informer admissionregistrationinformers.MutatingWebhookConfigurationInformer) *MutatingWebhookConfigurationManager {
manager := &MutatingWebhookConfigurationManager{
var _ generic.Source = &mutatingWebhookConfigurationManager{}
func NewMutatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
informer := f.Admissionregistration().V1beta1().MutatingWebhookConfigurations()
manager := &mutatingWebhookConfigurationManager{
configuration: &atomic.Value{},
lister: informer.Lister(),
hasSynced: informer.Informer().HasSynced,
}
// Start with an empty list
......@@ -55,11 +61,15 @@ func NewMutatingWebhookConfigurationManager(informer admissionregistrationinform
}
// Webhooks returns the merged MutatingWebhookConfiguration.
func (m *MutatingWebhookConfigurationManager) Webhooks() *v1beta1.MutatingWebhookConfiguration {
return m.configuration.Load().(*v1beta1.MutatingWebhookConfiguration)
func (m *mutatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook {
return m.configuration.Load().(*v1beta1.MutatingWebhookConfiguration).Webhooks
}
func (m *mutatingWebhookConfigurationManager) HasSynced() bool {
return m.hasSynced()
}
func (m *MutatingWebhookConfigurationManager) updateConfiguration() {
func (m *mutatingWebhookConfigurationManager) updateConfiguration() {
configurations, err := m.lister.List(labels.Everything())
if err != nil {
utilruntime.HandleError(fmt.Errorf("error updating configuration: %v", err))
......
......@@ -17,107 +17,47 @@ limitations under the License.
package configuration
import (
"fmt"
"reflect"
"testing"
"time"
"k8s.io/api/admissionregistration/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
)
type fakeMutatingWebhookConfigSharedInformer struct {
informer *fakeMutatingWebhookConfigInformer
lister *fakeMutatingWebhookConfigLister
}
func (f *fakeMutatingWebhookConfigSharedInformer) Informer() cache.SharedIndexInformer {
return f.informer
}
func (f *fakeMutatingWebhookConfigSharedInformer) Lister() admissionregistrationlisters.MutatingWebhookConfigurationLister {
return f.lister
}
type fakeMutatingWebhookConfigInformer struct {
eventHandler cache.ResourceEventHandler
}
func (f *fakeMutatingWebhookConfigInformer) AddEventHandler(handler cache.ResourceEventHandler) {
fmt.Println("added handler")
f.eventHandler = handler
}
func (f *fakeMutatingWebhookConfigInformer) AddEventHandlerWithResyncPeriod(handler cache.ResourceEventHandler, resyncPeriod time.Duration) {
panic("unsupported")
}
func (f *fakeMutatingWebhookConfigInformer) GetStore() cache.Store {
panic("unsupported")
}
func (f *fakeMutatingWebhookConfigInformer) GetController() cache.Controller {
panic("unsupported")
}
func (f *fakeMutatingWebhookConfigInformer) Run(stopCh <-chan struct{}) {
panic("unsupported")
}
func (f *fakeMutatingWebhookConfigInformer) HasSynced() bool {
panic("unsupported")
}
func (f *fakeMutatingWebhookConfigInformer) LastSyncResourceVersion() string {
panic("unsupported")
}
func (f *fakeMutatingWebhookConfigInformer) AddIndexers(indexers cache.Indexers) error {
panic("unsupported")
}
func (f *fakeMutatingWebhookConfigInformer) GetIndexer() cache.Indexer { panic("unsupported") }
type fakeMutatingWebhookConfigLister struct {
list []*v1beta1.MutatingWebhookConfiguration
err error
}
func (f *fakeMutatingWebhookConfigLister) List(selector labels.Selector) (ret []*v1beta1.MutatingWebhookConfiguration, err error) {
return f.list, f.err
}
func (f *fakeMutatingWebhookConfigLister) Get(name string) (*v1beta1.MutatingWebhookConfiguration, error) {
panic("unsupported")
}
func TestGetMutatingWebhookConfig(t *testing.T) {
informer := &fakeMutatingWebhookConfigSharedInformer{
informer: &fakeMutatingWebhookConfigInformer{},
lister: &fakeMutatingWebhookConfigLister{},
}
// Build a test client that the admission plugin can use to look up the MutatingWebhookConfiguration
client := fake.NewSimpleClientset()
informerFactory := informers.NewSharedInformerFactory(client, 0)
stop := make(chan struct{})
defer close(stop)
informerFactory.Start(stop)
informerFactory.WaitForCacheSync(stop)
configManager := NewMutatingWebhookConfigurationManager(informerFactory).(*mutatingWebhookConfigurationManager)
configManager.updateConfiguration()
// no configurations
informer.lister.list = nil
manager := NewMutatingWebhookConfigurationManager(informer)
if configurations := manager.Webhooks(); len(configurations.Webhooks) != 0 {
t.Errorf("expected empty webhooks, but got %v", configurations.Webhooks)
if configurations := configManager.Webhooks(); len(configurations) != 0 {
t.Errorf("expected empty webhooks, but got %v", configurations)
}
// list err
webhookConfiguration := &v1beta1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}},
}
informer.lister.list = []*v1beta1.MutatingWebhookConfiguration{webhookConfiguration.DeepCopy()}
informer.lister.err = fmt.Errorf("mutating webhook configuration list error")
informer.informer.eventHandler.OnAdd(webhookConfiguration.DeepCopy())
if configurations := manager.Webhooks(); len(configurations.Webhooks) != 0 {
t.Errorf("expected empty webhooks, but got %v", configurations.Webhooks)
}
mutatingInformer := informerFactory.Admissionregistration().V1beta1().MutatingWebhookConfigurations()
mutatingInformer.Informer().GetIndexer().Add(webhookConfiguration)
configManager.updateConfiguration()
// configuration populated
informer.lister.err = nil
informer.informer.eventHandler.OnAdd(webhookConfiguration.DeepCopy())
configurations := manager.Webhooks()
if len(configurations.Webhooks) == 0 {
configurations := configManager.Webhooks()
if len(configurations) == 0 {
t.Errorf("expected non empty webhooks")
}
if !reflect.DeepEqual(configurations.Webhooks, webhookConfiguration.Webhooks) {
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations.Webhooks)
if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) {
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations)
}
}
......@@ -24,21 +24,27 @@ import (
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
admissionregistrationinformers "k8s.io/client-go/informers/admissionregistration/v1beta1"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/informers"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
"k8s.io/client-go/tools/cache"
)
// ValidatingWebhookConfigurationManager collects the validating webhook objects so that they can be called.
type ValidatingWebhookConfigurationManager struct {
// validatingWebhookConfigurationManager collects the validating webhook objects so that they can be called.
type validatingWebhookConfigurationManager struct {
configuration *atomic.Value
lister admissionregistrationlisters.ValidatingWebhookConfigurationLister
hasSynced func() bool
}
func NewValidatingWebhookConfigurationManager(informer admissionregistrationinformers.ValidatingWebhookConfigurationInformer) *ValidatingWebhookConfigurationManager {
manager := &ValidatingWebhookConfigurationManager{
var _ generic.Source = &validatingWebhookConfigurationManager{}
func NewValidatingWebhookConfigurationManager(f informers.SharedInformerFactory) generic.Source {
informer := f.Admissionregistration().V1beta1().ValidatingWebhookConfigurations()
manager := &validatingWebhookConfigurationManager{
configuration: &atomic.Value{},
lister: informer.Lister(),
hasSynced: informer.Informer().HasSynced,
}
// Start with an empty list
......@@ -55,11 +61,16 @@ func NewValidatingWebhookConfigurationManager(informer admissionregistrationinfo
}
// Webhooks returns the merged ValidatingWebhookConfiguration.
func (v *ValidatingWebhookConfigurationManager) Webhooks() *v1beta1.ValidatingWebhookConfiguration {
return v.configuration.Load().(*v1beta1.ValidatingWebhookConfiguration)
func (v *validatingWebhookConfigurationManager) Webhooks() []v1beta1.Webhook {
return v.configuration.Load().(*v1beta1.ValidatingWebhookConfiguration).Webhooks
}
// HasSynced returns true if the shared informers have synced.
func (v *validatingWebhookConfigurationManager) HasSynced() bool {
return v.hasSynced()
}
func (v *ValidatingWebhookConfigurationManager) updateConfiguration() {
func (v *validatingWebhookConfigurationManager) updateConfiguration() {
configurations, err := v.lister.List(labels.Everything())
if err != nil {
utilruntime.HandleError(fmt.Errorf("error updating configuration: %v", err))
......
......@@ -17,107 +17,49 @@ limitations under the License.
package configuration
import (
"fmt"
"reflect"
"testing"
"time"
"k8s.io/api/admissionregistration/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
admissionregistrationlisters "k8s.io/client-go/listers/admissionregistration/v1beta1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
)
type fakeValidatingWebhookConfigSharedInformer struct {
informer *fakeValidatingWebhookConfigInformer
lister *fakeValidatingWebhookConfigLister
}
func (f *fakeValidatingWebhookConfigSharedInformer) Informer() cache.SharedIndexInformer {
return f.informer
}
func (f *fakeValidatingWebhookConfigSharedInformer) Lister() admissionregistrationlisters.ValidatingWebhookConfigurationLister {
return f.lister
}
type fakeValidatingWebhookConfigInformer struct {
eventHandler cache.ResourceEventHandler
}
func (f *fakeValidatingWebhookConfigInformer) AddEventHandler(handler cache.ResourceEventHandler) {
fmt.Println("added handler")
f.eventHandler = handler
}
func (f *fakeValidatingWebhookConfigInformer) AddEventHandlerWithResyncPeriod(handler cache.ResourceEventHandler, resyncPeriod time.Duration) {
panic("unsupported")
}
func (f *fakeValidatingWebhookConfigInformer) GetStore() cache.Store {
panic("unsupported")
}
func (f *fakeValidatingWebhookConfigInformer) GetController() cache.Controller {
panic("unsupported")
}
func (f *fakeValidatingWebhookConfigInformer) Run(stopCh <-chan struct{}) {
panic("unsupported")
}
func (f *fakeValidatingWebhookConfigInformer) HasSynced() bool {
panic("unsupported")
}
func (f *fakeValidatingWebhookConfigInformer) LastSyncResourceVersion() string {
panic("unsupported")
}
func (f *fakeValidatingWebhookConfigInformer) AddIndexers(indexers cache.Indexers) error {
panic("unsupported")
}
func (f *fakeValidatingWebhookConfigInformer) GetIndexer() cache.Indexer { panic("unsupported") }
type fakeValidatingWebhookConfigLister struct {
list []*v1beta1.ValidatingWebhookConfiguration
err error
}
func (f *fakeValidatingWebhookConfigLister) List(selector labels.Selector) (ret []*v1beta1.ValidatingWebhookConfiguration, err error) {
return f.list, f.err
}
func (f *fakeValidatingWebhookConfigLister) Get(name string) (*v1beta1.ValidatingWebhookConfiguration, error) {
panic("unsupported")
}
func TestGettValidatingWebhookConfig(t *testing.T) {
informer := &fakeValidatingWebhookConfigSharedInformer{
informer: &fakeValidatingWebhookConfigInformer{},
lister: &fakeValidatingWebhookConfigLister{},
func TestGetValidatingWebhookConfig(t *testing.T) {
// Build a test client that the admission plugin can use to look up the ValidatingWebhookConfiguration
client := fake.NewSimpleClientset()
informerFactory := informers.NewSharedInformerFactory(client, 0)
stop := make(chan struct{})
defer close(stop)
informerFactory.Start(stop)
informerFactory.WaitForCacheSync(stop)
manager := NewValidatingWebhookConfigurationManager(informerFactory)
if validatingConfig, ok := manager.(*validatingWebhookConfigurationManager); ok {
validatingConfig.updateConfiguration()
}
// no configurations
informer.lister.list = nil
manager := NewValidatingWebhookConfigurationManager(informer)
if configurations := manager.Webhooks(); len(configurations.Webhooks) != 0 {
t.Errorf("expected empty webhooks, but got %v", configurations.Webhooks)
if configurations := manager.Webhooks(); len(configurations) != 0 {
t.Errorf("expected empty webhooks, but got %v", configurations)
}
// list error
webhookConfiguration := &v1beta1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{Name: "webhook1"},
Webhooks: []v1beta1.Webhook{{Name: "webhook1.1"}},
}
informer.lister.list = []*v1beta1.ValidatingWebhookConfiguration{webhookConfiguration.DeepCopy()}
informer.lister.err = fmt.Errorf("validating webhook configuration list error")
informer.informer.eventHandler.OnAdd(webhookConfiguration.DeepCopy())
if configurations := manager.Webhooks(); len(configurations.Webhooks) != 0 {
t.Errorf("expected empty webhooks, but got %v", configurations.Webhooks)
}
validatingInformer := informerFactory.Admissionregistration().V1beta1().ValidatingWebhookConfigurations()
validatingInformer.Informer().GetIndexer().Add(webhookConfiguration)
if validatingConfig, ok := manager.(*validatingWebhookConfigurationManager); ok {
validatingConfig.updateConfiguration()
}
// configuration populated
informer.lister.err = nil
informer.informer.eventHandler.OnAdd(webhookConfiguration.DeepCopy())
configurations := manager.Webhooks()
if len(configurations.Webhooks) == 0 {
if len(configurations) == 0 {
t.Errorf("expected non empty webhooks")
}
if !reflect.DeepEqual(configurations.Webhooks, webhookConfiguration.Webhooks) {
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations.Webhooks)
if !reflect.DeepEqual(configurations, webhookConfiguration.Webhooks) {
t.Errorf("Expected\n%#v\ngot\n%#v", webhookConfiguration.Webhooks, configurations)
}
}
......@@ -50,7 +50,7 @@ type ClientManager struct {
cache *lru.Cache
}
// NewClientManager creates a ClientManager.
// NewClientManager creates a clientManager.
func NewClientManager() (ClientManager, error) {
cache, err := lru.New(defaultCacheSize)
if err != nil {
......@@ -90,13 +90,13 @@ func (cm *ClientManager) SetServiceResolver(sr ServiceResolver) {
func (cm *ClientManager) Validate() error {
var errs []error
if cm.negotiatedSerializer == nil {
errs = append(errs, fmt.Errorf("the ClientManager requires a negotiatedSerializer"))
errs = append(errs, fmt.Errorf("the clientManager requires a negotiatedSerializer"))
}
if cm.serviceResolver == nil {
errs = append(errs, fmt.Errorf("the ClientManager requires a serviceResolver"))
errs = append(errs, fmt.Errorf("the clientManager requires a serviceResolver"))
}
if cm.authInfoResolver == nil {
errs = append(errs, fmt.Errorf("the ClientManager requires an authInfoResolver"))
errs = append(errs, fmt.Errorf("the clientManager requires an authInfoResolver"))
}
return utilerrors.NewAggregate(errs)
}
......
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"conversion.go",
"interfaces.go",
"webhook.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/generic",
visibility = ["//visibility:public"],
deps = [
"//vendor/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = ["conversion_test.go"],
embed = [":go_default_library"],
deps = [
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/example:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library",
],
)
......@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package versioned
package generic
import (
"fmt"
......@@ -23,25 +23,13 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
// Convertor converts objects to the desired version.
type Convertor struct {
// convertor converts objects to the desired version.
type convertor struct {
Scheme *runtime.Scheme
}
// Convert converts the in object to the out object and returns an error if the
// conversion fails.
func (c Convertor) Convert(in runtime.Object, out runtime.Object) error {
// For custom resources, because ConvertToGVK reuses the passed in object as
// the output. c.Scheme.Convert resets the objects to empty if in == out, so
// we skip the conversion if that's the case.
if in == out {
return nil
}
return c.Scheme.Convert(in, out, nil)
}
// ConvertToGVK converts object to the desired gvk.
func (c Convertor) ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind) (runtime.Object, error) {
func (c *convertor) ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind) (runtime.Object, error) {
// Unlike other resources, custom resources do not have internal version, so
// if obj is a custom resource, it should not need conversion.
if obj.GetObjectKind().GroupVersionKind() == gvk {
......@@ -59,9 +47,9 @@ func (c Convertor) ConvertToGVK(obj runtime.Object, gvk schema.GroupVersionKind)
}
// Validate checks if the conversion has a scheme.
func (c *Convertor) Validate() error {
func (c *convertor) Validate() error {
if c.Scheme == nil {
return fmt.Errorf("the Convertor requires a scheme")
return fmt.Errorf("the convertor requires a scheme")
}
return nil
}
......@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package versioned
package generic
import (
"reflect"
......@@ -39,7 +39,7 @@ func initiateScheme() *runtime.Scheme {
func TestConvertToGVK(t *testing.T) {
scheme := initiateScheme()
c := Convertor{Scheme: scheme}
c := convertor{Scheme: scheme}
table := map[string]struct {
obj runtime.Object
gvk schema.GroupVersionKind
......@@ -131,89 +131,21 @@ func TestConvertToGVK(t *testing.T) {
}
}
func TestConvert(t *testing.T) {
// TestRuntimeSchemeConvert verifies that scheme.Convert(x, x, nil) for an unstructured x is a no-op.
// This did not use to be like that and we had to wrap scheme.Convert before.
func TestRuntimeSchemeConvert(t *testing.T) {
scheme := initiateScheme()
c := Convertor{Scheme: scheme}
sampleCRD := unstructured.Unstructured{
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.k8s.io/v1",
"kind": "Flunder",
"data": map[string]interface{}{
"Key": "Value",
},
"foo": "bar",
},
}
clone := obj.DeepCopy()
table := map[string]struct {
in runtime.Object
out runtime.Object
expectedObj runtime.Object
}{
"convert example/v1#Pod to example#Pod": {
in: &examplev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Labels: map[string]string{
"key": "value",
},
},
Spec: examplev1.PodSpec{
RestartPolicy: examplev1.RestartPolicy("never"),
},
},
out: &example.Pod{},
expectedObj: &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example.PodSpec{
RestartPolicy: example.RestartPolicy("never"),
},
},
},
"convert example2/v1#replicaset to example#replicaset": {
in: &example2v1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "rs1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example2v1.ReplicaSetSpec{
Replicas: func() *int32 { var i int32; i = 1; return &i }(),
},
},
out: &example.ReplicaSet{},
expectedObj: &example.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "rs1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example.ReplicaSetSpec{
Replicas: 1,
},
},
},
"no conversion if the object is the same": {
in: &sampleCRD,
out: &sampleCRD,
expectedObj: &sampleCRD,
},
if err := scheme.Convert(obj, obj, nil); err != nil {
t.Fatalf("unexpected convert error: %v", err)
}
for name, test := range table {
t.Run(name, func(t *testing.T) {
err := c.Convert(test.in, test.out)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(test.out, test.expectedObj) {
t.Errorf("\nexpected:\n%#v\ngot:\n %#v\n", test.expectedObj, test.out)
}
})
if !reflect.DeepEqual(obj, clone) {
t.Errorf("unexpected mutation of self-converted Unstructured: obj=%#v, clone=%#v", obj, clone)
}
}
/*
Copyright 2017 The Kubernetes Authors.
Copyright 2018 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.
......@@ -14,29 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package versioned
package generic
import (
"context"
"k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
)
// Attributes is a wrapper around the original admission attributes. It allows
// override the internal objects with the versioned ones.
type Attributes struct {
admission.Attributes
OldObject runtime.Object
Object runtime.Object
// Source can list dynamic webhook plugins.
type Source interface {
Webhooks() []v1beta1.Webhook
HasSynced() bool
}
// GetObject overrides the original GetObjects() and it returns the versioned
// object.
func (v Attributes) GetObject() runtime.Object {
return v.Object
// VersionedAttributes is a wrapper around the original admission attributes, adding versioned
// variants of the object and old object.
type VersionedAttributes struct {
admission.Attributes
VersionedOldObject runtime.Object
VersionedObject runtime.Object
}
// GetOldObject overrides the original GetOldObjects() and it returns the
// versioned oldObject.
func (v Attributes) GetOldObject() runtime.Object {
return v.OldObject
// Dispatcher dispatches webhook call to a list of webhooks with admission attributes as argument.
type Dispatcher interface {
// Dispatch a request to the webhooks using the given webhooks. A non-nil error means the request is rejected.
Dispatch(ctx context.Context, a *VersionedAttributes, hooks []*v1beta1.Webhook) error
}
/*
Copyright 2018 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 generic
import (
"context"
"fmt"
"io"
"k8s.io/api/admissionregistration/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
"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/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
)
// Webhook is an abstract admission plugin with all the infrastructure to define Admit or Validate on-top.
type Webhook struct {
*admission.Handler
sourceFactory sourceFactory
hookSource Source
clientManager *config.ClientManager
convertor *convertor
namespaceMatcher *namespace.Matcher
dispatcher Dispatcher
}
var (
_ genericadmissioninit.WantsExternalKubeClientSet = &Webhook{}
_ admission.Interface = &Webhook{}
)
type sourceFactory func(f informers.SharedInformerFactory) Source
type dispatcherFactory func(cm *config.ClientManager) Dispatcher
// NewWebhook creates a new generic admission webhook.
func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory sourceFactory, dispatcherFactory dispatcherFactory) (*Webhook, error) {
kubeconfigFile, err := config.LoadConfig(configFile)
if err != nil {
return nil, err
}
cm, err := config.NewClientManager()
if err != nil {
return nil, err
}
authInfoResolver, err := config.NewDefaultAuthenticationInfoResolver(kubeconfigFile)
if err != nil {
return nil, err
}
// Set defaults which may be overridden later.
cm.SetAuthenticationInfoResolver(authInfoResolver)
cm.SetServiceResolver(config.NewDefaultServiceResolver())
return &Webhook{
Handler: handler,
sourceFactory: sourceFactory,
clientManager: &cm,
convertor: &convertor{},
namespaceMatcher: &namespace.Matcher{},
dispatcher: dispatcherFactory(&cm),
}, nil
}
// SetAuthenticationInfoResolverWrapper sets the
// AuthenticationInfoResolverWrapper.
// TODO find a better way wire this, but keep this pull small for now.
func (a *Webhook) SetAuthenticationInfoResolverWrapper(wrapper config.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 config.ServiceResolver) {
a.clientManager.SetServiceResolver(sr)
}
// SetScheme sets a serializer(NegotiatedSerializer) which is derived from the scheme
func (a *Webhook) SetScheme(scheme *runtime.Scheme) {
if scheme != nil {
a.convertor.Scheme = scheme
}
}
// SetExternalKubeClientSet implements the WantsExternalKubeInformerFactory interface.
// It sets external ClientSet for admission plugins that need it
func (a *Webhook) SetExternalKubeClientSet(client clientset.Interface) {
a.namespaceMatcher.Client = client
}
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
func (a *Webhook) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
namespaceInformer := f.Core().V1().Namespaces()
a.namespaceMatcher.NamespaceLister = namespaceInformer.Lister()
a.hookSource = a.sourceFactory(f)
a.SetReadyFunc(func() bool {
return namespaceInformer.Informer().HasSynced() && a.hookSource.HasSynced()
})
}
// ValidateInitialization implements the InitializationValidator interface.
func (a *Webhook) ValidateInitialization() error {
if a.hookSource == nil {
return fmt.Errorf("kubernetes client is not properly setup")
}
if err := a.namespaceMatcher.Validate(); err != nil {
return fmt.Errorf("namespaceMatcher is not properly setup: %v", err)
}
if err := a.clientManager.Validate(); err != nil {
return fmt.Errorf("clientManager is not properly setup: %v", err)
}
if err := a.convertor.Validate(); err != nil {
return fmt.Errorf("convertor is not properly setup: %v", err)
}
return nil
}
// ShouldCallHook makes a decision on whether to call the webhook or not by the attribute.
func (a *Webhook) ShouldCallHook(h *v1beta1.Webhook, attr admission.Attributes) (bool, *apierrors.StatusError) {
var matches bool
for _, r := range h.Rules {
m := rules.Matcher{Rule: r, Attr: attr}
if m.Matches() {
matches = true
break
}
}
if !matches {
return false, nil
}
return a.namespaceMatcher.MatchNamespaceSelector(h, attr)
}
// Dispatch is called by the downstream Validate or Admit methods.
func (a *Webhook) Dispatch(attr admission.Attributes) error {
if rules.IsWebhookConfigurationResource(attr) {
return nil
}
if !a.WaitForReady() {
return admission.NewForbidden(attr, fmt.Errorf("not yet ready to handle request"))
}
hooks := a.hookSource.Webhooks()
ctx := context.TODO()
var relevantHooks []*v1beta1.Webhook
for i := range hooks {
call, err := a.ShouldCallHook(&hooks[i], attr)
if err != nil {
return err
}
if call {
relevantHooks = append(relevantHooks, &hooks[i])
}
}
if len(relevantHooks) == 0 {
// no matching hooks
return nil
}
// convert the object to the external version before sending it to the webhook
versionedAttr := VersionedAttributes{
Attributes: attr,
}
if oldObj := attr.GetOldObject(); oldObj != nil {
out, err := a.convertor.ConvertToGVK(oldObj, attr.GetKind())
if err != nil {
return apierrors.NewInternalError(err)
}
versionedAttr.VersionedOldObject = out
}
if obj := attr.GetObject(); obj != nil {
out, err := a.convertor.ConvertToGVK(obj, attr.GetKind())
if err != nil {
return apierrors.NewInternalError(err)
}
versionedAttr.VersionedObject = out
}
return a.dispatcher.Dispatch(ctx, &versionedAttr, relevantHooks)
}
......@@ -3,8 +3,9 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"admission.go",
"dispatcher.go",
"doc.go",
"plugin.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
visibility = ["//visibility:public"],
......@@ -19,36 +20,35 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/configuration:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/metrics:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
srcs = [
"dispatcher_test.go",
"plugin_test.go",
],
embed = [":go_default_library"],
deps = [
"//vendor/k8s.io/api/admission/v1beta1:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/example:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library",
],
)
......
/*
Copyright 2018 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 mutating delegates admission checks to dynamically configured
// mutating webhooks.
package mutating
import (
"context"
"fmt"
"time"
jsonpatch "github.com/evanphx/json-patch"
"github.com/golang/glog"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/api/admissionregistration/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
)
type mutatingDispatcher struct {
cm *config.ClientManager
plugin *Plugin
}
func newMutatingDispatcher(p *Plugin) func(cm *config.ClientManager) generic.Dispatcher {
return func(cm *config.ClientManager) generic.Dispatcher {
return &mutatingDispatcher{cm, p}
}
}
var _ generic.Dispatcher = &mutatingDispatcher{}
func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr *generic.VersionedAttributes, relevantHooks []*v1beta1.Webhook) error {
for _, hook := range relevantHooks {
t := time.Now()
err := a.callAttrMutatingHook(ctx, hook, attr)
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, attr.Attributes, "admit", hook.Name)
if err == nil {
continue
}
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
if callErr, ok := err.(*webhookerrors.ErrCallingWebhook); ok {
if ignoreClientCallFailures {
glog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr)
continue
}
glog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err)
}
return apierrors.NewInternalError(err)
}
// convert attr.VersionedObject to the internal version in the underlying admission.Attributes
return a.plugin.scheme.Convert(attr.VersionedObject, attr.Attributes.GetObject(), nil)
}
// note that callAttrMutatingHook updates attr
func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta1.Webhook, attr *generic.VersionedAttributes) error {
// Make the webhook request
request := request.CreateAdmissionReview(attr)
client, err := a.cm.HookClient(h)
if err != nil {
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
response := &admissionv1beta1.AdmissionReview{}
if err := client.Post().Context(ctx).Body(&request).Do().Into(response); err != nil {
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
if response.Response == nil {
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
}
if !response.Response.Allowed {
return webhookerrors.ToStatusErr(h.Name, response.Response.Result)
}
patchJS := response.Response.Patch
if len(patchJS) == 0 {
return nil
}
patchObj, err := jsonpatch.DecodePatch(patchJS)
if err != nil {
return apierrors.NewInternalError(err)
}
objJS, err := runtime.Encode(a.plugin.jsonSerializer, attr.VersionedObject)
if err != nil {
return apierrors.NewInternalError(err)
}
patchedJS, err := patchObj.Apply(objJS)
if err != nil {
return apierrors.NewInternalError(err)
}
// TODO: if we have multiple mutating webhooks, we can remember the json
// instead of encoding and decoding for each one.
if _, _, err := a.plugin.jsonSerializer.Decode(patchedJS, nil, attr.VersionedObject); err != nil {
return apierrors.NewInternalError(err)
}
a.plugin.scheme.Default(attr.VersionedObject)
return nil
}
/*
Copyright 2018 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 mutating
import (
"context"
"reflect"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/apiserver/pkg/apis/example"
examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
example2v1 "k8s.io/apiserver/pkg/apis/example2/v1"
)
var sampleCRD = unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.k8s.io/v1",
"kind": "Flunder",
"data": map[string]interface{}{
"Key": "Value",
},
},
}
func TestDispatch(t *testing.T) {
scheme := runtime.NewScheme()
example.AddToScheme(scheme)
examplev1.AddToScheme(scheme)
example2v1.AddToScheme(scheme)
tests := []struct {
name string
in runtime.Object
out runtime.Object
expectedObj runtime.Object
}{
{
name: "convert example/v1#Pod to example#Pod",
in: &examplev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Labels: map[string]string{
"key": "value",
},
},
Spec: examplev1.PodSpec{
RestartPolicy: examplev1.RestartPolicy("never"),
},
},
out: &example.Pod{},
expectedObj: &example.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example.PodSpec{
RestartPolicy: example.RestartPolicy("never"),
},
},
},
{
name: "convert example2/v1#replicaset to example#replicaset",
in: &example2v1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "rs1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example2v1.ReplicaSetSpec{
Replicas: func() *int32 { var i int32; i = 1; return &i }(),
},
},
out: &example.ReplicaSet{},
expectedObj: &example.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "rs1",
Labels: map[string]string{
"key": "value",
},
},
Spec: example.ReplicaSetSpec{
Replicas: 1,
},
},
},
{
name: "no conversion if the object is the same",
in: &sampleCRD,
out: &sampleCRD,
expectedObj: &sampleCRD,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
a := &mutatingDispatcher{
plugin: &Plugin{
scheme: scheme,
},
}
attr := generic.VersionedAttributes{
Attributes: admission.NewAttributesRecord(test.out, nil, schema.GroupVersionKind{}, "", "", schema.GroupVersionResource{}, "", admission.Operation(""), nil),
VersionedOldObject: nil,
VersionedObject: test.in,
}
if err := a.Dispatch(context.TODO(), &attr, nil); err != nil {
t.Fatalf("%s: unexpected error: %v", test.name, err)
}
if !reflect.DeepEqual(attr.Attributes.GetObject(), test.expectedObj) {
t.Errorf("\nexpected:\n%#v\ngot:\n %#v\n", test.expectedObj, test.out)
}
})
}
}
/*
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 mutating
import (
"fmt"
"io"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/configuration"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
const (
// Name of admission plug-in
PluginName = "MutatingAdmissionWebhook"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
plugin, err := NewMutatingWebhook(configFile)
if err != nil {
return nil, err
}
return plugin, nil
})
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*generic.Webhook
scheme *runtime.Scheme
jsonSerializer *json.Serializer
}
var _ admission.MutationInterface = &Plugin{}
// NewMutatingWebhook returns a generic admission webhook plugin.
func NewMutatingWebhook(configFile io.Reader) (*Plugin, error) {
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
p := &Plugin{}
var err error
p.Webhook, err = generic.NewWebhook(handler, configFile, configuration.NewMutatingWebhookConfigurationManager, newMutatingDispatcher(p))
if err != nil {
return nil, err
}
return p, nil
}
// SetScheme sets a serializer(NegotiatedSerializer) which is derived from the scheme
func (a *Plugin) SetScheme(scheme *runtime.Scheme) {
a.Webhook.SetScheme(scheme)
if scheme != nil {
a.scheme = scheme
a.jsonSerializer = json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
}
}
// ValidateInitialization implements the InitializationValidator interface.
func (a *Plugin) ValidateInitialization() error {
if err := a.Webhook.ValidateInitialization(); err != nil {
return err
}
if a.scheme == nil {
return fmt.Errorf("scheme is not properly setup")
}
if a.jsonSerializer == nil {
return fmt.Errorf("jsonSerializer is not properly setup")
}
return nil
}
// Admit makes an admission decision based on the request attributes.
func (a *Plugin) Admit(attr admission.Attributes) error {
return a.Webhook.Dispatch(attr)
}
/*
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 mutating
import (
"net/url"
"strings"
"testing"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing"
)
// TestAdmit tests that MutatingWebhook#Admit works as expected
func TestAdmit(t *testing.T) {
scheme := runtime.NewScheme()
v1beta1.AddToScheme(scheme)
corev1.AddToScheme(scheme)
testServer := webhooktesting.NewTestServer(t)
testServer.StartTLS()
defer testServer.Close()
serverURL, err := url.ParseRequestURI(testServer.URL)
if err != nil {
t.Fatalf("this should never happen? %v", err)
}
stopCh := make(chan struct{})
defer close(stopCh)
for _, tt := range webhooktesting.NewTestCases(serverURL) {
wh, err := NewMutatingWebhook(nil)
if err != nil {
t.Errorf("%s: failed to create mutating webhook: %v", tt.Name, err)
continue
}
ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh)
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
wh.SetScheme(scheme)
wh.SetExternalKubeClientSet(client)
wh.SetExternalKubeInformerFactory(informer)
informer.Start(stopCh)
informer.WaitForCacheSync(stopCh)
if err = wh.ValidateInitialization(); err != nil {
t.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
continue
}
err = wh.Admit(webhooktesting.NewAttribute(ns))
if tt.ExpectAllow != (err == nil) {
t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
}
// ErrWebhookRejected is not an error for our purposes
if tt.ErrorContains != "" {
if err == nil || !strings.Contains(err.Error(), tt.ErrorContains) {
t.Errorf("%s: expected an error saying %q, but got: %v", tt.Name, tt.ErrorContains, err)
}
}
if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
t.Errorf("%s: expected a StatusError, got %T", tt.Name, err)
}
}
}
// TestAdmitCachedClient tests that MutatingWebhook#Admit should cache restClient
func TestAdmitCachedClient(t *testing.T) {
scheme := runtime.NewScheme()
v1beta1.AddToScheme(scheme)
corev1.AddToScheme(scheme)
testServer := webhooktesting.NewTestServer(t)
testServer.StartTLS()
defer testServer.Close()
serverURL, err := url.ParseRequestURI(testServer.URL)
if err != nil {
t.Fatalf("this should never happen? %v", err)
}
stopCh := make(chan struct{})
defer close(stopCh)
wh, err := NewMutatingWebhook(nil)
if err != nil {
t.Fatalf("Failed to create mutating webhook: %v", err)
}
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
wh.SetScheme(scheme)
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, true, stopCh)
// override the webhook source. The client cache will stay the same.
cacheMisses := new(int32)
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(cacheMisses)))
wh.SetExternalKubeClientSet(client)
wh.SetExternalKubeInformerFactory(informer)
informer.Start(stopCh)
informer.WaitForCacheSync(stopCh)
if err = wh.ValidateInitialization(); err != nil {
t.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
continue
}
err = wh.Admit(webhooktesting.NewAttribute(ns))
if tt.ExpectAllow != (err == nil) {
t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
}
if tt.ExpectCacheMiss && *cacheMisses == 0 {
t.Errorf("%s: expected cache miss, but got no AuthenticationInfoResolver call", tt.Name)
}
if !tt.ExpectCacheMiss && *cacheMisses > 0 {
t.Errorf("%s: expected client to be cached, but got %d AuthenticationInfoResolver calls", tt.Name, *cacheMisses)
}
}
}
......@@ -14,7 +14,7 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/uuid:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library",
],
)
......
......@@ -22,11 +22,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
// CreateAdmissionReview creates an AdmissionReview for the provided admission.Attributes
func CreateAdmissionReview(attr admission.Attributes) admissionv1beta1.AdmissionReview {
func CreateAdmissionReview(attr *generic.VersionedAttributes) admissionv1beta1.AdmissionReview {
gvk := attr.GetKind()
gvr := attr.GetResource()
aUserInfo := attr.GetUserInfo()
......@@ -61,10 +61,10 @@ func CreateAdmissionReview(attr admission.Attributes) admissionv1beta1.Admission
Operation: admissionv1beta1.Operation(attr.GetOperation()),
UserInfo: userInfo,
Object: runtime.RawExtension{
Object: attr.GetObject(),
Object: attr.VersionedObject,
},
OldObject: runtime.RawExtension{
Object: attr.GetOldObject(),
Object: attr.VersionedOldObject,
},
},
}
......
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"authentication_info_resolver.go",
"service_resolver.go",
"testcase.go",
"webhook_server.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/testing",
visibility = ["//visibility:public"],
deps = [
"//vendor/k8s.io/api/admission/v1beta1:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
/*
Copyright 2018 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 testing
import (
"sync/atomic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
"k8s.io/client-go/rest"
)
// Wrapper turns an AuthenticationInfoResolver into a AuthenticationInfoResolverWrapper that unconditionally
// returns the given AuthenticationInfoResolver.
func Wrapper(r config.AuthenticationInfoResolver) func(config.AuthenticationInfoResolver) config.AuthenticationInfoResolver {
return func(config.AuthenticationInfoResolver) config.AuthenticationInfoResolver {
return r
}
}
// NewAuthenticationInfoResolver creates a fake AuthenticationInfoResolver that counts cache misses on
// every call to its methods.
func NewAuthenticationInfoResolver(cacheMisses *int32) config.AuthenticationInfoResolver {
return &authenticationInfoResolver{
restConfig: &rest.Config{
TLSClientConfig: rest.TLSClientConfig{
CAData: testcerts.CACert,
CertData: testcerts.ClientCert,
KeyData: testcerts.ClientKey,
},
},
cacheMisses: cacheMisses,
}
}
type authenticationInfoResolver struct {
restConfig *rest.Config
cacheMisses *int32
}
func (a *authenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
atomic.AddInt32(a.cacheMisses, 1)
return a.restConfig, nil
}
func (a *authenticationInfoResolver) ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error) {
atomic.AddInt32(a.cacheMisses, 1)
return a.restConfig, nil
}
/*
Copyright 2018 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 testing
import (
"fmt"
"net/url"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
)
type serviceResolver struct {
base url.URL
}
// NewServiceResolver returns a static service resolve that return the given URL or
// an error for the failResolve namespace.
func NewServiceResolver(base url.URL) config.ServiceResolver {
return &serviceResolver{base}
}
func (f serviceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) {
if namespace == "failResolve" {
return nil, fmt.Errorf("couldn't resolve service location")
}
u := f.base
return &u, nil
}
/*
Copyright 2018 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 testing
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"k8s.io/api/admission/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
)
// NewTestServer returns a webhook test HTTPS server with fixed webhook test certs.
func NewTestServer(t *testing.T) *httptest.Server {
// Create the test webhook server
sCert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey)
if err != nil {
t.Fatal(err)
}
rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(testcerts.CACert)
testServer := httptest.NewUnstartedServer(http.HandlerFunc(webhookHandler))
testServer.TLS = &tls.Config{
Certificates: []tls.Certificate{sCert},
ClientCAs: rootCAs,
ClientAuth: tls.RequireAndVerifyClientCert,
}
return testServer
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
fmt.Printf("got req: %v\n", r.URL.Path)
switch r.URL.Path {
case "/internalErr":
http.Error(w, "webhook internal server error", http.StatusInternalServerError)
return
case "/invalidReq":
w.WriteHeader(http.StatusSwitchingProtocols)
w.Write([]byte("webhook invalid request"))
return
case "/invalidResp":
w.Header().Set("Content-Type", "application/json")
w.Write([]byte("webhook invalid response"))
case "/disallow":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
Response: &v1beta1.AdmissionResponse{
Allowed: false,
},
})
case "/disallowReason":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
Response: &v1beta1.AdmissionResponse{
Allowed: false,
Result: &metav1.Status{
Message: "you shall not pass",
},
},
})
case "/allow":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
Response: &v1beta1.AdmissionResponse{
Allowed: true,
},
})
case "/nilResponse":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{})
default:
http.NotFound(w, r)
}
}
......@@ -3,8 +3,9 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"admission.go",
"dispatcher.go",
"doc.go",
"plugin.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/validating",
visibility = ["//visibility:public"],
......@@ -13,40 +14,27 @@ go_library(
"//vendor/k8s.io/api/admission/v1beta1:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/configuration:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/initializer:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/metrics:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/rules:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/versioned:go_default_library",
"//vendor/k8s.io/client-go/informers:go_default_library",
"//vendor/k8s.io/client-go/kubernetes:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["admission_test.go"],
srcs = ["plugin_test.go"],
embed = [":go_default_library"],
deps = [
"//vendor/k8s.io/api/admission/v1beta1:go_default_library",
"//vendor/k8s.io/api/admissionregistration/v1beta1:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/config:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts:go_default_library",
"//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:go_default_library",
],
)
......
/*
Copyright 2018 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 validating
import (
"context"
"fmt"
"sync"
"time"
"github.com/golang/glog"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/api/admissionregistration/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
)
type validatingDispatcher struct {
cm *config.ClientManager
}
func newValidatingDispatcher(cm *config.ClientManager) generic.Dispatcher {
return &validatingDispatcher{cm}
}
var _ generic.Dispatcher = &validatingDispatcher{}
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr *generic.VersionedAttributes, relevantHooks []*v1beta1.Webhook) error {
wg := sync.WaitGroup{}
errCh := make(chan error, len(relevantHooks))
wg.Add(len(relevantHooks))
for i := range relevantHooks {
go func(hook *v1beta1.Webhook) {
defer wg.Done()
t := time.Now()
err := d.callHook(ctx, hook, attr)
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, attr.Attributes, "validating", hook.Name)
if err == nil {
return
}
ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1beta1.Ignore
if callErr, ok := err.(*webhookerrors.ErrCallingWebhook); ok {
if ignoreClientCallFailures {
glog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
utilruntime.HandleError(callErr)
return
}
glog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err)
errCh <- apierrors.NewInternalError(err)
return
}
glog.Warningf("rejected by webhook %q: %#v", hook.Name, err)
errCh <- err
}(relevantHooks[i])
}
wg.Wait()
close(errCh)
var errs []error
for e := range errCh {
errs = append(errs, e)
}
if len(errs) == 0 {
return nil
}
if len(errs) > 1 {
for i := 1; i < len(errs); i++ {
// TODO: merge status errors; until then, just return the first one.
utilruntime.HandleError(errs[i])
}
}
return errs[0]
}
func (d *validatingDispatcher) callHook(ctx context.Context, h *v1beta1.Webhook, attr *generic.VersionedAttributes) error {
// Make the webhook request
request := request.CreateAdmissionReview(attr)
client, err := d.cm.HookClient(h)
if err != nil {
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
response := &admissionv1beta1.AdmissionReview{}
if err := client.Post().Context(ctx).Body(&request).Do().Into(response); err != nil {
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: err}
}
if response.Response == nil {
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
}
if response.Response.Allowed {
return nil
}
return webhookerrors.ToStatusErr(h.Name, response.Response.Result)
}
/*
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 validating
import (
"io"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/configuration"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
const (
// Name of admission plug-in
PluginName = "ValidatingAdmissionWebhook"
)
// Register registers a plugin
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
plugin, err := NewValidatingAdmissionWebhook(configFile)
if err != nil {
return nil, err
}
return plugin, nil
})
}
// Plugin is an implementation of admission.Interface.
type Plugin struct {
*generic.Webhook
}
var _ admission.ValidationInterface = &Plugin{}
// NewValidatingAdmissionWebhook returns a generic admission webhook plugin.
func NewValidatingAdmissionWebhook(configFile io.Reader) (*Plugin, error) {
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
webhook, err := generic.NewWebhook(handler, configFile, configuration.NewValidatingWebhookConfigurationManager, newValidatingDispatcher)
if err != nil {
return nil, err
}
return &Plugin{webhook}, nil
}
// Validate makes an admission decision based on the request attributes.
func (a *Plugin) Validate(attr admission.Attributes) error {
return a.Webhook.Dispatch(attr)
}
/*
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 validating
import (
"net/url"
"strings"
"testing"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing"
)
// TestValidate tests that ValidatingWebhook#Validate works as expected
func TestValidate(t *testing.T) {
scheme := runtime.NewScheme()
v1beta1.AddToScheme(scheme)
corev1.AddToScheme(scheme)
testServer := webhooktesting.NewTestServer(t)
testServer.StartTLS()
defer testServer.Close()
serverURL, err := url.ParseRequestURI(testServer.URL)
if err != nil {
t.Fatalf("this should never happen? %v", err)
}
stopCh := make(chan struct{})
defer close(stopCh)
for _, tt := range webhooktesting.NewTestCases(serverURL) {
// TODO: re-enable all tests
if !strings.Contains(tt.Name, "no match") {
continue
}
wh, err := NewValidatingAdmissionWebhook(nil)
if err != nil {
t.Errorf("%s: failed to create validating webhook: %v", tt.Name, err)
continue
}
ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh)
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(new(int32))))
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
wh.SetScheme(scheme)
wh.SetExternalKubeClientSet(client)
wh.SetExternalKubeInformerFactory(informer)
informer.Start(stopCh)
informer.WaitForCacheSync(stopCh)
if err = wh.ValidateInitialization(); err != nil {
t.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
continue
}
err = wh.Validate(webhooktesting.NewAttribute(ns))
if tt.ExpectAllow != (err == nil) {
t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
}
// ErrWebhookRejected is not an error for our purposes
if tt.ErrorContains != "" {
if err == nil || !strings.Contains(err.Error(), tt.ErrorContains) {
t.Errorf("%s: expected an error saying %q, but got %v", tt.Name, tt.ErrorContains, err)
}
}
if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
t.Errorf("%s: expected a StatusError, got %T", tt.Name, err)
}
}
}
// TestValidateCachedClient tests that ValidatingWebhook#Validate should cache restClient
func TestValidateCachedClient(t *testing.T) {
scheme := runtime.NewScheme()
v1beta1.AddToScheme(scheme)
corev1.AddToScheme(scheme)
testServer := webhooktesting.NewTestServer(t)
testServer.StartTLS()
defer testServer.Close()
serverURL, err := url.ParseRequestURI(testServer.URL)
if err != nil {
t.Fatalf("this should never happen? %v", err)
}
stopCh := make(chan struct{})
defer close(stopCh)
wh, err := NewValidatingAdmissionWebhook(nil)
if err != nil {
t.Fatalf("Failed to create validating webhook: %v", err)
}
wh.SetServiceResolver(webhooktesting.NewServiceResolver(*serverURL))
wh.SetScheme(scheme)
for _, tt := range webhooktesting.NewCachedClientTestcases(serverURL) {
ns := "webhook-test"
client, informer := webhooktesting.NewFakeDataSource(ns, tt.Webhooks, false, stopCh)
// override the webhook source. The client cache will stay the same.
cacheMisses := new(int32)
wh.SetAuthenticationInfoResolverWrapper(webhooktesting.Wrapper(webhooktesting.NewAuthenticationInfoResolver(cacheMisses)))
wh.SetExternalKubeClientSet(client)
wh.SetExternalKubeInformerFactory(informer)
informer.Start(stopCh)
informer.WaitForCacheSync(stopCh)
if err = wh.ValidateInitialization(); err != nil {
t.Errorf("%s: failed to validate initialization: %v", tt.Name, err)
continue
}
err = wh.Validate(webhooktesting.NewAttribute(ns))
if tt.ExpectAllow != (err == nil) {
t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
}
if tt.ExpectCacheMiss && *cacheMisses == 0 {
t.Errorf("%s: expected cache miss, but got no AuthenticationInfoResolver call", tt.Name)
}
if !tt.ExpectCacheMiss && *cacheMisses > 0 {
t.Errorf("%s: expected client to be cached, but got %d AuthenticationInfoResolver calls", tt.Name, *cacheMisses)
}
}
}
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"attributes.go",
"conversion.go",
"doc.go",
],
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned",
visibility = ["//visibility:public"],
deps = [
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/admission:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["conversion_test.go"],
embed = [":go_default_library"],
deps = [
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/example:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
"//vendor/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
......
/*
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 versioned provides tools for making sure the objects sent to a
// webhook are in a version the webhook understands.
package versioned // import "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned"
......@@ -172,16 +172,16 @@ func (ps *Plugins) InitPlugin(name string, config io.Reader, pluginInitializer P
plugin, found, err := ps.getPlugin(name, config)
if err != nil {
return nil, fmt.Errorf("Couldn't init admission plugin %q: %v", name, err)
return nil, fmt.Errorf("couldn't init admission plugin %q: %v", name, err)
}
if !found {
return nil, fmt.Errorf("Unknown admission plugin: %s", name)
return nil, fmt.Errorf("unknown admission plugin: %s", name)
}
pluginInitializer.Initialize(plugin)
// ensure that plugins have been properly initialized
if err := ValidateInitialization(plugin); err != nil {
return nil, err
return nil, fmt.Errorf("failed to initialize admission plugin %q: %v", name, err)
}
return plugin, nil
......
......@@ -847,6 +847,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/generic",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......@@ -867,10 +871,6 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/apis/apiserver",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......
......@@ -819,6 +819,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/generic",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......@@ -839,10 +843,6 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/admission/plugin/webhook/versioned",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/apis/apiserver",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment