support annotations for admission webhook

This commit is contained in:
Cao Shufeng 2018-07-31 13:25:53 +08:00
parent edc3e40dce
commit 0ebfc3e078
8 changed files with 126 additions and 26 deletions

View File

@ -51,6 +51,7 @@ go_test(
"//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example/v1:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library", "//staging/src/k8s.io/apiserver/pkg/apis/example2/v1:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library", "//vendor/github.com/stretchr/testify/require:go_default_library",
], ],
) )

View File

@ -102,6 +102,13 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *v1beta
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")} return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
} }
for k, v := range response.Response.AuditAnnotations {
key := h.Name + "/" + k
if err := attr.AddAnnotation(key, v); err != nil {
glog.Warningf("Failed to set admission audit annotation %s to %s for mutating webhook %s: %v", key, v, h.Name, err)
}
}
if !response.Response.Allowed { if !response.Response.Allowed {
return webhookerrors.ToStatusErr(h.Name, response.Response.Result) return webhookerrors.ToStatusErr(h.Name, response.Response.Result)
} }

View File

@ -22,6 +22,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"k8s.io/api/admission/v1beta1" "k8s.io/api/admission/v1beta1"
@ -102,6 +103,16 @@ func TestAdmit(t *testing.T) {
if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr { if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
t.Errorf("%s: expected a StatusError, got %T", tt.Name, err) t.Errorf("%s: expected a StatusError, got %T", tt.Name, err)
} }
fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
if !ok {
t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
continue
}
if len(tt.ExpectAnnotations) == 0 {
assert.Empty(t, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
} else {
assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
}
} }
} }

View File

