Skip to content

Commit 027ce71

Browse files
committed
Remove path from admission, more response helpers
This removes the path functionality from admission webhooks, since it's not strictly needed. Relevant metrics are moved up to the webhook server, and path is added to register. This also introduces a couple of helpers that should make code a bit clearer when writing admission webhooks (Allowed, Denied, Patched), and an alias file to avoid importing both webhook and webhook/admission.
1 parent f46c199 commit 027ce71

File tree

12 files changed

+264
-203
lines changed

12 files changed

+264
-203
lines changed

example/main.go

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import (
3232
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
3333
"sigs.k8s.io/controller-runtime/pkg/source"
3434
"sigs.k8s.io/controller-runtime/pkg/webhook"
35-
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3635
)
3736

3837
var log = logf.Log.WithName("example-controller")
@@ -78,17 +77,6 @@ func main() {
7877
}
7978

8079
// Setup webhooks
81-
entryLog.Info("setting up webhooks")
82-
mutatingWebhook := &admission.Webhook{
83-
Path: "/mutate-pods",
84-
Handler: &podAnnotator{},
85-
}
86-
87-
validatingWebhook := &admission.Webhook{
88-
Path: "/validate-pods",
89-
Handler: &podValidator{},
90-
}
91-
9280
entryLog.Info("setting up webhook server")
9381
hookServer := &webhook.Server{
9482
Port: 9876,
@@ -100,11 +88,8 @@ func main() {
10088
}
10189

10290
entryLog.Info("registering webhooks to the webhook server")
103-
err = hookServer.Register(mutatingWebhook, validatingWebhook)
104-
if err != nil {
105-
entryLog.Error(err, "unable to setup the admission server")
106-
os.Exit(1)
107-
}
91+
hookServer.Register("/mutate-pods", webhook.Admission{&podAnnotator{}})
92+
hookServer.Register("/validate-pods", webhook.Admission{&podValidator{}})
10893

10994
entryLog.Info("starting manager")
11095
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {

example/mutatingwebhook.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ type podAnnotator struct {
3232
decoder admission.Decoder
3333
}
3434

35-
// Implement admission.Handler so the controller can handle admission request.
36-
var _ admission.Handler = &podAnnotator{}
37-
3835
// podAnnotator adds an annotation to every incoming pods.
3936
func (a *podAnnotator) Handle(ctx context.Context, req admission.Request) admission.Response {
4037
pod := &corev1.Pod{}
@@ -44,10 +41,10 @@ func (a *podAnnotator) Handle(ctx context.Context, req admission.Request) admiss
4441
return admission.ErrorResponse(http.StatusBadRequest, err)
4542
}
4643

47-
err = a.mutatePodsFn(ctx, pod)
48-
if err != nil {
49-
return admission.ErrorResponse(http.StatusInternalServerError, err)
44+
if pod.Annotations == nil {
45+
pod.Annotations = map[string]string{}
5046
}
47+
pod.Annotations["example-mutating-admission-webhook"] = "foo"
5148

5249
marshaledPod, err := json.Marshal(pod)
5350
if err != nil {
@@ -57,15 +54,6 @@ func (a *podAnnotator) Handle(ctx context.Context, req admission.Request) admiss
5754
return admission.PatchResponseFromRaw(req.AdmissionRequest.Object.Raw, marshaledPod)
5855
}
5956

60-
// mutatePodsFn add an annotation to the given pod
61-
func (a *podAnnotator) mutatePodsFn(ctx context.Context, pod *corev1.Pod) error {
62-
if pod.Annotations == nil {
63-
pod.Annotations = map[string]string{}
64-
}
65-
pod.Annotations["example-mutating-admission-webhook"] = "foo"
66-
return nil
67-
}
68-
6957
// podAnnotator implements inject.Client.
7058
// A client will be automatically injected.
7159

example/validatingwebhook.go

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ type podValidator struct {
3232
decoder admission.Decoder
3333
}
3434

35-
// Implement admission.Handler so the controller can handle admission request.
36-
var _ admission.Handler = &podValidator{}
37-
3835
// podValidator admits a pod iff a specific annotation exists.
3936
func (v *podValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
4037
pod := &corev1.Pod{}
@@ -44,26 +41,16 @@ func (v *podValidator) Handle(ctx context.Context, req admission.Request) admiss
4441
return admission.ErrorResponse(http.StatusBadRequest, err)
4542
}
4643

47-
allowed, reason, err := v.validatePodsFn(ctx, pod)
48-
if err != nil {
49-
return admission.ErrorResponse(http.StatusInternalServerError, err)
50-
}
51-
return admission.ValidationResponse(allowed, reason)
52-
}
53-
54-
func (v *podValidator) validatePodsFn(ctx context.Context, pod *corev1.Pod) (bool, string, error) {
5544
key := "example-mutating-admission-webhook"
5645
anno, found := pod.Annotations[key]
57-
switch {
58-
case !found:
59-
return found, fmt.Sprintf("failed to find annotation with key: %q", key), nil
60-
case found && anno == "foo":
61-
return found, "", nil
62-
case found && anno != "foo":
63-
return false,
64-
fmt.Sprintf("the value associate with key %q is expected to be %q, but got %q", key, "foo", anno), nil
46+
if !found {
47+
return admission.Denied(fmt.Sprintf("missing annotation %s", key))
48+
}
49+
if anno != "foo" {
50+
return admission.Denied(fmt.Sprintf("annotation %s did not have value %q", key, "foo"))
6551
}
66-
return false, "", nil
52+
53+
return admission.Allowed("")
6754
}
6855

6956
// podValidator implements inject.Client.

pkg/webhook/admission/http.go

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,33 +24,24 @@ import (
2424
"io"
2525
"io/ioutil"
2626
"net/http"
27-
"time"
2827

2928
"k8s.io/api/admission/v1beta1"
3029
admissionv1beta1 "k8s.io/api/admission/v1beta1"
3130
"k8s.io/apimachinery/pkg/runtime"
3231
"k8s.io/apimachinery/pkg/runtime/serializer"
3332
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
34-
"sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics"
3533
)
3634

37-
var admissionv1beta1scheme = runtime.NewScheme()
38-
var admissionv1beta1schemecodecs = serializer.NewCodecFactory(admissionv1beta1scheme)
35+
var admissionScheme = runtime.NewScheme()
36+
var admissionCodecs = serializer.NewCodecFactory(admissionScheme)
3937

4038
func init() {
41-
addToScheme(admissionv1beta1scheme)
42-
}
43-
44-
func addToScheme(scheme *runtime.Scheme) {
45-
utilruntime.Must(admissionv1beta1.AddToScheme(scheme))
39+
utilruntime.Must(admissionv1beta1.AddToScheme(admissionScheme))
4640
}
4741

4842
var _ http.Handler = &Webhook{}
4943

50-
func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
51-
startTS := time.Now()
52-
defer metrics.RequestLatency.WithLabelValues(wh.Path).Observe(time.Now().Sub(startTS).Seconds())
53-
44+
func (wh Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5445
var body []byte
5546
var err error
5647

@@ -85,7 +76,7 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
8576
// avoid an extra copy
8677
Request: &req.AdmissionRequest,
8778
}
88-
if _, _, err := admissionv1beta1schemecodecs.UniversalDeserializer().Decode(body, nil, &ar); err != nil {
79+
if _, _, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar); err != nil {
8980
log.Error(err, "unable to decode the request")
9081
reviewResponse = ErrorResponse(http.StatusBadRequest, err)
9182
wh.writeResponse(w, reviewResponse)
@@ -98,14 +89,6 @@ func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
9889
}
9990

10091
func (wh *Webhook) writeResponse(w io.Writer, response Response) {
101-
if response.Result.Code != 0 {
102-
if response.Result.Code == http.StatusOK {
103-
metrics.TotalRequests.WithLabelValues(wh.Path, "true").Inc()
104-
} else {
105-
metrics.TotalRequests.WithLabelValues(wh.Path, "false").Inc()
106-
}
107-
}
108-
10992
encoder := json.NewEncoder(w)
11093
responseAdmissionReview := v1beta1.AdmissionReview{
11194
Response: &response.AdmissionResponse,

pkg/webhook/admission/inject.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616

1717
package admission
1818

19-
// Decoder is used by the ControllerManager to inject decoder into webhook handlers.
19+
// DecoderInjector is used by the ControllerManager to inject decoder into webhook handlers.
2020
type DecoderInjector interface {
2121
InjectDecoder(Decoder) error
2222
}

pkg/webhook/admission/response.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,28 @@ import (
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2626
)
2727

28+
// Allowed constructs a response indicating that the given operation
29+
// is allowed (without any patches).
30+
func Allowed(reason string) Response {
31+
return ValidationResponse(true, reason)
32+
}
33+
34+
// Denied constructs a response indicating that the given operation
35+
// is not allowed.
36+
func Denied(reason string) Response {
37+
return ValidationResponse(false, reason)
38+
}
39+
40+
// Patched constructs a response indicating that the given operation is
41+
// allowed, and that the target object should be modified by the given
42+
// JSONPatch operations.
43+
func Patched(reason string, patches ...jsonpatch.JsonPatchOperation) Response {
44+
resp := Allowed(reason)
45+
resp.Patches = patches
46+
47+
return resp
48+
}
49+
2850
// ErrorResponse creates a new Response for error-handling a request.
2951
func ErrorResponse(code int32, err error) Response {
3052
return Response{

pkg/webhook/admission/response_test.go

Lines changed: 136 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,95 @@ import (
2929
)
3030

3131
var _ = Describe("Admission Webhook Response Helpers", func() {
32+
Describe("Allowed", func() {
33+
It("should return an 'allowed' response", func() {
34+
Expect(Allowed("")).To(Equal(
35+
Response{
36+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
37+
Allowed: true,
38+
},
39+
},
40+
))
41+
})
42+
43+
It("should populate a status with a reason when a reason is given", func() {
44+
Expect(Allowed("acceptable")).To(Equal(
45+
Response{
46+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
47+
Allowed: true,
48+
Result: &metav1.Status{
49+
Reason: "acceptable",
50+
},
51+
},
52+
},
53+
))
54+
})
55+
})
56+
57+
Describe("Denied", func() {
58+
It("should return a 'not allowed' response", func() {
59+
Expect(Denied("")).To(Equal(
60+
Response{
61+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
62+
Allowed: false,
63+
},
64+
},
65+
))
66+
})
67+
68+
It("should populate a status with a reason when a reason is given", func() {
69+
Expect(Denied("UNACCEPTABLE!")).To(Equal(
70+
Response{
71+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
72+
Allowed: false,
73+
Result: &metav1.Status{
74+
Reason: "UNACCEPTABLE!",
75+
},
76+
},
77+
},
78+
))
79+
})
80+
})
81+
82+
Describe("Patched", func() {
83+
ops := []jsonpatch.JsonPatchOperation{
84+
{
85+
Operation: "replace",
86+
Path: "/spec/selector/matchLabels",
87+
Value: map[string]string{"foo": "bar"},
88+
},
89+
{
90+
Operation: "delete",
91+
Path: "/spec/replicas",
92+
},
93+
}
94+
It("should return an 'allowed' response with the given patches", func() {
95+
Expect(Patched("", ops...)).To(Equal(
96+
Response{
97+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
98+
Allowed: true,
99+
},
100+
Patches: ops,
101+
},
102+
))
103+
})
104+
It("should populate a status with a reason when a reason is given", func() {
105+
Expect(Patched("some changes", ops...)).To(Equal(
106+
Response{
107+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
108+
Allowed: true,
109+
Result: &metav1.Status{
110+
Reason: "some changes",
111+
},
112+
},
113+
Patches: ops,
114+
},
115+
))
116+
})
117+
})
118+
32119
Describe("ErrorResponse", func() {
33-
It("should return the response with an error", func() {
120+
It("should return a denied response with an error", func() {
34121
err := errors.New("this is an error")
35122
expected := Response{
36123
AdmissionResponse: admissionv1beta1.AdmissionResponse{
@@ -47,30 +134,65 @@ var _ = Describe("Admission Webhook Response Helpers", func() {
47134
})
48135

49136
Describe("ValidationResponse", func() {
50-
It("should return the response with an admission decision", func() {
51-
expected := Response{
52-
AdmissionResponse: admissionv1beta1.AdmissionResponse{
53-
Allowed: true,
54-
Result: &metav1.Status{
55-
Reason: metav1.StatusReason("allow to admit"),
137+
It("should populate a status with a reason when a reason is given", func() {
138+
By("checking that a message is populated for 'allowed' responses")
139+
Expect(ValidationResponse(true, "acceptable")).To(Equal(
140+
Response{
141+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
142+
Allowed: true,
143+
Result: &metav1.Status{
144+
Reason: "acceptable",
145+
},
56146
},
57147
},
58-
}
59-
resp := ValidationResponse(true, "allow to admit")
60-
Expect(resp).To(Equal(expected))
148+
))
149+
150+
By("checking that a message is populated for 'denied' responses")
151+
Expect(ValidationResponse(false, "UNACCEPTABLE!")).To(Equal(
152+
Response{
153+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
154+
Allowed: false,
155+
Result: &metav1.Status{
156+
Reason: "UNACCEPTABLE!",
157+
},
158+
},
159+
},
160+
))
161+
})
162+
163+
It("should return an admission decision", func() {
164+
By("checking that it returns a 'denied' response when allowed is false")
165+
Expect(ValidationResponse(true, "")).To(Equal(
166+
Response{
167+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
168+
Allowed: true,
169+
},
170+
},
171+
))
172+
173+
By("checking that it returns an 'allowed' response when allowed is true")
174+
Expect(ValidationResponse(false, "")).To(Equal(
175+
Response{
176+
AdmissionResponse: admissionv1beta1.AdmissionResponse{
177+
Allowed: false,
178+
},
179+
},
180+
))
61181
})
62182
})
63183

64-
Describe("PatchResponse", func() {
65-
It("should return the response with patches", func() {
184+
Describe("PatchResponseFromRaw", func() {
185+
It("should return an 'allowed' response with a patch of the diff between two sets of serialized JSON", func() {
66186
expected := Response{
67-
Patches: []jsonpatch.JsonPatchOperation{},
187+
Patches: []jsonpatch.JsonPatchOperation{
188+
{Operation: "replace", Path: "/a", Value: "bar"},
189+
},
68190
AdmissionResponse: admissionv1beta1.AdmissionResponse{
69191
Allowed: true,
70192
PatchType: func() *admissionv1beta1.PatchType { pt := admissionv1beta1.PatchTypeJSONPatch; return &pt }(),
71193
},
72194
}
73-
resp := PatchResponseFromRaw([]byte(`{}`), []byte(`{}`))
195+
resp := PatchResponseFromRaw([]byte(`{"a": "foo"}`), []byte(`{"a": "bar"}`))
74196
Expect(resp).To(Equal(expected))
75197
})
76198
})

0 commit comments

Comments
 (0)