Skip to content

Commit c7cb69d

Browse files
committed
Handle panics during conversion more gracefully
Signed-off-by: Stefan Büringer [email protected]
1 parent fface7c commit c7cb69d

File tree

9 files changed

+112
-11
lines changed

9 files changed

+112
-11
lines changed

pkg/webhook/conversion/conversion.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ See pkg/conversion for interface definitions required to ensure an API Type is c
2222
package conversion
2323

2424
import (
25+
"context"
2526
"encoding/json"
2627
"fmt"
2728
"net/http"
@@ -31,8 +32,10 @@ import (
3132
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3233
"k8s.io/apimachinery/pkg/runtime"
3334
"k8s.io/apimachinery/pkg/runtime/schema"
35+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3436
"sigs.k8s.io/controller-runtime/pkg/conversion"
3537
logf "sigs.k8s.io/controller-runtime/pkg/log"
38+
conversionmetrics "sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics"
3639
)
3740

3841
var (
@@ -53,6 +56,8 @@ type webhook struct {
5356
var _ http.Handler = &webhook{}
5457

5558
func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
59+
ctx := r.Context()
60+
5661
convertReview := &apix.ConversionReview{}
5762
err := json.NewDecoder(r.Body).Decode(convertReview)
5863
if err != nil {
@@ -69,7 +74,7 @@ func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6974

7075
// TODO(droot): may be move the conversion logic to a separate module to
7176
// decouple it from the http layer ?
72-
resp, err := wh.handleConvertRequest(convertReview.Request)
77+
resp, err := wh.handleConvertRequest(ctx, convertReview.Request)
7378
if err != nil {
7479
log.Error(err, "failed to convert", "request", convertReview.Request.UID)
7580
convertReview.Response = errored(err)
@@ -87,7 +92,18 @@ func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
8792
}
8893

8994
// handles a version conversion request.
90-
func (wh *webhook) handleConvertRequest(req *apix.ConversionRequest) (*apix.ConversionResponse, error) {
95+
func (wh *webhook) handleConvertRequest(ctx context.Context, req *apix.ConversionRequest) (_ *apix.ConversionResponse, retErr error) {
96+
defer func() {
97+
if r := recover(); r != nil {
98+
conversionmetrics.WebhookPanics.WithLabelValues().Inc()
99+
100+
for _, fn := range utilruntime.PanicHandlers {
101+
fn(ctx, r)
102+
}
103+
retErr = fmt.Errorf("panic occurred during conversion: %v", r)
104+
return
105+
}
106+
}()
91107
if req == nil {
92108
return nil, fmt.Errorf("conversion request is nil")
93109
}

pkg/webhook/conversion/conversion_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,29 @@ var _ = Describe("Conversion Webhook", func() {
296296
Expect(convReview.Response.ConvertedObjects).To(BeEmpty())
297297
})
298298

299+
It("should return error on panic in conversion", func() {
300+
301+
v1Obj := makeV1Obj()
302+
v1Obj.Spec.PanicInConversion = true
303+
304+
convReq := &apix.ConversionReview{
305+
TypeMeta: metav1.TypeMeta{},
306+
Request: &apix.ConversionRequest{
307+
DesiredAPIVersion: "jobs.testprojects.kb.io/v3",
308+
Objects: []runtime.RawExtension{
309+
{
310+
Object: v1Obj,
311+
},
312+
},
313+
},
314+
}
315+
316+
convReview := doRequest(convReq)
317+
318+
Expect(convReview.Response.ConvertedObjects).To(HaveLen(0))
319+
Expect(convReview.Response.Result.Status).To(Equal(metav1.StatusFailure))
320+
Expect(convReview.Response.Result.Message).To(Equal("panic occurred during conversion: PanicInConversion field set to true"))
321+
})
299322
})
300323

301324
var _ = Describe("IsConvertible", func() {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package metrics
18+
19+
import (
20+
"github.com/prometheus/client_golang/prometheus"
21+
"sigs.k8s.io/controller-runtime/pkg/metrics"
22+
)
23+
24+
var (
25+
// WebhookPanics is a prometheus counter metrics which holds the total
26+
// number of panics from conversion webhooks.
27+
WebhookPanics = prometheus.NewCounterVec(prometheus.CounterOpts{
28+
Name: "controller_runtime_conversion_webhook_panics_total",
29+
Help: "Total number of conversion webhook panics",
30+
}, []string{})
31+
)
32+
33+
func init() {
34+
metrics.Registry.MustRegister(
35+
WebhookPanics,
36+
)
37+
// Init metric.
38+
WebhookPanics.WithLabelValues().Add(0)
39+
}

pkg/webhook/conversion/testdata/api/v1/externaljob_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package v1
1717

1818
import (
1919
"fmt"
20+
2021
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2122
"sigs.k8s.io/controller-runtime/pkg/conversion"
2223

@@ -31,6 +32,9 @@ type ExternalJobSpec struct {
3132
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
3233
// Important: Run "make" to regenerate code after modifying this file
3334
RunAt string `json:"runAt"`
35+
36+
// PanicInConversion triggers a panic during conversion when set to true.
37+
PanicInConversion bool `json:"panicInConversion"`
3438
}
3539

3640
// ExternalJobStatus defines the observed state of ExternalJob
@@ -66,6 +70,9 @@ func init() {
6670
// ConvertTo implements conversion logic to convert to Hub type (v2.ExternalJob
6771
// in this case)
6872
func (ej *ExternalJob) ConvertTo(dst conversion.Hub) error {
73+
if ej.Spec.PanicInConversion {
74+
panic("PanicInConversion field set to true")
75+
}
6976
switch t := dst.(type) {
7077
case *v2.ExternalJob:
7178
jobv2 := dst.(*v2.ExternalJob)
@@ -80,6 +87,9 @@ func (ej *ExternalJob) ConvertTo(dst conversion.Hub) error {
8087
// ConvertFrom implements conversion logic to convert from Hub type (v2.ExternalJob
8188
// in this case)
8289
func (ej *ExternalJob) ConvertFrom(src conversion.Hub) error {
90+
if ej.Spec.PanicInConversion {
91+
panic("PanicInConversion field set to true")
92+
}
8393
switch t := src.(type) {
8494
case *v2.ExternalJob:
8595
jobv2 := src.(*v2.ExternalJob)

pkg/webhook/conversion/testdata/api/v1/zz_generated.deepcopy.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/webhook/conversion/testdata/api/v2/externaljob_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ type ExternalJobSpec struct {
2727
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
2828
// Important: Run "make" to regenerate code after modifying this file
2929
ScheduleAt string `json:"scheduleAt"`
30+
31+
// PanicInConversion triggers a panic during conversion when set to true.
32+
PanicInConversion bool `json:"panicInConversion"`
3033
}
3134

3235
// ExternalJobStatus defines the observed state of ExternalJob

pkg/webhook/conversion/testdata/api/v2/zz_generated.deepcopy.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/webhook/conversion/testdata/api/v3/externaljob_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package v3
1717

1818
import (
1919
"fmt"
20+
2021
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2122
"sigs.k8s.io/controller-runtime/pkg/conversion"
2223

@@ -31,6 +32,9 @@ type ExternalJobSpec struct {
3132
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
3233
// Important: Run "make" to regenerate code after modifying this file
3334
DeferredAt string `json:"deferredAt"`
35+
36+
// PanicInConversion triggers a panic during conversion when set to true.
37+
PanicInConversion bool `json:"panicInConversion"`
3438
}
3539

3640
// ExternalJobStatus defines the observed state of ExternalJob
@@ -66,6 +70,9 @@ func init() {
6670
// ConvertTo implements conversion logic to convert to Hub type (v2.ExternalJob
6771
// in this case)
6872
func (ej *ExternalJob) ConvertTo(dst conversion.Hub) error {
73+
if ej.Spec.PanicInConversion {
74+
panic("PanicInConversion field set to true")
75+
}
6976
switch t := dst.(type) {
7077
case *v2.ExternalJob:
7178
jobv2 := dst.(*v2.ExternalJob)
@@ -80,6 +87,9 @@ func (ej *ExternalJob) ConvertTo(dst conversion.Hub) error {
8087
// ConvertFrom implements conversion logic to convert from Hub type (v2.ExternalJob
8188
// in this case)
8289
func (ej *ExternalJob) ConvertFrom(src conversion.Hub) error {
90+
if ej.Spec.PanicInConversion {
91+
panic("PanicInConversion field set to true")
92+
}
8393
switch t := src.(type) {
8494
case *v2.ExternalJob:
8595
jobv2 := src.(*v2.ExternalJob)

pkg/webhook/conversion/testdata/api/v3/zz_generated.deepcopy.go

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)