@ -18,6 +18,7 @@ package testing
import ( import (
"net/url" "net/url"
"sync"
registrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" registrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -95,7 +96,37 @@ func newAttributesRecord(object metav1.Object, oldObject metav1.Object, kind sch
UID: "webhook-test", UID: "webhook-test",
} }
return admission.NewAttributesRecord(object.(runtime.Object), oldObject.(runtime.Object), kind, namespace, name, gvr, subResource, admission.Update, dryRun, &userInfo) return &FakeAttributes{
Attributes: admission.NewAttributesRecord(object.(runtime.Object), oldObject.(runtime.Object), kind, namespace, name, gvr, subResource, admission.Update, dryRun, &userInfo),
}
}
// FakeAttributes decorate admission.Attributes. It's used to trace the added annotations.
type FakeAttributes struct {
admission.Attributes
annotations map[string]string
mutex sync.Mutex
}
// AddAnnotation adds an annotation key value pair to FakeAttributes
func (f *FakeAttributes) AddAnnotation(k, v string) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if err := f.Attributes.AddAnnotation(k, v); err != nil {
return err
}
if f.annotations == nil {
f.annotations = make(map[string]string)
}
f.annotations[k] = v
return nil
}
// GetAnnotations reads annotations from FakeAttributes
func (f *FakeAttributes) GetAnnotations() map[string]string {
f.mutex.Lock()
defer f.mutex.Unlock()
return f.annotations
} }
// NewAttribute returns static admission Attributes for testing. // NewAttribute returns static admission Attributes for testing.
@ -145,15 +176,16 @@ func (c urlConfigGenerator) ccfgURL(urlPath string) registrationv1beta1.WebhookC
// Test is a webhook test case. // Test is a webhook test case.
type Test struct { type Test struct {
Name string Name string
Webhooks []registrationv1beta1.Webhook Webhooks []registrationv1beta1.Webhook
Path string Path string
IsCRD bool IsCRD bool
IsDryRun bool IsDryRun bool
AdditionalLabels map[string]string AdditionalLabels map[string]string
ExpectLabels map[string]string ExpectLabels map[string]string
ExpectAllow bool ExpectAllow bool
ErrorContains string ErrorContains string
ExpectAnnotations map[string]string
} }
// NewNonMutatingTestCases returns test cases with a given base url. // NewNonMutatingTestCases returns test cases with a given base url.
@ -181,12 +213,13 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
{ {
Name: "match & allow", Name: "match & allow",
Webhooks: []registrationv1beta1.Webhook{{ Webhooks: []registrationv1beta1.Webhook{{
Name: "allow", Name: "allow.example.com",
ClientConfig: ccfgSVC("allow"), ClientConfig: ccfgSVC("allow"),
Rules: matchEverythingRules, Rules: matchEverythingRules,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
}}, }},
ExpectAllow: true, ExpectAllow: true,
ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"},
}, },
{ {
Name: "match & disallow", Name: "match & disallow",
@ -312,12 +345,13 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
{ {
Name: "match & allow (url)", Name: "match & allow (url)",
Webhooks: []registrationv1beta1.Webhook{{ Webhooks: []registrationv1beta1.Webhook{{
Name: "allow", Name: "allow.example.com",
ClientConfig: ccfgURL("allow"), ClientConfig: ccfgURL("allow"),
Rules: matchEverythingRules, Rules: matchEverythingRules,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
}}, }},
ExpectAllow: true, ExpectAllow: true,
ExpectAnnotations: map[string]string{"allow.example.com/key1": "value1"},
}, },
{ {
Name: "match & disallow (url)", Name: "match & disallow (url)",
@ -374,6 +408,16 @@ func NewNonMutatingTestCases(url *url.URL) []Test {
IsDryRun: true, IsDryRun: true,
ErrorContains: "does not support dry run", ErrorContains: "does not support dry run",
}, },
{
Name: "illegal annotation format",
Webhooks: []registrationv1beta1.Webhook{{
Name: "invalidAnnotation",
ClientConfig: ccfgURL("invalidAnnotation"),
Rules: matchEverythingRules,
NamespaceSelector: &metav1.LabelSelector{},
}},
ExpectAllow: true,
},
// No need to test everything with the url case, since only the // No need to test everything with the url case, since only the
// connection is different. // connection is different.
} }
@ -387,14 +431,15 @@ func NewMutatingTestCases(url *url.URL) []Test {
{ {
Name: "match & remove label", Name: "match & remove label",
Webhooks: []registrationv1beta1.Webhook{{ Webhooks: []registrationv1beta1.Webhook{{
Name: "removeLabel", Name: "removelabel.example.com",
ClientConfig: ccfgSVC("removeLabel"), ClientConfig: ccfgSVC("removeLabel"),
Rules: matchEverythingRules, Rules: matchEverythingRules,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
}}, }},
ExpectAllow: true, ExpectAllow: true,
AdditionalLabels: map[string]string{"remove": "me"}, AdditionalLabels: map[string]string{"remove": "me"},
ExpectLabels: map[string]string{"pod.name": "my-pod"}, ExpectLabels: map[string]string{"pod.name": "my-pod"},
ExpectAnnotations: map[string]string{"removelabel.example.com/key1": "value1"},
}, },
{ {
Name: "match & add label", Name: "match & add label",
@ -422,15 +467,16 @@ func NewMutatingTestCases(url *url.URL) []Test {
{ {
Name: "match CRD & remove label", Name: "match CRD & remove label",
Webhooks: []registrationv1beta1.Webhook{{ Webhooks: []registrationv1beta1.Webhook{{
Name: "removeLabel", Name: "removelabel.example.com",
ClientConfig: ccfgSVC("removeLabel"), ClientConfig: ccfgSVC("removeLabel"),
Rules: matchEverythingRules, Rules: matchEverythingRules,
NamespaceSelector: &metav1.LabelSelector{}, NamespaceSelector: &metav1.LabelSelector{},
}}, }},
IsCRD: true, IsCRD: true,
ExpectAllow: true, ExpectAllow: true,
AdditionalLabels: map[string]string{"remove": "me"}, AdditionalLabels: map[string]string{"remove": "me"},
ExpectLabels: map[string]string{"crd.name": "my-test-crd"}, ExpectLabels: map[string]string{"crd.name": "my-test-crd"},
ExpectAnnotations: map[string]string{"removelabel.example.com/key1": "value1"},
}, },
{ {
Name: "match & invalid mutation", Name: "match & invalid mutation",

View File

@ -83,6 +83,9 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
Response: &v1beta1.AdmissionResponse{ Response: &v1beta1.AdmissionResponse{
Allowed: true, Allowed: true,
AuditAnnotations: map[string]string{
"key1": "value1",
},
}, },
}) })
case "/removeLabel": case "/removeLabel":
@ -93,6 +96,9 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
Allowed: true, Allowed: true,
PatchType: &pt, PatchType: &pt,
Patch: []byte(`[{"op": "remove", "path": "/metadata/labels/remove"}]`), Patch: []byte(`[{"op": "remove", "path": "/metadata/labels/remove"}]`),
AuditAnnotations: map[string]string{
"key1": "value1",
},
}, },
}) })
case "/addLabel": case "/addLabel":
@ -118,6 +124,16 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
case "/nilResponse": case "/nilResponse":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{}) json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{})
case "/invalidAnnotation":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
Response: &v1beta1.AdmissionResponse{
Allowed: true,
AuditAnnotations: map[string]string{
"invalid*key": "value1",
},
},
})
default: default:
http.NotFound(w, r) http.NotFound(w, r)
} }

