mirror of https://github.com/k3s-io/k3s
add NamespaceSelector to the api
business logic in webhook plugin and unit test add a e2e test for namespace selectorpull/6/head
parent
7c8596a95f
commit
7006d224be
|
@ -191,6 +191,52 @@ type Webhook struct {
|
|||
// allowed values are Ignore or Fail. Defaults to Ignore.
|
||||
// +optional
|
||||
FailurePolicy *FailurePolicyType
|
||||
|
||||
// NamespaceSelector decides whether to run the webhook on an object based
|
||||
// on whether the namespace for that object matches the selector. If the
|
||||
// object itself is a namespace, the matching is performed on
|
||||
// object.metadata.labels. If the object is other cluster scoped resource,
|
||||
// it is not subjected to the webhook.
|
||||
//
|
||||
// For example, to run the webhook on any objects whose namespace is not
|
||||
// associated with "runlevel" of "0" or "1"; you will set the selector as
|
||||
// follows:
|
||||
// "namespaceSelector": {
|
||||
// "matchExpressions": [
|
||||
// {
|
||||
// "key": "runlevel",
|
||||
// "operator": "NotIn",
|
||||
// "values": [
|
||||
// "0",
|
||||
// "1"
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// If instead you want to only run the webhook on any objects whose
|
||||
// namespace is associated with the "environment" of "prod" or "staging";
|
||||
// you will set the selector as follows:
|
||||
// "namespaceSelector": {
|
||||
// "matchExpressions": [
|
||||
// {
|
||||
// "key": "environment",
|
||||
// "operator": "In",
|
||||
// "values": [
|
||||
// "prod",
|
||||
// "staging"
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// See
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
// for more examples of label selectors.
|
||||
//
|
||||
// Default to the empty LabelSelector, which matches everything.
|
||||
// +optional
|
||||
NamespaceSelector *metav1.LabelSelector
|
||||
}
|
||||
|
||||
// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make
|
||||
|
|
|
@ -18,6 +18,7 @@ package v1alpha1
|
|||
|
||||
import (
|
||||
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
|
@ -30,4 +31,8 @@ func SetDefaults_Webhook(obj *admissionregistrationv1alpha1.Webhook) {
|
|||
policy := admissionregistrationv1alpha1.Ignore
|
||||
obj.FailurePolicy = &policy
|
||||
}
|
||||
if obj.NamespaceSelector == nil {
|
||||
selector := metav1.LabelSelector{}
|
||||
obj.NamespaceSelector = &selector
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"strings"
|
||||
|
||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
@ -195,6 +196,10 @@ func validateWebhook(hook *admissionregistration.Webhook, fldPath *field.Path) f
|
|||
allErrors = append(allErrors, validateURLPath(fldPath.Child("clientConfig", "urlPath"), hook.ClientConfig.URLPath)...)
|
||||
}
|
||||
|
||||
if hook.NamespaceSelector != nil {
|
||||
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
|
||||
}
|
||||
|
||||
return allErrors
|
||||
}
|
||||
|
||||
|
|
|
@ -197,6 +197,52 @@ type Webhook struct {
|
|||
// allowed values are Ignore or Fail. Defaults to Ignore.
|
||||
// +optional
|
||||
FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"`
|
||||
|
||||
// NamespaceSelector decides whether to run the webhook on an object based
|
||||
// on whether the namespace for that object matches the selector. If the
|
||||
// object itself is a namespace, the matching is performed on
|
||||
// object.metadata.labels. If the object is other cluster scoped resource,
|
||||
// it is not subjected to the webhook.
|
||||
//
|
||||
// For example, to run the webhook on any objects whose namespace is not
|
||||
// associated with "runlevel" of "0" or "1"; you will set the selector as
|
||||
// follows:
|
||||
// "namespaceSelector": {
|
||||
// "matchExpressions": [
|
||||
// {
|
||||
// "key": "runlevel",
|
||||
// "operator": "NotIn",
|
||||
// "values": [
|
||||
// "0",
|
||||
// "1"
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// If instead you want to only run the webhook on any objects whose
|
||||
// namespace is associated with the "environment" of "prod" or "staging";
|
||||
// you will set the selector as follows:
|
||||
// "namespaceSelector": {
|
||||
// "matchExpressions": [
|
||||
// {
|
||||
// "key": "environment",
|
||||
// "operator": "In",
|
||||
// "values": [
|
||||
// "prod",
|
||||
// "staging"
|
||||
// ]
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
//
|
||||
// See
|
||||
// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||
// for more examples of label selectors.
|
||||
//
|
||||
// Default to the empty LabelSelector, which matches everything.
|
||||
// +optional
|
||||
NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty" protobuf:"bytes,5,opt,name=namespaceSelector"`
|
||||
}
|
||||
|
||||
// RuleWithOperations is a tuple of Operations and Resources. It is recommended to make
|
||||
|
|
|
@ -32,7 +32,9 @@ import (
|
|||
admissionv1alpha1 "k8s.io/api/admission/v1alpha1"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
|
@ -41,7 +43,9 @@ import (
|
|||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/configuration"
|
||||
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
|
@ -123,6 +127,8 @@ type GenericAdmissionWebhook struct {
|
|||
hookSource WebhookSource
|
||||
serviceResolver ServiceResolver
|
||||
negotiatedSerializer runtime.NegotiatedSerializer
|
||||
namespaceLister corelisters.NamespaceLister
|
||||
client clientset.Interface
|
||||
|
||||
authInfoResolver AuthenticationInfoResolver
|
||||
cache *lru.Cache
|
||||
|
@ -163,9 +169,17 @@ func (a *GenericAdmissionWebhook) SetScheme(scheme *runtime.Scheme) {
|
|||
|
||||
// WantsExternalKubeClientSet defines a function which sets external ClientSet for admission plugins that need it
|
||||
func (a *GenericAdmissionWebhook) SetExternalKubeClientSet(client clientset.Interface) {
|
||||
a.client = client
|
||||
a.hookSource = configuration.NewValidatingWebhookConfigurationManager(client.AdmissionregistrationV1alpha1().ValidatingWebhookConfigurations())
|
||||
}
|
||||
|
||||
// SetExternalKubeInformerFactory implements the WantsExternalKubeInformerFactory interface.
|
||||
func (a *GenericAdmissionWebhook) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
namespaceInformer := f.Core().V1().Namespaces()
|
||||
a.namespaceLister = namespaceInformer.Lister()
|
||||
a.SetReadyFunc(namespaceInformer.Informer().HasSynced)
|
||||
}
|
||||
|
||||
// ValidateInitialization implements the InitializationValidator interface.
|
||||
func (a *GenericAdmissionWebhook) ValidateInitialization() error {
|
||||
if a.hookSource == nil {
|
||||
|
@ -174,6 +188,9 @@ func (a *GenericAdmissionWebhook) ValidateInitialization() error {
|
|||
if a.negotiatedSerializer == nil {
|
||||
return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a runtime.Scheme to be provided to derive a serializer")
|
||||
}
|
||||
if a.namespaceLister == nil {
|
||||
return fmt.Errorf("the GenericAdmissionWebhook admission plugin requires a namespaceLister")
|
||||
}
|
||||
go a.hookSource.Run(wait.NeverStop)
|
||||
return nil
|
||||
}
|
||||
|
@ -255,7 +272,74 @@ func (a *GenericAdmissionWebhook) Admit(attr admission.Attributes) error {
|
|||
return errs[0]
|
||||
}
|
||||
|
||||
func (a *GenericAdmissionWebhook) getNamespaceLabels(attr admission.Attributes) (map[string]string, error) {
|
||||
// If the request itself is creating or updating a namespace, then get the
|
||||
// labels from attr.Object, because namespaceLister doesn't have the latest
|
||||
// namespace yet.
|
||||
//
|
||||
// However, if the request is deleting a namespace, then get the label from
|
||||
// the namespace in the namespaceLister, because a delete request is not
|
||||
// going to change the object, and attr.Object will be a DeleteOptions
|
||||
// rather than a namespace object.
|
||||
if attr.GetResource().Resource == "namespaces" &&
|
||||
len(attr.GetSubresource()) == 0 &&
|
||||
(attr.GetOperation() == admission.Create || attr.GetOperation() == admission.Update) {
|
||||
accessor, err := meta.Accessor(attr.GetObject())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accessor.GetLabels(), nil
|
||||
}
|
||||
|
||||
namespaceName := attr.GetNamespace()
|
||||
namespace, err := a.namespaceLister.Get(namespaceName)
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if apierrors.IsNotFound(err) {
|
||||
// in case of latency in our caches, make a call direct to storage to verify that it truly exists or not
|
||||
namespace, err = a.client.Core().Namespaces().Get(namespaceName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return namespace.Labels, nil
|
||||
}
|
||||
|
||||
// whether the request is exempted by the webhook because of the
|
||||
// namespaceSelector of the webhook.
|
||||
func (a *GenericAdmissionWebhook) exemptedByNamespaceSelector(h *v1alpha1.Webhook, attr admission.Attributes) (bool, error) {
|
||||
namespaceName := attr.GetNamespace()
|
||||
if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" {
|
||||
// If the request is about a cluster scoped resource, and it is not a
|
||||
// namespace, it is exempted from all webhooks for now.
|
||||
// TODO: figure out a way selective exempt cluster scoped resources.
|
||||
// Also update the comment in types.go
|
||||
return true, nil
|
||||
}
|
||||
namespaceLabels, err := a.getNamespaceLabels(attr)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return false, err
|
||||
}
|
||||
if err != nil {
|
||||
return false, apierrors.NewInternalError(err)
|
||||
}
|
||||
// TODO: adding an LRU cache to cache the translation
|
||||
selector, err := metav1.LabelSelectorAsSelector(h.NamespaceSelector)
|
||||
if err != nil {
|
||||
return false, apierrors.NewInternalError(err)
|
||||
}
|
||||
return !selector.Matches(labels.Set(namespaceLabels)), nil
|
||||
}
|
||||
|
||||
func (a *GenericAdmissionWebhook) callHook(ctx context.Context, h *v1alpha1.Webhook, attr admission.Attributes) error {
|
||||
excluded, err := a.exemptedByNamespaceSelector(h, attr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if excluded {
|
||||
return nil
|
||||
}
|
||||
matches := false
|
||||
for _, r := range h.Rules {
|
||||
m := RuleMatcher{Rule: r, Attr: attr}
|
||||
|
|
|
@ -24,16 +24,20 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/admission/v1alpha1"
|
||||
registrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
api "k8s.io/api/core/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
apierrors "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/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/rest"
|
||||
|
@ -48,6 +52,11 @@ func (f *fakeHookSource) Webhooks() (*registrationv1alpha1.ValidatingWebhookConf
|
|||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
for i, h := range f.hooks {
|
||||
if h.NamespaceSelector == nil {
|
||||
f.hooks[i].NamespaceSelector = &metav1.LabelSelector{}
|
||||
}
|
||||
}
|
||||
return ®istrationv1alpha1.ValidatingWebhookConfiguration{Webhooks: f.hooks}, nil
|
||||
}
|
||||
|
||||
|
@ -65,11 +74,26 @@ func (f fakeServiceResolver) ResolveEndpoint(namespace, name string) (*url.URL,
|
|||
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)
|
||||
}
|
||||
|
||||
// TestAdmit tests that GenericAdmissionWebhook#Admit works as expected
|
||||
func TestAdmit(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
v1alpha1.AddToScheme(scheme)
|
||||
api.AddToScheme(scheme)
|
||||
corev1.AddToScheme(scheme)
|
||||
|
||||
testServer := newTestServer(t)
|
||||
testServer.StartTLS()
|
||||
|
@ -85,12 +109,22 @@ func TestAdmit(t *testing.T) {
|
|||
wh.authInfoResolver = newFakeAuthenticationInfoResolver()
|
||||
wh.serviceResolver = fakeServiceResolver{base: *serverURL}
|
||||
wh.SetScheme(scheme)
|
||||
namespace := "webhook-test"
|
||||
wh.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 := api.SchemeGroupVersion.WithKind("Pod")
|
||||
kind := corev1.SchemeGroupVersion.WithKind("Pod")
|
||||
name := "my-pod"
|
||||
namespace := "webhook-test"
|
||||
object := api.Pod{
|
||||
object := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"pod.name": name,
|
||||
|
@ -103,11 +137,11 @@ func TestAdmit(t *testing.T) {
|
|||
Kind: "Pod",
|
||||
},
|
||||
}
|
||||
oldObject := api.Pod{
|
||||
oldObject := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
operation := admission.Update
|
||||
resource := api.Resource("pods").WithVersion("v1")
|
||||
resource := corev1.Resource("pods").WithVersion("v1")
|
||||
subResource := ""
|
||||
userInfo := user.DefaultInfo{
|
||||
Name: "webhook-test",
|
||||
|
@ -167,6 +201,40 @@ func TestAdmit(t *testing.T) {
|
|||
},
|
||||
errorContains: "you shall not pass",
|
||||
},
|
||||
"match & disallow & but allowed because namespaceSelector exempt the namespace": {
|
||||
hookSource: fakeHookSource{
|
||||
hooks: []registrationv1alpha1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: newFakeHookClientConfig("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: []registrationv1alpha1.Webhook{{
|
||||
Name: "disallow",
|
||||
ClientConfig: newFakeHookClientConfig("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: []registrationv1alpha1.Webhook{{
|
||||
|
@ -230,9 +298,11 @@ func TestAdmit(t *testing.T) {
|
|||
}
|
||||
|
||||
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)
|
||||
|
@ -254,7 +324,7 @@ func TestAdmit(t *testing.T) {
|
|||
func TestAdmitCachedClient(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
v1alpha1.AddToScheme(scheme)
|
||||
api.AddToScheme(scheme)
|
||||
corev1.AddToScheme(scheme)
|
||||
|
||||
testServer := newTestServer(t)
|
||||
testServer.StartTLS()
|
||||
|
@ -270,12 +340,22 @@ func TestAdmitCachedClient(t *testing.T) {
|
|||
wh.authInfoResolver = newFakeAuthenticationInfoResolver()
|
||||
wh.serviceResolver = fakeServiceResolver{base: *serverURL}
|
||||
wh.SetScheme(scheme)
|
||||
namespace := "webhook-test"
|
||||
wh.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 := api.SchemeGroupVersion.WithKind("Pod")
|
||||
kind := corev1.SchemeGroupVersion.WithKind("Pod")
|
||||
name := "my-pod"
|
||||
namespace := "webhook-test"
|
||||
object := api.Pod{
|
||||
object := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"pod.name": name,
|
||||
|
@ -288,11 +368,11 @@ func TestAdmitCachedClient(t *testing.T) {
|
|||
Kind: "Pod",
|
||||
},
|
||||
}
|
||||
oldObject := api.Pod{
|
||||
oldObject := corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
|
||||
}
|
||||
operation := admission.Update
|
||||
resource := api.Resource("pods").WithVersion("v1")
|
||||
resource := corev1.Resource("pods").WithVersion("v1")
|
||||
subResource := ""
|
||||
userInfo := user.DefaultInfo{
|
||||
Name: "webhook-test",
|
||||
|
@ -522,3 +602,89 @@ func newMatchEverythingRules() []registrationv1alpha1.RuleWithOperations {
|
|||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func TestGetNamespaceLabels(t *testing.T) {
|
||||
namespace1Labels := map[string]string{
|
||||
"runlevel": "1",
|
||||
}
|
||||
namespace1 := corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "1",
|
||||
Labels: namespace1Labels,
|
||||
},
|
||||
}
|
||||
namespace2Labels := map[string]string{
|
||||
"runlevel": "2",
|
||||
}
|
||||
namespace2 := corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "2",
|
||||
Labels: namespace2Labels,
|
||||
},
|
||||
}
|
||||
namespaceLister := fakeNamespaceLister{map[string]*corev1.Namespace{
|
||||
"1": &namespace1,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
attr admission.Attributes
|
||||
expectedLabels map[string]string
|
||||
}{
|
||||
{
|
||||
name: "request is for creating namespace, the labels should be from the object itself",
|
||||
attr: admission.NewAttributesRecord(&namespace2, nil, schema.GroupVersionKind{}, "", namespace2.Name, schema.GroupVersionResource{Resource: "namespaces"}, "", admission.Create, nil),
|
||||
expectedLabels: namespace2Labels,
|
||||
},
|
||||
{
|
||||
name: "request is for updating namespace, the labels should be from the new object",
|
||||
attr: admission.NewAttributesRecord(&namespace2, nil, schema.GroupVersionKind{}, namespace2.Name, namespace2.Name, schema.GroupVersionResource{Resource: "namespaces"}, "", admission.Update, nil),
|
||||
expectedLabels: namespace2Labels,
|
||||
},
|
||||
{
|
||||
name: "request is for deleting namespace, the labels should be from the cache",
|
||||
attr: admission.NewAttributesRecord(&namespace2, nil, schema.GroupVersionKind{}, namespace1.Name, namespace1.Name, schema.GroupVersionResource{Resource: "namespaces"}, "", admission.Delete, nil),
|
||||
expectedLabels: namespace1Labels,
|
||||
},
|
||||
{
|
||||
name: "request is for namespace/finalizer",
|
||||
attr: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, namespace1.Name, "mock-name", schema.GroupVersionResource{Resource: "namespaces"}, "finalizers", admission.Create, nil),
|
||||
expectedLabels: namespace1Labels,
|
||||
},
|
||||
{
|
||||
name: "request is for pod",
|
||||
attr: admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, namespace1.Name, "mock-name", schema.GroupVersionResource{Resource: "pods"}, "", admission.Create, nil),
|
||||
expectedLabels: namespace1Labels,
|
||||
},
|
||||
}
|
||||
wh, err := NewGenericAdmissionWebhook(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wh.namespaceLister = namespaceLister
|
||||
for _, tt := range tests {
|
||||
actualLabels, err := wh.getNamespaceLabels(tt.attr)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !reflect.DeepEqual(actualLabels, tt.expectedLabels) {
|
||||
t.Errorf("expected labels to be %#v, got %#v", tt.expectedLabels, actualLabels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExemptClusterScopedResource(t *testing.T) {
|
||||
hook := ®istrationv1alpha1.Webhook{
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
}
|
||||
attr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "mock-name", schema.GroupVersionResource{Version: "v1", Resource: "nodes"}, "", admission.Create, nil)
|
||||
g := GenericAdmissionWebhook{}
|
||||
exempted, err := g.exemptedByNamespaceSelector(hook, attr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exempted {
|
||||
t.Errorf("cluster scoped resources (but not a namespace) should be exempted from all webhooks")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
utilversion "k8s.io/kubernetes/pkg/util/version"
|
||||
"k8s.io/kubernetes/test/e2e/framework"
|
||||
|
||||
|
@ -36,11 +37,16 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
secretName = "sample-webhook-secret"
|
||||
deploymentName = "sample-webhook-deployment"
|
||||
serviceName = "e2e-test-webhook"
|
||||
roleBindingName = "webhook-auth-reader"
|
||||
webhookConfigName = "e2e-test-webhook-config"
|
||||
secretName = "sample-webhook-secret"
|
||||
deploymentName = "sample-webhook-deployment"
|
||||
serviceName = "e2e-test-webhook"
|
||||
roleBindingName = "webhook-auth-reader"
|
||||
webhookConfigName = "e2e-test-webhook-config"
|
||||
skipNamespaceLabelKey = "skip-webhook-admission"
|
||||
skipNamespaceLabelValue = "yes"
|
||||
skippedNamespaceName = "exempted-namesapce"
|
||||
disallowedPodName = "disallowed-pod"
|
||||
disallowedConfigMapName = "disallowed-configmap"
|
||||
)
|
||||
|
||||
var serverWebhookVersion = utilversion.MustParseSemantic("v1.8.0")
|
||||
|
@ -51,7 +57,7 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
|
|||
cleanWebhookTest(f)
|
||||
})
|
||||
|
||||
It("Should be able to deny pod creation", func() {
|
||||
It("Should be able to deny pod and configmap creation", func() {
|
||||
// Make sure the relevant provider supports admission webhook
|
||||
framework.SkipUnlessServerVersionGTE(serverWebhookVersion, f.ClientSet.Discovery())
|
||||
framework.SkipUnlessProviderIs("gce", "gke")
|
||||
|
@ -68,7 +74,7 @@ var _ = SIGDescribe("AdmissionWebhook", func() {
|
|||
// Note that in 1.9 we will have backwards incompatible change to
|
||||
// admission webhooks, so the image will be updated to 1.9 sometime in
|
||||
// the development 1.9 cycle.
|
||||
deployWebhookAndService(f, "gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v1", context)
|
||||
deployWebhookAndService(f, "gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v2", context)
|
||||
registerWebhook(f, context)
|
||||
testWebhook(f)
|
||||
})
|
||||
|
@ -223,7 +229,7 @@ func registerWebhook(f *framework.Framework, context *certContext) {
|
|||
},
|
||||
Webhooks: []v1alpha1.Webhook{
|
||||
{
|
||||
Name: "e2e-test-webhook.k8s.io",
|
||||
Name: "deny-unwanted-pod-container-name-and-label.k8s.io",
|
||||
Rules: []v1alpha1.RuleWithOperations{{
|
||||
Operations: []v1alpha1.OperationType{v1alpha1.Create},
|
||||
Rule: v1alpha1.Rule{
|
||||
|
@ -237,6 +243,36 @@ func registerWebhook(f *framework.Framework, context *certContext) {
|
|||
Namespace: namespace,
|
||||
Name: serviceName,
|
||||
},
|
||||
URLPath: "/pods",
|
||||
CABundle: context.signingCert,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "deny-unwanted-configmap-data.k8s.io",
|
||||
Rules: []v1alpha1.RuleWithOperations{{
|
||||
Operations: []v1alpha1.OperationType{v1alpha1.Create},
|
||||
Rule: v1alpha1.Rule{
|
||||
APIGroups: []string{""},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"configmaps"},
|
||||
},
|
||||
}},
|
||||
// The webhook skips the namespace that has label "skip-webhook-admission":"yes"
|
||||
NamespaceSelector: &metav1.LabelSelector{
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{
|
||||
Key: skipNamespaceLabelKey,
|
||||
Operator: metav1.LabelSelectorOpNotIn,
|
||||
Values: []string{skipNamespaceLabelValue},
|
||||
},
|
||||
},
|
||||
},
|
||||
ClientConfig: v1alpha1.WebhookClientConfig{
|
||||
Service: v1alpha1.ServiceReference{
|
||||
Namespace: namespace,
|
||||
Name: serviceName,
|
||||
},
|
||||
URLPath: "/configmaps",
|
||||
CABundle: context.signingCert,
|
||||
},
|
||||
},
|
||||
|
@ -262,12 +298,45 @@ func testWebhook(f *framework.Framework) {
|
|||
// TODO: Test if webhook can detect pod with non-compliant metadata.
|
||||
// Currently metadata is lost because webhook uses the external version of
|
||||
// the objects, and the apiserver sends the internal objects.
|
||||
|
||||
By("create a configmap that should be denied by the webhook")
|
||||
// Creating the configmap, the request should be rejected
|
||||
configmap := nonCompliantConfigMap(f)
|
||||
_, err = client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configmap)
|
||||
Expect(err).NotTo(BeNil())
|
||||
expectedErrMsg = "the configmap contains unwanted key and value"
|
||||
if !strings.Contains(err.Error(), expectedErrMsg) {
|
||||
framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
|
||||
}
|
||||
|
||||
By("create a namespace that bypass the webhook")
|
||||
err = wait.Poll(100*time.Millisecond, 30*time.Second, func() (bool, error) {
|
||||
_, err2 := client.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: skippedNamespaceName,
|
||||
Labels: map[string]string{
|
||||
skipNamespaceLabelKey: skipNamespaceLabelValue,
|
||||
},
|
||||
}})
|
||||
if err2 != nil {
|
||||
if strings.HasPrefix(err2.Error(), "object is being deleted:") {
|
||||
return false, nil
|
||||
}
|
||||
return false, err2
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
framework.ExpectNoError(err, "creating namespace %q", skippedNamespaceName)
|
||||
|
||||
By("create a configmap that violates the webhook policy but is in a whitelisted namespace")
|
||||
configmap = nonCompliantConfigMap(f)
|
||||
_, err = client.CoreV1().ConfigMaps(skippedNamespaceName).Create(configmap)
|
||||
Expect(err).To(BeNil())
|
||||
}
|
||||
|
||||
func nonCompliantPod(f *framework.Framework) *v1.Pod {
|
||||
return &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "disallowed-pod",
|
||||
Name: disallowedPodName,
|
||||
Labels: map[string]string{
|
||||
"webhook-e2e-test": "disallow",
|
||||
},
|
||||
|
@ -283,6 +352,17 @@ func nonCompliantPod(f *framework.Framework) *v1.Pod {
|
|||
}
|
||||
}
|
||||
|
||||
func nonCompliantConfigMap(f *framework.Framework) *v1.ConfigMap {
|
||||
return &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: disallowedConfigMapName,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"webhook-e2e-test": "webhook-disallow",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func cleanWebhookTest(f *framework.Framework) {
|
||||
client := f.ClientSet
|
||||
_ = client.AdmissionregistrationV1alpha1().ValidatingWebhookConfigurations().Delete(webhookConfigName, nil)
|
||||
|
@ -291,4 +371,6 @@ func cleanWebhookTest(f *framework.Framework) {
|
|||
_ = client.ExtensionsV1beta1().Deployments(namespaceName).Delete(deploymentName, nil)
|
||||
_ = client.CoreV1().Secrets(namespaceName).Delete(secretName, nil)
|
||||
_ = client.RbacV1beta1().RoleBindings("kube-system").Delete(roleBindingName, nil)
|
||||
_ = client.CoreV1().ConfigMaps(skippedNamespaceName).Delete(disallowedConfigMapName, nil)
|
||||
_ = client.CoreV1().Namespaces().Delete(skippedNamespaceName, nil)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,6 @@
|
|||
|
||||
build:
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webhook .
|
||||
docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v1 .
|
||||
docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v2 .
|
||||
push:
|
||||
gcloud docker --push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v1 .
|
||||
gcloud docker -- push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v2
|
||||
|
|
|
@ -44,7 +44,8 @@ func (c *Config) addFlags() {
|
|||
}
|
||||
|
||||
// only allow pods to pull images from specific registry.
|
||||
func admit(data []byte) *v1alpha1.AdmissionReviewStatus {
|
||||
func admitPods(data []byte) *v1alpha1.AdmissionReviewStatus {
|
||||
glog.V(2).Info("admitting pods")
|
||||
ar := v1alpha1.AdmissionReview{}
|
||||
if err := json.Unmarshal(data, &ar); err != nil {
|
||||
glog.Error(err)
|
||||
|
@ -86,7 +87,42 @@ func admit(data []byte) *v1alpha1.AdmissionReviewStatus {
|
|||
return &reviewStatus
|
||||
}
|
||||
|
||||
func serve(w http.ResponseWriter, r *http.Request) {
|
||||
// deny configmaps with specific key-value pair.
|
||||
func admitConfigMaps(data []byte) *v1alpha1.AdmissionReviewStatus {
|
||||
glog.V(2).Info("admitting configmaps")
|
||||
ar := v1alpha1.AdmissionReview{}
|
||||
if err := json.Unmarshal(data, &ar); err != nil {
|
||||
glog.Error(err)
|
||||
return nil
|
||||
}
|
||||
configMapResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}
|
||||
if ar.Spec.Resource != configMapResource {
|
||||
glog.Errorf("expect resource to be %s", configMapResource)
|
||||
return nil
|
||||
}
|
||||
|
||||
raw := ar.Spec.Object.Raw
|
||||
configmap := v1.ConfigMap{}
|
||||
if err := json.Unmarshal(raw, &configmap); err != nil {
|
||||
glog.Error(err)
|
||||
return nil
|
||||
}
|
||||
reviewStatus := v1alpha1.AdmissionReviewStatus{}
|
||||
reviewStatus.Allowed = true
|
||||
for k, v := range configmap.Data {
|
||||
if k == "webhook-e2e-test" && v == "webhook-disallow" {
|
||||
reviewStatus.Allowed = false
|
||||
reviewStatus.Result = &metav1.Status{
|
||||
Reason: "the configmap contains unwanted key and value",
|
||||
}
|
||||
}
|
||||
}
|
||||
return &reviewStatus
|
||||
}
|
||||
|
||||
type admitFunc func(data []byte) *v1alpha1.AdmissionReviewStatus
|
||||
|
||||
func serve(w http.ResponseWriter, r *http.Request, admit admitFunc) {
|
||||
var body []byte
|
||||
if r.Body != nil {
|
||||
if data, err := ioutil.ReadAll(r.Body); err == nil {
|
||||
|
@ -102,8 +138,10 @@ func serve(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
reviewStatus := admit(body)
|
||||
ar := v1alpha1.AdmissionReview{
|
||||
Status: *reviewStatus,
|
||||
|
||||
ar := v1alpha1.AdmissionReview{}
|
||||
if reviewStatus != nil {
|
||||
ar.Status = *reviewStatus
|
||||
}
|
||||
|
||||
resp, err := json.Marshal(ar)
|
||||
|
@ -115,12 +153,20 @@ func serve(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func servePods(w http.ResponseWriter, r *http.Request) {
|
||||
serve(w, r, admitPods)
|
||||
}
|
||||
func serveConfigmaps(w http.ResponseWriter, r *http.Request) {
|
||||
serve(w, r, admitConfigMaps)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var config Config
|
||||
config.addFlags()
|
||||
flag.Parse()
|
||||
|
||||
http.HandleFunc("/", serve)
|
||||
http.HandleFunc("/pods", servePods)
|
||||
http.HandleFunc("/configmaps", serveConfigmaps)
|
||||
clientset := getClient()
|
||||
server := &http.Server{
|
||||
Addr: ":443",
|
||||
|
|
Loading…
Reference in New Issue