Skip to content

Commit e17b4ce

Browse files
committed
webhook: add an option to recover from panics in handler
Currently, a panic occcurence in a webhook handler is not recovered and crashes the webhook server. This change adds an option to Webhook to recover panics similar to how it is handled in Reconciler. It ensures that panics are converted to normal error response.
1 parent 2f77235 commit e17b4ce

File tree

2 files changed

+77
-1
lines changed

2 files changed

+77
-1
lines changed

pkg/webhook/admission/webhook.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ package admission
1919
import (
2020
"context"
2121
"errors"
22+
"fmt"
2223
"net/http"
2324

25+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
26+
2427
"github.com/go-logr/logr"
2528
jsonpatch "gomodules.xyz/jsonpatch/v2"
2629
admissionv1 "k8s.io/api/admission/v1"
@@ -121,6 +124,9 @@ type Webhook struct {
121124
// and potentially patches to apply to the handler.
122125
Handler Handler
123126

127+
// RecoverPanic indicates whether the panic caused by webhook should be recovered.
128+
RecoverPanic bool
129+
124130
// WithContextFunc will allow you to take the http.Request.Context() and
125131
// add any additional information such as passing the request path or
126132
// headers thus allowing you to read them from within the handler
@@ -142,7 +148,22 @@ func (wh *Webhook) InjectLogger(l logr.Logger) error {
142148
// If the webhook is mutating type, it delegates the AdmissionRequest to each handler and merge the patches.
143149
// If the webhook is validating type, it delegates the AdmissionRequest to each handler and
144150
// deny the request if anyone denies.
145-
func (wh *Webhook) Handle(ctx context.Context, req Request) Response {
151+
func (wh *Webhook) Handle(ctx context.Context, req Request) (response Response) {
152+
defer func() {
153+
if r := recover(); r != nil {
154+
if wh.RecoverPanic {
155+
for _, fn := range utilruntime.PanicHandlers {
156+
fn(r)
157+
}
158+
response = Errored(http.StatusInternalServerError, fmt.Errorf("panic: %v [recovered]", r))
159+
return
160+
}
161+
162+
wh.log.Info(fmt.Sprintf("Observed a panic in reconciler: %v", r))
163+
panic(r)
164+
}
165+
}()
166+
146167
resp := wh.Handler.Handle(ctx, req)
147168
if err := resp.Complete(req); err != nil {
148169
wh.log.Error(err, "unable to encode response")

pkg/webhook/admission/webhook_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,61 @@ var _ = Describe("Admission Webhooks", func() {
192192
Expect(handler.dep.decoder).NotTo(BeNil())
193193
})
194194
})
195+
196+
Describe("panic recovery", func() {
197+
It("should recover panic if RecoverPanic is true", func() {
198+
panicHandler := func() *Webhook {
199+
handler := &fakeHandler{
200+
fn: func(ctx context.Context, req Request) Response {
201+
panic("injected panic")
202+
},
203+
}
204+
webhook := &Webhook{
205+
Handler: handler,
206+
RecoverPanic: true,
207+
log: logf.RuntimeLog.WithName("webhook"),
208+
}
209+
210+
return webhook
211+
}
212+
213+
By("setting up a webhook with a panicking handler")
214+
webhook := panicHandler()
215+
216+
By("invoking the webhook")
217+
resp := webhook.Handle(context.Background(), Request{})
218+
219+
By("checking that it errored the request")
220+
Expect(resp.Allowed).To(BeFalse())
221+
Expect(resp.Result.Code).To(Equal(int32(http.StatusInternalServerError)))
222+
Expect(resp.Result.Message).To(Equal("panic: injected panic [recovered]"))
223+
})
224+
225+
It("should not recover panic if RecoverPanic is false by default", func() {
226+
panicHandler := func() *Webhook {
227+
handler := &fakeHandler{
228+
fn: func(ctx context.Context, req Request) Response {
229+
panic("injected panic")
230+
},
231+
}
232+
webhook := &Webhook{
233+
Handler: handler,
234+
log: logf.RuntimeLog.WithName("webhook"),
235+
}
236+
237+
return webhook
238+
}
239+
240+
By("setting up a webhook with a panicking handler")
241+
defer func() {
242+
Expect(recover()).ShouldNot(BeNil())
243+
}()
244+
webhook := panicHandler()
245+
246+
By("invoking the webhook")
247+
webhook.Handle(context.Background(), Request{})
248+
})
249+
})
195250
})
196251

197252
type stringInjector interface {

0 commit comments

Comments
 (0)