View File

@ -36,6 +36,7 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:go_default_library", "//staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library", "//vendor/github.com/stretchr/testify/require:go_default_library",
], ],
) )

View File

@ -23,15 +23,15 @@ import (
"time" "time"
"github.com/golang/glog" "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" admissionv1beta1 "k8s.io/api/admission/v1beta1"
"k8s.io/api/admissionregistration/v1beta1" "k8s.io/api/admissionregistration/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics" admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
"k8s.io/apiserver/pkg/admission/plugin/webhook/config"
webhookerrors "k8s.io/apiserver/pkg/admission/plugin/webhook/errors" 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" "k8s.io/apiserver/pkg/admission/plugin/webhook/request"
) )
@ -116,6 +116,12 @@ func (d *validatingDispatcher) callHook(ctx context.Context, h *v1beta1.Webhook,
if response.Response == nil { if response.Response == nil {
return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")} return &webhookerrors.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("Webhook response was absent")}
} }
for k, v := range response.Response.AuditAnnotations {
key := h.Name + "/" + k
if err := attr.AddAnnotation(key, v); err != nil {
glog.Warningf("Failed to set admission audit annotation %s to %s for validating webhook %s: %v", key, v, h.Name, err)
}
}
if response.Response.Allowed { if response.Response.Allowed {
return nil return nil
} }

View File

@ -21,6 +21,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"k8s.io/api/admission/v1beta1" "k8s.io/api/admission/v1beta1"
@ -72,7 +73,8 @@ func TestValidate(t *testing.T) {
continue continue
} }
err = wh.Validate(webhooktesting.NewAttribute(ns, nil, tt.IsDryRun)) attr := webhooktesting.NewAttribute(ns, nil, tt.IsDryRun)
err = wh.Validate(attr)
if tt.ExpectAllow != (err == nil) { if tt.ExpectAllow != (err == nil) {
t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err) t.Errorf("%s: expected allowed=%v, but got err=%v", tt.Name, tt.ExpectAllow, err)
} }
@ -85,6 +87,16 @@ func TestValidate(t *testing.T) {
if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr { if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
t.Errorf("%s: expected a StatusError, got %T", tt.Name, err) t.Errorf("%s: expected a StatusError, got %T", tt.Name, err)
} }
fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
if !ok {
t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
continue
}
if len(tt.ExpectAnnotations) == 0 {
assert.Empty(t, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
} else {
assert.Equal(t, tt.ExpectAnnotations, fakeAttr.GetAnnotations(), tt.Name+": annotations not set as expected.")
}
} }
} }