Skip to content

Commit c7df6d0

Browse files
authored
Merge pull request #3278 from sbueringer/pr-conversion-panics
✨ Handle panics during conversion more gracefully
2 parents 6824653 + 336ab90 commit c7df6d0

File tree

9 files changed

+113
-11
lines changed

9 files changed

+113
-11
lines changed

pkg/webhook/conversion/conversion.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ 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"
27+
"errors"
2628
"fmt"
2729
"net/http"
2830

@@ -31,8 +33,10 @@ import (
3133
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3234
"k8s.io/apimachinery/pkg/runtime"
3335
"k8s.io/apimachinery/pkg/runtime/schema"
36+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3437
"sigs.k8s.io/controller-runtime/pkg/conversion"
3538
logf "sigs.k8s.io/controller-runtime/pkg/log"
39+
conversionmetrics "sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics"
3640
)
3741

3842
var (
@@ -53,6 +57,8 @@ type webhook struct {
5357
var _ http.Handler = &webhook{}
5458

5559
func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
60+
ctx := r.Context()
61+
5662
convertReview := &apix.ConversionReview{}
5763
err := json.NewDecoder(r.Body).Decode(convertReview)
5864
if err != nil {
@@ -69,7 +75,7 @@ func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6975

7076
// TODO(droot): may be move the conversion logic to a separate module to
7177
// decouple it from the http layer ?
72-
resp, err := wh.handleConvertRequest(convertReview.Request)
78+
resp, err := wh.handleConvertRequest(ctx, convertReview.Request)
7379
if err != nil {
7480
log.Error(err, "failed to convert", "request", convertReview.Request.UID)
7581
convertReview.Response = errored(err)
@@ -87,7 +93,18 @@ func (wh *webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
8793
}
8894

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

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("internal error occurred during conversion"))
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)