mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 04:33:26 +00:00
support annotations for admission webhook
This commit is contained in:
parent
edc3e40dce
commit
0ebfc3e078
@ -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",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user