mirror of https://github.com/k3s-io/k3s
admission/webhook: refactor to webhook = generic-webhook + source + dispatcher
- unify test cases - remove broken VersionedAttributes override abstraction This overriding had no effect. The versioned.Attributes were never used as admission.Attributes.Better make the versioned objects explicit than hiding them under a wrong abstraction. - remove wrapping of scheme.Convert - internalize conversion packagepull/8/head
parent
44ffcdd9c5
commit
72f8a369d0
|
@ -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) updateConfiguration() {
|
||||
func (m *mutatingWebhookConfigurationManager) HasSynced() bool {
|
||||
return m.hasSynced()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (v *ValidatingWebhookConfigurationManager) updateConfiguration() {
|
||||
// HasSynced returns true if the shared informers have synced.
|
||||
func (v *validatingWebhookConfigurationManager) HasSynced() bool {
|
||||
return v.hasSynced()
|
||||
}
|
||||
|
||||
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 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)
|
||||
|
||||
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{},
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
// Source can list dynamic webhook plugins.
|
||||
type Source interface {
|
||||
Webhooks() []v1beta1.Webhook
|
||||
HasSynced() bool
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
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)
|
||||
}
|
|
@ -1,311 +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 mutating delegates admission checks to dynamically configured
|
||||
// mutating webhooks.
|
||||
package mutating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"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"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/configuration"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
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/namespace"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/rules"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/versioned"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// WebhookSource can list dynamic webhook plugins.
|
||||
type WebhookSource interface {
|
||||
Webhooks() *v1beta1.MutatingWebhookConfiguration
|
||||
}
|
||||
|
||||
// NewMutatingWebhook returns a generic admission webhook plugin.
|
||||
func NewMutatingWebhook(configFile io.Reader) (*MutatingWebhook, 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 &MutatingWebhook{
|
||||
Handler: admission.NewHandler(
|
||||
admission.Connect,
|
||||
admission.Create,
|
||||
admission.Delete,
|
||||
admission.Update,
|
||||
),
|
||||
clientManager: cm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ admission.MutationInterface = &MutatingWebhook{}
|
||||
|
||||
// MutatingWebhook is an implementation of admission.Interface.
|
||||
type MutatingWebhook struct {
|
||||
*admission.Handler
|
||||
hookSource WebhookSource
|
||||
namespaceMatcher namespace.Matcher
|
||||
clientManager config.ClientManager
|
||||
convertor versioned.Convertor
|
||||
defaulter runtime.ObjectDefaulter
|
||||
jsonSerializer runtime.Serializer
|
||||
}
|
||||
|
||||
var (
|
||||
_ = genericadmissioninit.WantsExternalKubeClientSet(&MutatingWebhook{})
|
||||
)
|
||||
|
||||
// TODO find a better way wire this, but keep this pull small for now.
|
||||
func (a *MutatingWebhook) 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 *MutatingWebhook) SetServiceResolver(sr config.ServiceResolver) {
|
||||
a.clientManager.SetServiceResolver(sr)
|
||||
}
|
||||
|
||||
// SetScheme sets a serializer(NegotiatedSerializer) which is derived from the scheme
|
||||
func (a *MutatingWebhook) SetScheme(scheme *runtime.Scheme) {
|
||||
if scheme != nil {
|
||||
a.convertor.Scheme = scheme
|
||||
a.defaulter = scheme
|
||||
a.jsonSerializer = json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
|
||||
}
|
||||
}
|
||||
|
||||
// WantsExternalKubeClientSet defines a function which sets external ClientSet for admission plugins that need it
|
||||
func (a *MutatingWebhook) SetExternalKubeClientSet(client clientset.Interface) {
|
||||
a.namespaceMatcher.Client = client
|
||||
}
|
||||
|
||||
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
|
||||
func (a *MutatingWebhook) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
namespaceInformer := f.Core().V1().Namespaces()
|
||||
a.namespaceMatcher.NamespaceLister = namespaceInformer.Lister()
|
||||
mutatingWebhookConfigurationsInformer := f.Admissionregistration().V1beta1().MutatingWebhookConfigurations()
|
||||
a.hookSource = configuration.NewMutatingWebhookConfigurationManager(mutatingWebhookConfigurationsInformer)
|
||||
a.SetReadyFunc(func() bool {
|
||||
return namespaceInformer.Informer().HasSynced() && mutatingWebhookConfigurationsInformer.Informer().HasSynced()
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateInitialization implements the InitializationValidator interface.
|
||||
func (a *MutatingWebhook) ValidateInitialization() error {
|
||||
if a.hookSource == nil {
|
||||
return fmt.Errorf("MutatingWebhook admission plugin requires a Kubernetes client to be provided")
|
||||
}
|
||||
if a.jsonSerializer == nil {
|
||||
return fmt.Errorf("MutatingWebhook admission plugin's jsonSerializer is not properly setup")
|
||||
}
|
||||
if err := a.namespaceMatcher.Validate(); err != nil {
|
||||
return fmt.Errorf("MutatingWebhook.namespaceMatcher is not properly setup: %v", err)
|
||||
}
|
||||
if err := a.clientManager.Validate(); err != nil {
|
||||
return fmt.Errorf("MutatingWebhook.clientManager is not properly setup: %v", err)
|
||||
}
|
||||
if err := a.convertor.Validate(); err != nil {
|
||||
return fmt.Errorf("MutatingWebhook.convertor is not properly setup: %v", err)
|
||||
}
|
||||
if a.defaulter == nil {
|
||||
return fmt.Errorf("MutatingWebhook.defaulter is not properly setup")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *MutatingWebhook) loadConfiguration(attr admission.Attributes) *v1beta1.MutatingWebhookConfiguration {
|
||||
hookConfig := a.hookSource.Webhooks()
|
||||
return hookConfig
|
||||
}
|
||||
|
||||
// Admit makes an admission decision based on the request attributes.
|
||||
func (a *MutatingWebhook) Admit(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"))
|
||||
}
|
||||
|
||||
hookConfig := a.loadConfiguration(attr)
|
||||
hooks := hookConfig.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 := versioned.Attributes{
|
||||
Attributes: attr,
|
||||
}
|
||||
if oldObj := attr.GetOldObject(); oldObj != nil {
|
||||
out, err := a.convertor.ConvertToGVK(oldObj, attr.GetKind())
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
versionedAttr.OldObject = out
|
||||
}
|
||||
if obj := attr.GetObject(); obj != nil {
|
||||
out, err := a.convertor.ConvertToGVK(obj, attr.GetKind())
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
versionedAttr.Object = out
|
||||
}
|
||||
|
||||
for _, hook := range relevantHooks {
|
||||
t := time.Now()
|
||||
err := a.callAttrMutatingHook(ctx, hook, versionedAttr)
|
||||
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, attr, "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.Object to the internal version
|
||||
return a.convertor.Convert(versionedAttr.Object, attr.GetObject())
|
||||
}
|
||||
|
||||
// TODO: factor into a common place along with the validating webhook version.
|
||||
func (a *MutatingWebhook) 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)
|
||||
}
|
||||
|
||||
// note that callAttrMutatingHook updates attr
|
||||
func (a *MutatingWebhook) callAttrMutatingHook(ctx context.Context, h *v1beta1.Webhook, attr versioned.Attributes) error {
|
||||
// Make the webhook request
|
||||
request := request.CreateAdmissionReview(attr)
|
||||
client, err := a.clientManager.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.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.jsonSerializer, attr.Object)
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
patchedJS, err := patchObj.Apply(objJS)
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
if _, _, err := a.jsonSerializer.Decode(patchedJS, nil, attr.Object); err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
a.defaulter.Default(attr.Object)
|
||||
return nil
|
||||
}
|
|
@ -1,653 +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 mutating
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
registrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type fakeHookSource struct {
|
||||
hooks []registrationv1beta1.Webhook
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeHookSource) Webhooks() *registrationv1beta1.MutatingWebhookConfiguration {
|
||||
if f.err != nil {
|
||||
return nil
|
||||
}
|
||||
for i, h := range f.hooks {
|
||||
if h.NamespaceSelector == nil {
|
||||
f.hooks[i].NamespaceSelector = &metav1.LabelSelector{}
|
||||
}
|
||||
}
|
||||
return ®istrationv1beta1.MutatingWebhookConfiguration{Webhooks: f.hooks}
|
||||
}
|
||||
|
||||
func (f *fakeHookSource) Run(stopCh <-chan struct{}) {}
|
||||
|
||||
type fakeServiceResolver struct {
|
||||
base url.URL
|
||||
}
|
||||
|
||||
func (f fakeServiceResolver) 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
|
||||
}
|
||||
|
||||
type fakeNamespaceLister struct {
|
||||
namespaces map[string]*corev1.Namespace
|
||||
}
|
||||
|
||||
func (f fakeNamespaceLister) List(selector labels.Selector) (ret []*corev1.Namespace, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f fakeNamespaceLister) Get(name string) (*corev1.Namespace, error) {
|
||||
ns, ok := f.namespaces[name]
|
||||
if ok {
|
||||
return ns, nil
|
||||
}
|
||||
return nil, errors.NewNotFound(corev1.Resource("namespaces"), name)
|
||||
}
|
||||
|
||||
// ccfgSVC returns a client config using the service reference mechanism.
|
||||
func ccfgSVC(urlPath string) registrationv1beta1.WebhookClientConfig {
|
||||
return registrationv1beta1.WebhookClientConfig{
|
||||
Service: ®istrationv1beta1.ServiceReference{
|
||||
Name: "webhook-test",
|
||||
Namespace: "default",
|
||||
Path: &urlPath,
|
||||
},
|
||||
CABundle: testcerts.CACert,
|
||||
}
|
||||
}
|
||||
|
||||
type urlConfigGenerator struct {
|
||||
baseURL *url.URL
|
||||
}
|
||||
|
||||
// ccfgURL returns a client config using the URL mechanism.
|
||||
func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1beta1.WebhookClientConfig {
|
||||
u2 := *c.baseURL
|
||||
u2.Path = urlPath
|
||||
urlString := u2.String()
|
||||
return registrationv1beta1.WebhookClientConfig{
|
||||
URL: &urlString,
|
||||
CABundle: testcerts.CACert,
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdmit tests that MutatingWebhook#Admit works as expected
|
||||
func TestAdmit(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(scheme)
|
||||
corev1.AddToScheme(scheme)
|
||||
|
||||
testServer := newTestServer(t)
|
||||
testServer.StartTLS()
|
||||
defer testServer.Close()
|
||||
serverURL, err := url.ParseRequestURI(testServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("this should never happen? %v", err)
|
||||
}
|
||||
wh, err := NewMutatingWebhook(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cm, err := config.NewClientManager()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create client manager: %v", err)
|
||||
}
|
||||
cm.SetAuthenticationInfoResolver(newFakeAuthenticationInfoResolver(new(int32)))
|
||||
cm.SetServiceResolver(fakeServiceResolver{base: *serverURL})
|
||||
wh.clientManager = cm
|
||||
wh.SetScheme(scheme)
|
||||
if err = wh.clientManager.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
namespace := "webhook-test"
|
||||
wh.namespaceMatcher.NamespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{
|
||||
namespace: {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"runlevel": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set up a test object for the call
|
||||
kind := corev1.SchemeGroupVersion.WithKind("Pod")
|
||||
name := "my-pod"
|
||||
object := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"pod.name": name,
|
||||
},
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
}
|
||||
oldObject := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
operation := admission.Update
|
||||
resource := corev1.Resource("pods").WithVersion("v1")
|
||||
subResource := ""
|
||||
userInfo := user.DefaultInfo{
|
||||
Name: "webhook-test",
|
||||
UID: "webhook-test",
|
||||
}
|
||||
|
||||
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
|
||||
|
||||
type test struct {
|
||||
hookSource fakeHookSource
|
||||
path string
|
||||
expectAllow bool
|
||||
errorContains string
|
||||
}
|
||||
|
||||
matchEverythingRules := []registrationv1beta1.RuleWithOperations{{
|
||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.OperationAll},
|
||||
Rule: registrationv1beta1.Rule{
|
||||
APIGroups: []string{"*"},
|
||||
APIVersions: []string{"*"},
|
||||
Resources: []string{"*/*"},
|
||||
},
|
||||
}}
|
||||
|
||||
policyFail := registrationv1beta1.Fail
|
||||
policyIgnore := registrationv1beta1.Ignore
|
||||
|
||||
table := map[string]test{
|
||||
"no match": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "nomatch",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: []registrationv1beta1.RuleWithOperations{{
|
||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.Create},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & allow": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "allow",
|
||||
ClientConfig: ccfgSVC("allow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & disallow": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "without explanation",
|
||||
},
|
||||
"match & disallow ii": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallowReason",
|
||||
ClientConfig: ccfgSVC("disallowReason"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "you shall not pass",
|
||||
},
|
||||
"match & disallow & but allowed because namespaceSelector exempt the namespace": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||
Key: "runlevel",
|
||||
Values: []string{"1"},
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & disallow & but allowed because namespaceSelector exempt the namespace ii": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||
Key: "runlevel",
|
||||
Values: []string{"0"},
|
||||
Operator: metav1.LabelSelectorOpNotIn,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & fail (but allow because fail open)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyIgnore,
|
||||
}, {
|
||||
Name: "internalErr B",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyIgnore,
|
||||
}, {
|
||||
Name: "internalErr C",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & fail (but disallow because fail closed on nil)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
}, {
|
||||
Name: "internalErr B",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
}, {
|
||||
Name: "internalErr C",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
expectAllow: false,
|
||||
},
|
||||
"match & fail (but fail because fail closed)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyFail,
|
||||
}, {
|
||||
Name: "internalErr B",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyFail,
|
||||
}, {
|
||||
Name: "internalErr C",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyFail,
|
||||
}},
|
||||
},
|
||||
expectAllow: false,
|
||||
},
|
||||
"match & allow (url)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "allow",
|
||||
ClientConfig: ccfgURL("allow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & disallow (url)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgURL("disallow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "without explanation",
|
||||
},
|
||||
// No need to test everything with the url case, since only the
|
||||
// connection is different.
|
||||
}
|
||||
|
||||
for name, tt := range table {
|
||||
if !strings.Contains(name, "no match") {
|
||||
continue
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
wh.hookSource = &tt.hookSource
|
||||
err = wh.Admit(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, name, resource, subResource, operation, &userInfo))
|
||||
if tt.expectAllow != (err == nil) {
|
||||
t.Errorf("expected allowed=%v, but got err=%v", 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(" expected an error saying %q, but got %v", tt.errorContains, err)
|
||||
}
|
||||
}
|
||||
if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
|
||||
t.Errorf("%s: expected a StatusError, got %T", 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 := newTestServer(t)
|
||||
testServer.StartTLS()
|
||||
defer testServer.Close()
|
||||
serverURL, err := url.ParseRequestURI(testServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("this should never happen? %v", err)
|
||||
}
|
||||
wh, err := NewMutatingWebhook(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cm, err := config.NewClientManager()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create client manager: %v", err)
|
||||
}
|
||||
cm.SetServiceResolver(fakeServiceResolver{base: *serverURL})
|
||||
wh.clientManager = cm
|
||||
wh.SetScheme(scheme)
|
||||
namespace := "webhook-test"
|
||||
wh.namespaceMatcher.NamespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{
|
||||
namespace: {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"runlevel": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set up a test object for the call
|
||||
kind := corev1.SchemeGroupVersion.WithKind("Pod")
|
||||
name := "my-pod"
|
||||
object := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"pod.name": name,
|
||||
},
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
}
|
||||
oldObject := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
operation := admission.Update
|
||||
resource := corev1.Resource("pods").WithVersion("v1")
|
||||
subResource := ""
|
||||
userInfo := user.DefaultInfo{
|
||||
Name: "webhook-test",
|
||||
UID: "webhook-test",
|
||||
}
|
||||
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
hookSource fakeHookSource
|
||||
expectAllow bool
|
||||
expectCache bool
|
||||
}
|
||||
|
||||
policyIgnore := registrationv1beta1.Ignore
|
||||
cases := []test{
|
||||
{
|
||||
name: "cache 1",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache1",
|
||||
ClientConfig: ccfgSVC("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: true,
|
||||
},
|
||||
{
|
||||
name: "cache 2",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache2",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: true,
|
||||
},
|
||||
{
|
||||
name: "cache 3",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache3",
|
||||
ClientConfig: ccfgSVC("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: false,
|
||||
},
|
||||
{
|
||||
name: "cache 4",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache4",
|
||||
ClientConfig: ccfgURL("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: true,
|
||||
},
|
||||
{
|
||||
name: "cache 5",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache5",
|
||||
ClientConfig: ccfgURL("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
wh.hookSource = &testcase.hookSource
|
||||
authInfoResolverCount := new(int32)
|
||||
r := newFakeAuthenticationInfoResolver(authInfoResolverCount)
|
||||
wh.clientManager.SetAuthenticationInfoResolver(r)
|
||||
if err = wh.clientManager.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = wh.Admit(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, testcase.name, resource, subResource, operation, &userInfo))
|
||||
if testcase.expectAllow != (err == nil) {
|
||||
t.Errorf("expected allowed=%v, but got err=%v", testcase.expectAllow, err)
|
||||
}
|
||||
|
||||
if testcase.expectCache && *authInfoResolverCount != 1 {
|
||||
t.Errorf("expected cacheclient, but got none")
|
||||
}
|
||||
|
||||
if !testcase.expectCache && *authInfoResolverCount != 0 {
|
||||
t.Errorf("expected not cacheclient, but got cache")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func newFakeAuthenticationInfoResolver(count *int32) *fakeAuthenticationInfoResolver {
|
||||
return &fakeAuthenticationInfoResolver{
|
||||
restConfig: &rest.Config{
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
CAData: testcerts.CACert,
|
||||
CertData: testcerts.ClientCert,
|
||||
KeyData: testcerts.ClientKey,
|
||||
},
|
||||
},
|
||||
cachedCount: count,
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthenticationInfoResolver struct {
|
||||
restConfig *rest.Config
|
||||
cachedCount *int32
|
||||
}
|
||||
|
||||
func (c *fakeAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
|
||||
atomic.AddInt32(c.cachedCount, 1)
|
||||
return c.restConfig, nil
|
||||
}
|
||||
|
||||
func (c *fakeAuthenticationInfoResolver) ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error) {
|
||||
atomic.AddInt32(c.cachedCount, 1)
|
||||
return c.restConfig, nil
|
||||
}
|
||||
|
||||
func newMatchEverythingRules() []registrationv1beta1.RuleWithOperations {
|
||||
return []registrationv1beta1.RuleWithOperations{{
|
||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.OperationAll},
|
||||
Rule: registrationv1beta1.Rule{
|
||||
APIGroups: []string{"*"},
|
||||
APIVersions: []string{"*"},
|
||||
Resources: []string{"*/*"},
|
||||
},
|
||||
}}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
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"
|
||||
"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.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
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"authentication_info_resolver.go",
|
||||
"service_resolver.go",
|
||||
"source.go",
|
||||
"testcase.go",
|
||||
"webhook_server.go",
|
||||
],
|
||||
importpath = "k8s.io/apiserver/pkg/admission/plugin/webhook/fake",
|
||||
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/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/generic:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/admission/plugin/webhook/namespace: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/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"],
|
||||
)
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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
|
||||
}
|
|
@ -0,0 +1,426 @@
|
|||
/*
|
||||
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 (
|
||||
"net/url"
|
||||
|
||||
registrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
fakeclientset "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
var matchEverythingRules = []registrationv1beta1.RuleWithOperations{{
|
||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.OperationAll},
|
||||
Rule: registrationv1beta1.Rule{
|
||||
APIGroups: []string{"*"},
|
||||
APIVersions: []string{"*"},
|
||||
Resources: []string{"*/*"},
|
||||
},
|
||||
}}
|
||||
|
||||
// NewFakeDataSource returns a mock client and informer returning the given webhooks.
|
||||
func NewFakeDataSource(name string, webhooks []registrationv1beta1.Webhook, mutating bool, stopCh <-chan struct{}) (clientset kubernetes.Interface, factory informers.SharedInformerFactory) {
|
||||
var objs = []runtime.Object{
|
||||
&corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Labels: map[string]string{
|
||||
"runlevel": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if mutating {
|
||||
objs = append(objs, ®istrationv1beta1.MutatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-webhooks",
|
||||
},
|
||||
Webhooks: webhooks,
|
||||
})
|
||||
} else {
|
||||
objs = append(objs, ®istrationv1beta1.ValidatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-webhooks",
|
||||
},
|
||||
Webhooks: webhooks,
|
||||
})
|
||||
}
|
||||
|
||||
client := fakeclientset.NewSimpleClientset(objs...)
|
||||
informerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||
|
||||
return client, informerFactory
|
||||
}
|
||||
|
||||
// NewAttribute returns static admission Attributes for testing.
|
||||
func NewAttribute(namespace string) admission.Attributes {
|
||||
// Set up a test object for the call
|
||||
kind := corev1.SchemeGroupVersion.WithKind("Pod")
|
||||
name := "my-pod"
|
||||
object := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"pod.name": name,
|
||||
},
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
}
|
||||
oldObject := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
operation := admission.Update
|
||||
resource := corev1.Resource("pods").WithVersion("v1")
|
||||
subResource := ""
|
||||
userInfo := user.DefaultInfo{
|
||||
Name: "webhook-test",
|
||||
UID: "webhook-test",
|
||||
}
|
||||
|
||||
return admission.NewAttributesRecord(&object, &oldObject, kind, namespace, name, resource, subResource, operation, &userInfo)
|
||||
}
|
||||
|
||||
type urlConfigGenerator struct {
|
||||
baseURL *url.URL
|
||||
}
|
||||
|
||||
func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1beta1.WebhookClientConfig {
|
||||
u2 := *c.baseURL
|
||||
u2.Path = urlPath
|
||||
urlString := u2.String()
|
||||
return registrationv1beta1.WebhookClientConfig{
|
||||
URL: &urlString,
|
||||
CABundle: testcerts.CACert,
|
||||
}
|
||||
}
|
||||
|
||||
// Test is a webhook test case.
|
||||
type Test struct {
|
||||
Name string
|
||||
Webhooks []registrationv1beta1.Webhook
|
||||
Path string
|
||||
ExpectAllow bool
|
||||
ErrorContains string
|
||||
}
|
||||
|
||||
// NewTestCases returns test cases with a given base url.
|
||||
func NewTestCases(url *url.URL) []Test {
|
||||
policyFail := registrationv1beta1.Fail
|
||||
policyIgnore := registrationv1beta1.Ignore
|
||||
ccfgURL := urlConfigGenerator{url}.ccfgURL
|
||||
|
||||
return []Test{
|
||||
{
|
||||
Name: "no match",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "nomatch",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: []registrationv1beta1.RuleWithOperations{{
|
||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.Create},
|
||||
}},
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
},
|
||||
{
|
||||
Name: "match & allow",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "allow",
|
||||
ClientConfig: ccfgSVC("allow"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
},
|
||||
{
|
||||
Name: "match & disallow",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
}},
|
||||
ErrorContains: "without explanation",
|
||||
},
|
||||
{
|
||||
Name: "match & disallow ii",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallowReason",
|
||||
ClientConfig: ccfgSVC("disallowReason"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
}},
|
||||
|
||||
ErrorContains: "you shall not pass",
|
||||
},
|
||||
{
|
||||
Name: "match & disallow & but allowed because namespaceSelector exempt the ns",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||
Key: "runlevel",
|
||||
Values: []string{"1"},
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
|
||||
ExpectAllow: true,
|
||||
},
|
||||
{
|
||||
Name: "match & disallow & but allowed because namespaceSelector exempt the ns ii",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||
Key: "runlevel",
|
||||
Values: []string{"0"},
|
||||
Operator: metav1.LabelSelectorOpNotIn,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
},
|
||||
{
|
||||
Name: "match & fail (but allow because fail open)",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyIgnore,
|
||||
}, {
|
||||
Name: "internalErr B",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyIgnore,
|
||||
}, {
|
||||
Name: "internalErr C",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
|
||||
ExpectAllow: true,
|
||||
},
|
||||
{
|
||||
Name: "match & fail (but disallow because fail close on nil FailurePolicy)",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
Rules: matchEverythingRules,
|
||||
}, {
|
||||
Name: "internalErr B",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
Rules: matchEverythingRules,
|
||||
}, {
|
||||
Name: "internalErr C",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
ExpectAllow: false,
|
||||
},
|
||||
{
|
||||
Name: "match & fail (but fail because fail closed)",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyFail,
|
||||
}, {
|
||||
Name: "internalErr B",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyFail,
|
||||
}, {
|
||||
Name: "internalErr C",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyFail,
|
||||
}},
|
||||
ExpectAllow: false,
|
||||
},
|
||||
{
|
||||
Name: "match & allow (url)",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "allow",
|
||||
ClientConfig: ccfgURL("allow"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
},
|
||||
{
|
||||
Name: "match & disallow (url)",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgURL("disallow"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
}},
|
||||
ErrorContains: "without explanation",
|
||||
}, {
|
||||
Name: "absent response and fail open",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "nilResponse",
|
||||
ClientConfig: ccfgURL("nilResponse"),
|
||||
FailurePolicy: &policyIgnore,
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
},
|
||||
{
|
||||
Name: "absent response and fail closed",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "nilResponse",
|
||||
ClientConfig: ccfgURL("nilResponse"),
|
||||
FailurePolicy: &policyFail,
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
}},
|
||||
ErrorContains: "Webhook response was absent",
|
||||
},
|
||||
// No need to test everything with the url case, since only the
|
||||
// connection is different.
|
||||
}
|
||||
}
|
||||
|
||||
// CachedTest is a test case for the client manager.
|
||||
type CachedTest struct {
|
||||
Name string
|
||||
Webhooks []registrationv1beta1.Webhook
|
||||
ExpectAllow bool
|
||||
ExpectCacheMiss bool
|
||||
}
|
||||
|
||||
// NewCachedClientTestcases returns a set of client manager test cases.
|
||||
func NewCachedClientTestcases(url *url.URL) []CachedTest {
|
||||
policyIgnore := registrationv1beta1.Ignore
|
||||
ccfgURL := urlConfigGenerator{url}.ccfgURL
|
||||
|
||||
return []CachedTest{
|
||||
{
|
||||
Name: "uncached: service webhook, path 'allow'",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache1",
|
||||
ClientConfig: ccfgSVC("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
ExpectCacheMiss: true,
|
||||
},
|
||||
{
|
||||
Name: "uncached: service webhook, path 'internalErr'",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache2",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
ExpectCacheMiss: true,
|
||||
},
|
||||
{
|
||||
Name: "cached: service webhook, path 'allow'",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache3",
|
||||
ClientConfig: ccfgSVC("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
ExpectCacheMiss: false,
|
||||
},
|
||||
{
|
||||
Name: "uncached: url webhook, path 'allow'",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache4",
|
||||
ClientConfig: ccfgURL("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
ExpectCacheMiss: true,
|
||||
},
|
||||
{
|
||||
Name: "cached: service webhook, path 'allow'",
|
||||
Webhooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache5",
|
||||
ClientConfig: ccfgURL("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
ExpectCacheMiss: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ccfgSVC returns a client config using the service reference mechanism.
|
||||
func ccfgSVC(urlPath string) registrationv1beta1.WebhookClientConfig {
|
||||
return registrationv1beta1.WebhookClientConfig{
|
||||
Service: ®istrationv1beta1.ServiceReference{
|
||||
Name: "webhook-test",
|
||||
Namespace: "default",
|
||||
Path: &urlPath,
|
||||
},
|
||||
CABundle: testcerts.CACert,
|
||||
}
|
||||
}
|
||||
|
||||
func newMatchEverythingRules() []registrationv1beta1.RuleWithOperations {
|
||||
return []registrationv1beta1.RuleWithOperations{{
|
||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.OperationAll},
|
||||
Rule: registrationv1beta1.Rule{
|
||||
APIGroups: []string{"*"},
|
||||
APIVersions: []string{"*"},
|
||||
Resources: []string{"*/*"},
|
||||
},
|
||||
}}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1,305 +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 validating delegates admission checks to dynamically configured
|
||||
// validating webhooks.
|
||||
package validating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/configuration"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
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/namespace"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/request"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/rules"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/versioned"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
// WebhookSource can list dynamic webhook plugins.
|
||||
type WebhookSource interface {
|
||||
Webhooks() *v1beta1.ValidatingWebhookConfiguration
|
||||
}
|
||||
|
||||
// NewValidatingAdmissionWebhook returns a generic admission webhook plugin.
|
||||
func NewValidatingAdmissionWebhook(configFile io.Reader) (*ValidatingAdmissionWebhook, 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 &ValidatingAdmissionWebhook{
|
||||
Handler: admission.NewHandler(
|
||||
admission.Connect,
|
||||
admission.Create,
|
||||
admission.Delete,
|
||||
admission.Update,
|
||||
),
|
||||
clientManager: cm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &ValidatingAdmissionWebhook{}
|
||||
|
||||
// ValidatingAdmissionWebhook is an implementation of admission.Interface.
|
||||
type ValidatingAdmissionWebhook struct {
|
||||
*admission.Handler
|
||||
hookSource WebhookSource
|
||||
namespaceMatcher namespace.Matcher
|
||||
clientManager config.ClientManager
|
||||
convertor versioned.Convertor
|
||||
}
|
||||
|
||||
var (
|
||||
_ = genericadmissioninit.WantsExternalKubeClientSet(&ValidatingAdmissionWebhook{})
|
||||
)
|
||||
|
||||
// TODO find a better way wire this, but keep this pull small for now.
|
||||
func (a *ValidatingAdmissionWebhook) 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 *ValidatingAdmissionWebhook) SetServiceResolver(sr config.ServiceResolver) {
|
||||
a.clientManager.SetServiceResolver(sr)
|
||||
}
|
||||
|
||||
// SetScheme sets a serializer(NegotiatedSerializer) which is derived from the scheme
|
||||
func (a *ValidatingAdmissionWebhook) SetScheme(scheme *runtime.Scheme) {
|
||||
if scheme != nil {
|
||||
a.convertor.Scheme = scheme
|
||||
}
|
||||
}
|
||||
|
||||
// WantsExternalKubeClientSet defines a function which sets external ClientSet for admission plugins that need it
|
||||
func (a *ValidatingAdmissionWebhook) SetExternalKubeClientSet(client clientset.Interface) {
|
||||
a.namespaceMatcher.Client = client
|
||||
}
|
||||
|
||||
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
|
||||
func (a *ValidatingAdmissionWebhook) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
namespaceInformer := f.Core().V1().Namespaces()
|
||||
a.namespaceMatcher.NamespaceLister = namespaceInformer.Lister()
|
||||
validatingWebhookConfigurationsInformer := f.Admissionregistration().V1beta1().ValidatingWebhookConfigurations()
|
||||
a.hookSource = configuration.NewValidatingWebhookConfigurationManager(validatingWebhookConfigurationsInformer)
|
||||
a.SetReadyFunc(func() bool {
|
||||
return namespaceInformer.Informer().HasSynced() && validatingWebhookConfigurationsInformer.Informer().HasSynced()
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateInitialization implements the InitializationValidator interface.
|
||||
func (a *ValidatingAdmissionWebhook) ValidateInitialization() error {
|
||||
if a.hookSource == nil {
|
||||
return fmt.Errorf("ValidatingAdmissionWebhook admission plugin requires a Kubernetes informer to be provided")
|
||||
}
|
||||
if err := a.namespaceMatcher.Validate(); err != nil {
|
||||
return fmt.Errorf("ValidatingAdmissionWebhook.namespaceMatcher is not properly setup: %v", err)
|
||||
}
|
||||
if err := a.clientManager.Validate(); err != nil {
|
||||
return fmt.Errorf("ValidatingAdmissionWebhook.clientManager is not properly setup: %v", err)
|
||||
}
|
||||
if err := a.convertor.Validate(); err != nil {
|
||||
return fmt.Errorf("ValidatingAdmissionWebhook.convertor is not properly setup: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ValidatingAdmissionWebhook) loadConfiguration(attr admission.Attributes) *v1beta1.ValidatingWebhookConfiguration {
|
||||
return a.hookSource.Webhooks()
|
||||
}
|
||||
|
||||
// Validate makes an admission decision based on the request attributes.
|
||||
func (a *ValidatingAdmissionWebhook) Validate(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"))
|
||||
}
|
||||
hookConfig := a.loadConfiguration(attr)
|
||||
hooks := hookConfig.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 := versioned.Attributes{
|
||||
Attributes: attr,
|
||||
}
|
||||
if oldObj := attr.GetOldObject(); oldObj != nil {
|
||||
out, err := a.convertor.ConvertToGVK(oldObj, attr.GetKind())
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
versionedAttr.OldObject = out
|
||||
}
|
||||
if obj := attr.GetObject(); obj != nil {
|
||||
out, err := a.convertor.ConvertToGVK(obj, attr.GetKind())
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
versionedAttr.Object = out
|
||||
}
|
||||
|
||||
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 := a.callHook(ctx, hook, versionedAttr)
|
||||
admissionmetrics.Metrics.ObserveWebhook(time.Since(t), err != nil, attr, "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]
|
||||
}
|
||||
|
||||
// TODO: factor into a common place along with the mutating webhook version.
|
||||
func (a *ValidatingAdmissionWebhook) 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)
|
||||
}
|
||||
|
||||
func (a *ValidatingAdmissionWebhook) callHook(ctx context.Context, h *v1beta1.Webhook, attr admission.Attributes) error {
|
||||
// Make the webhook request
|
||||
request := request.CreateAdmissionReview(attr)
|
||||
client, err := a.clientManager.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)
|
||||
}
|
|
@ -1,678 +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 validating
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
registrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type fakeHookSource struct {
|
||||
hooks []registrationv1beta1.Webhook
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeHookSource) Webhooks() *registrationv1beta1.ValidatingWebhookConfiguration {
|
||||
if f.err != nil {
|
||||
return nil
|
||||
}
|
||||
for i, h := range f.hooks {
|
||||
if h.NamespaceSelector == nil {
|
||||
f.hooks[i].NamespaceSelector = &metav1.LabelSelector{}
|
||||
}
|
||||
}
|
||||
return ®istrationv1beta1.ValidatingWebhookConfiguration{Webhooks: f.hooks}
|
||||
}
|
||||
|
||||
func (f *fakeHookSource) Run(stopCh <-chan struct{}) {}
|
||||
|
||||
type fakeServiceResolver struct {
|
||||
base url.URL
|
||||
}
|
||||
|
||||
func (f fakeServiceResolver) 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
|
||||
}
|
||||
|
||||
type fakeNamespaceLister struct {
|
||||
namespaces map[string]*corev1.Namespace
|
||||
}
|
||||
|
||||
func (f fakeNamespaceLister) List(selector labels.Selector) (ret []*corev1.Namespace, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f fakeNamespaceLister) Get(name string) (*corev1.Namespace, error) {
|
||||
ns, ok := f.namespaces[name]
|
||||
if ok {
|
||||
return ns, nil
|
||||
}
|
||||
return nil, errors.NewNotFound(corev1.Resource("namespaces"), name)
|
||||
}
|
||||
|
||||
// ccfgSVC returns a client config using the service reference mechanism.
|
||||
func ccfgSVC(urlPath string) registrationv1beta1.WebhookClientConfig {
|
||||
return registrationv1beta1.WebhookClientConfig{
|
||||
Service: ®istrationv1beta1.ServiceReference{
|
||||
Name: "webhook-test",
|
||||
Namespace: "default",
|
||||
Path: &urlPath,
|
||||
},
|
||||
CABundle: testcerts.CACert,
|
||||
}
|
||||
}
|
||||
|
||||
type urlConfigGenerator struct {
|
||||
baseURL *url.URL
|
||||
}
|
||||
|
||||
// ccfgURL returns a client config using the URL mechanism.
|
||||
func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1beta1.WebhookClientConfig {
|
||||
u2 := *c.baseURL
|
||||
u2.Path = urlPath
|
||||
urlString := u2.String()
|
||||
return registrationv1beta1.WebhookClientConfig{
|
||||
URL: &urlString,
|
||||
CABundle: testcerts.CACert,
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidate tests that ValidatingAdmissionWebhook#Validate works as expected
|
||||
func TestValidate(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(scheme)
|
||||
corev1.AddToScheme(scheme)
|
||||
|
||||
testServer := newTestServer(t)
|
||||
testServer.StartTLS()
|
||||
defer testServer.Close()
|
||||
serverURL, err := url.ParseRequestURI(testServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("this should never happen? %v", err)
|
||||
}
|
||||
wh, err := NewValidatingAdmissionWebhook(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cm, err := config.NewClientManager()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create client manager: %v", err)
|
||||
}
|
||||
cm.SetAuthenticationInfoResolver(newFakeAuthenticationInfoResolver(new(int32)))
|
||||
cm.SetServiceResolver(fakeServiceResolver{base: *serverURL})
|
||||
wh.clientManager = cm
|
||||
wh.SetScheme(scheme)
|
||||
if err = wh.clientManager.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
namespace := "webhook-test"
|
||||
wh.namespaceMatcher.NamespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{
|
||||
namespace: {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"runlevel": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set up a test object for the call
|
||||
kind := corev1.SchemeGroupVersion.WithKind("Pod")
|
||||
name := "my-pod"
|
||||
object := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"pod.name": name,
|
||||
},
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
}
|
||||
oldObject := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
operation := admission.Update
|
||||
resource := corev1.Resource("pods").WithVersion("v1")
|
||||
subResource := ""
|
||||
userInfo := user.DefaultInfo{
|
||||
Name: "webhook-test",
|
||||
UID: "webhook-test",
|
||||
}
|
||||
|
||||
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
|
||||
|
||||
type test struct {
|
||||
hookSource fakeHookSource
|
||||
path string
|
||||
expectAllow bool
|
||||
errorContains string
|
||||
}
|
||||
|
||||
matchEverythingRules := []registrationv1beta1.RuleWithOperations{{
|
||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.OperationAll},
|
||||
Rule: registrationv1beta1.Rule{
|
||||
APIGroups: []string{"*"},
|
||||
APIVersions: []string{"*"},
|
||||
Resources: []string{"*/*"},
|
||||
},
|
||||
}}
|
||||
|
||||
policyFail := registrationv1beta1.Fail
|
||||
policyIgnore := registrationv1beta1.Ignore
|
||||
|
||||
table := map[string]test{
|
||||
"no match": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "nomatch",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: []registrationv1beta1.RuleWithOperations{{
|
||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.Create},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & allow": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "allow",
|
||||
ClientConfig: ccfgSVC("allow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & disallow": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "without explanation",
|
||||
},
|
||||
"match & disallow ii": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallowReason",
|
||||
ClientConfig: ccfgSVC("disallowReason"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "you shall not pass",
|
||||
},
|
||||
"match & disallow & but allowed because namespaceSelector exempt the namespace": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||
Key: "runlevel",
|
||||
Values: []string{"1"},
|
||||
Operator: metav1.LabelSelectorOpIn,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & disallow & but allowed because namespaceSelector exempt the namespace ii": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgSVC("disallow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
NamespaceSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{{
|
||||
Key: "runlevel",
|
||||
Values: []string{"0"},
|
||||
Operator: metav1.LabelSelectorOpNotIn,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & fail (but allow because fail open)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyIgnore,
|
||||
}, {
|
||||
Name: "internalErr B",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyIgnore,
|
||||
}, {
|
||||
Name: "internalErr C",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & fail (but disallow because fail closed on nil)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
}, {
|
||||
Name: "internalErr B",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
}, {
|
||||
Name: "internalErr C",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
expectAllow: false,
|
||||
},
|
||||
"match & fail (but fail because fail closed)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "internalErr A",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyFail,
|
||||
}, {
|
||||
Name: "internalErr B",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyFail,
|
||||
}, {
|
||||
Name: "internalErr C",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: matchEverythingRules,
|
||||
FailurePolicy: &policyFail,
|
||||
}},
|
||||
},
|
||||
expectAllow: false,
|
||||
},
|
||||
"match & allow (url)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "allow",
|
||||
ClientConfig: ccfgURL("allow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"match & disallow (url)": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: ccfgURL("disallow"),
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "without explanation",
|
||||
},
|
||||
"absent response and fail open": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "nilResponse",
|
||||
ClientConfig: ccfgURL("nilResponse"),
|
||||
FailurePolicy: &policyIgnore,
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
},
|
||||
"absent response and fail closed": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "nilResponse",
|
||||
ClientConfig: ccfgURL("nilResponse"),
|
||||
FailurePolicy: &policyFail,
|
||||
Rules: matchEverythingRules,
|
||||
}},
|
||||
},
|
||||
errorContains: "Webhook response was absent",
|
||||
},
|
||||
// No need to test everything with the url case, since only the
|
||||
// connection is different.
|
||||
}
|
||||
|
||||
for name, tt := range table {
|
||||
if !strings.Contains(name, "no match") {
|
||||
continue
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
wh.hookSource = &tt.hookSource
|
||||
err = wh.Validate(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, name, resource, subResource, operation, &userInfo))
|
||||
if tt.expectAllow != (err == nil) {
|
||||
t.Errorf("expected allowed=%v, but got err=%v", 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(" expected an error saying %q, but got %v", tt.errorContains, err)
|
||||
}
|
||||
}
|
||||
if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
|
||||
t.Errorf("%s: expected a StatusError, got %T", name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateCachedClient tests that ValidatingAdmissionWebhook#Validate should cache restClient
|
||||
func TestValidateCachedClient(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(scheme)
|
||||
corev1.AddToScheme(scheme)
|
||||
|
||||
testServer := newTestServer(t)
|
||||
testServer.StartTLS()
|
||||
defer testServer.Close()
|
||||
serverURL, err := url.ParseRequestURI(testServer.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("this should never happen? %v", err)
|
||||
}
|
||||
wh, err := NewValidatingAdmissionWebhook(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cm, err := config.NewClientManager()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot create client manager: %v", err)
|
||||
}
|
||||
cm.SetServiceResolver(fakeServiceResolver{base: *serverURL})
|
||||
wh.clientManager = cm
|
||||
wh.SetScheme(scheme)
|
||||
namespace := "webhook-test"
|
||||
wh.namespaceMatcher.NamespaceLister = fakeNamespaceLister{map[string]*corev1.Namespace{
|
||||
namespace: {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"runlevel": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set up a test object for the call
|
||||
kind := corev1.SchemeGroupVersion.WithKind("Pod")
|
||||
name := "my-pod"
|
||||
object := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"pod.name": name,
|
||||
},
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
}
|
||||
oldObject := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
operation := admission.Update
|
||||
resource := corev1.Resource("pods").WithVersion("v1")
|
||||
subResource := ""
|
||||
userInfo := user.DefaultInfo{
|
||||
Name: "webhook-test",
|
||||
UID: "webhook-test",
|
||||
}
|
||||
ccfgURL := urlConfigGenerator{serverURL}.ccfgURL
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
hookSource fakeHookSource
|
||||
expectAllow bool
|
||||
expectCache bool
|
||||
}
|
||||
|
||||
policyIgnore := registrationv1beta1.Ignore
|
||||
cases := []test{
|
||||
{
|
||||
name: "cache 1",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache1",
|
||||
ClientConfig: ccfgSVC("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: true,
|
||||
},
|
||||
{
|
||||
name: "cache 2",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache2",
|
||||
ClientConfig: ccfgSVC("internalErr"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: true,
|
||||
},
|
||||
{
|
||||
name: "cache 3",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache3",
|
||||
ClientConfig: ccfgSVC("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: false,
|
||||
},
|
||||
{
|
||||
name: "cache 4",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache4",
|
||||
ClientConfig: ccfgURL("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: true,
|
||||
},
|
||||
{
|
||||
name: "cache 5",
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1beta1.Webhook{{
|
||||
Name: "cache5",
|
||||
ClientConfig: ccfgURL("allow"),
|
||||
Rules: newMatchEverythingRules(),
|
||||
FailurePolicy: &policyIgnore,
|
||||
}},
|
||||
},
|
||||
expectAllow: true,
|
||||
expectCache: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
wh.hookSource = &testcase.hookSource
|
||||
authInfoResolverCount := new(int32)
|
||||
r := newFakeAuthenticationInfoResolver(authInfoResolverCount)
|
||||
wh.clientManager.SetAuthenticationInfoResolver(r)
|
||||
if err = wh.clientManager.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = wh.Validate(admission.NewAttributesRecord(&object, &oldObject, kind, namespace, testcase.name, resource, subResource, operation, &userInfo))
|
||||
if testcase.expectAllow != (err == nil) {
|
||||
t.Errorf("expected allowed=%v, but got err=%v", testcase.expectAllow, err)
|
||||
}
|
||||
|
||||
if testcase.expectCache && *authInfoResolverCount != 1 {
|
||||
t.Errorf("expected cacheclient, but got none")
|
||||
}
|
||||
|
||||
if !testcase.expectCache && *authInfoResolverCount != 0 {
|
||||
t.Errorf("expected not cacheclient, but got cache")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 "/nilResposne":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func newFakeAuthenticationInfoResolver(count *int32) *fakeAuthenticationInfoResolver {
|
||||
return &fakeAuthenticationInfoResolver{
|
||||
restConfig: &rest.Config{
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
CAData: testcerts.CACert,
|
||||
CertData: testcerts.ClientCert,
|
||||
KeyData: testcerts.ClientKey,
|
||||
},
|
||||
},
|
||||
cachedCount: count,
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthenticationInfoResolver struct {
|
||||
restConfig *rest.Config
|
||||
cachedCount *int32
|
||||
}
|
||||
|
||||
func (c *fakeAuthenticationInfoResolver) ClientConfigFor(server string) (*rest.Config, error) {
|
||||
atomic.AddInt32(c.cachedCount, 1)
|
||||
return c.restConfig, nil
|
||||
}
|
||||
|
||||
func (c *fakeAuthenticationInfoResolver) ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error) {
|
||||
atomic.AddInt32(c.cachedCount, 1)
|
||||
return c.restConfig, nil
|
||||
}
|
||||
|
||||
func newMatchEverythingRules() []registrationv1beta1.RuleWithOperations {
|
||||
return []registrationv1beta1.RuleWithOperations{{
|
||||
Operations: []registrationv1beta1.OperationType{registrationv1beta1.OperationAll},
|
||||
Rule: registrationv1beta1.Rule{
|
||||
APIGroups: []string{"*"},
|
||||
APIVersions: []string{"*"},
|
||||
Resources: []string{"*/*"},
|
||||
},
|
||||
}}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,42 +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 versioned
|
||||
|
||||
import (
|
||||
"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
|
||||
}
|
||||
|
||||
// GetObject overrides the original GetObjects() and it returns the versioned
|
||||
// object.
|
||||
func (v Attributes) GetObject() runtime.Object {
|
||||
return v.Object
|
||||
}
|
||||
|
||||
// GetOldObject overrides the original GetOldObjects() and it returns the
|
||||
// versioned oldObject.
|
||||
func (v Attributes) GetOldObject() runtime.Object {
|
||||
return v.OldObject
|
||||
}
|
|
@ -1,19 +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 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
|
||||
|
|
Loading…
Reference in New Issue