Skip to content

Commit 100ad6e

Browse files
committed
Allow implementation of conversion outside of API packages
1 parent b8f1137 commit 100ad6e

File tree

8 files changed

+279
-17
lines changed

8 files changed

+279
-17
lines changed

pkg/builder/webhook.go

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package builder
1919
import (
2020
"context"
2121
"errors"
22+
"fmt"
2223
"net/http"
2324
"net/url"
2425
"regexp"
@@ -45,6 +46,7 @@ type WebhookBuilder struct {
4546
customPath string
4647
customValidatorCustomPath string
4748
customDefaulterCustomPath string
49+
converter conversion.Converter
4850
gvk schema.GroupVersionKind
4951
mgr manager.Manager
5052
config *rest.Config
@@ -86,6 +88,13 @@ func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator)
8688
return blder
8789
}
8890

91+
// WithConverter takes a converter.Converter interface which will then be configured to
92+
// be used by the conversion endpoint.
93+
func (blder *WebhookBuilder) WithConverter(converter conversion.Converter) *WebhookBuilder {
94+
blder.converter = converter
95+
return blder
96+
}
97+
8998
// WithLogConstructor overrides the webhook's LogConstructor.
9099
func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder {
91100
blder.logConstructor = logConstructor
@@ -287,18 +296,26 @@ func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook {
287296
}
288297

289298
func (blder *WebhookBuilder) registerConversionWebhook() error {
290-
ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType)
291-
if err != nil {
292-
log.Error(err, "conversion check failed", "GVK", blder.gvk)
293-
return err
294-
}
295-
if ok {
296-
if !blder.isAlreadyHandled("/convert") {
297-
blder.mgr.GetWebhookServer().Register("/convert", conversion.NewWebhookHandler(blder.mgr.GetScheme()))
299+
if blder.converter != nil {
300+
if err := blder.mgr.GetConversionRegistry().RegisterConverter(blder.gvk.GroupKind(), blder.converter); err != nil {
301+
return fmt.Errorf("failed to register Converter for %s: %w", blder.gvk.Kind, err)
302+
}
303+
} else {
304+
ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType)
305+
if err != nil {
306+
log.Error(err, "conversion check failed", "GVK", blder.gvk)
307+
return err
308+
}
309+
if !ok {
310+
return nil
298311
}
299-
log.Info("Conversion webhook enabled", "GVK", blder.gvk)
300312
}
301313

314+
if !blder.isAlreadyHandled("/convert") {
315+
blder.mgr.GetWebhookServer().Register("/convert", conversion.NewWebhookHandler(blder.mgr.GetScheme(), blder.mgr.GetConversionRegistry()))
316+
}
317+
log.Info("Conversion webhook enabled", "GVK", blder.gvk)
318+
302319
return nil
303320
}
304321

pkg/manager/internal.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"k8s.io/client-go/tools/leaderelection"
3737
"k8s.io/client-go/tools/leaderelection/resourcelock"
3838
"k8s.io/client-go/tools/record"
39+
"sigs.k8s.io/controller-runtime/pkg/webhook/conversion"
3940

4041
"sigs.k8s.io/controller-runtime/pkg/cache"
4142
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -130,6 +131,9 @@ type controllerManager struct {
130131
// webhookServer if unset, and Add() it to controllerManager.
131132
webhookServerOnce sync.Once
132133

134+
// conversionRegistry stores conversion.Converter for the conversion endpoint.
135+
conversionRegistry conversion.Registry
136+
133137
// leaderElectionID is the name of the resource that leader election
134138
// will use for holding the leader lock.
135139
leaderElectionID string
@@ -284,6 +288,10 @@ func (cm *controllerManager) GetWebhookServer() webhook.Server {
284288
return cm.webhookServer
285289
}
286290

291+
func (cm *controllerManager) GetConversionRegistry() conversion.Registry {
292+
return cm.conversionRegistry
293+
}
294+
287295
func (cm *controllerManager) GetLogger() logr.Logger {
288296
return cm.logger
289297
}

pkg/manager/internal/integration/manager_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ type ConversionWebhook struct {
262262
}
263263

264264
func createConversionWebhook(mgr manager.Manager) *ConversionWebhook {
265-
conversionHandler := conversion.NewWebhookHandler(mgr.GetScheme())
265+
conversionHandler := conversion.NewWebhookHandler(mgr.GetScheme(), mgr.GetConversionRegistry())
266266
httpClient := http.Client{
267267
// Setting a timeout to not get stuck when calling the readiness probe.
268268
Timeout: 5 * time.Second,

pkg/manager/manager.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ import (
2929
corev1 "k8s.io/api/core/v1"
3030
"k8s.io/apimachinery/pkg/api/meta"
3131
"k8s.io/apimachinery/pkg/runtime"
32+
"k8s.io/client-go/kubernetes/scheme"
3233
eventsv1client "k8s.io/client-go/kubernetes/typed/events/v1"
3334
"k8s.io/client-go/rest"
3435
"k8s.io/client-go/tools/events"
3536
"k8s.io/client-go/tools/leaderelection/resourcelock"
3637
"k8s.io/client-go/tools/record"
3738
"k8s.io/utils/ptr"
3839
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
40+
"sigs.k8s.io/controller-runtime/pkg/webhook/conversion"
3941

4042
"sigs.k8s.io/controller-runtime/pkg/cache"
4143
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -97,6 +99,10 @@ type Manager interface {
9799

98100
// GetControllerOptions returns controller global configuration options.
99101
GetControllerOptions() config.Controller
102+
103+
// GetConversionRegistry returns the conversion registry that is used to store conversion.Converter
104+
// for the conversion endpoint.
105+
GetConversionRegistry() conversion.Registry
100106
}
101107

102108
// Options are the arguments for creating a new Manager.
@@ -450,6 +456,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
450456
logger: options.Logger,
451457
elected: make(chan struct{}),
452458
webhookServer: options.WebhookServer,
459+
conversionRegistry: conversion.NewRegistry(options.Scheme),
453460
leaderElectionID: options.LeaderElectionID,
454461
leaseDuration: *options.LeaseDuration,
455462
renewDeadline: *options.RenewDeadline,
@@ -509,6 +516,11 @@ func setOptionsDefaults(config *rest.Config, options Options) (Options, error) {
509516
options.newRecorderProvider = intrec.NewProvider
510517
}
511518

519+
// Use the Kubernetes client-go scheme if none is specified
520+
if options.Scheme == nil {
521+
options.Scheme = scheme.Scheme
522+
}
523+
512524
// This is duplicated with pkg/cluster, we need it here
513525
// for the leader election and there to provide the user with
514526
// an EventBroadcaster

pkg/webhook/conversion/conversion.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,15 @@ var (
4343
log = logf.Log.WithName("conversion-webhook")
4444
)
4545

46-
func NewWebhookHandler(scheme *runtime.Scheme) http.Handler {
47-
return &webhook{scheme: scheme, decoder: NewDecoder(scheme)}
46+
func NewWebhookHandler(scheme *runtime.Scheme, registry Registry) http.Handler {
47+
return &webhook{scheme: scheme, decoder: NewDecoder(scheme), registry: registry}
4848
}
4949

5050
// webhook implements a CRD conversion webhook HTTP handler.
5151
type webhook struct {
52-
scheme *runtime.Scheme
53-
decoder *Decoder
52+
scheme *runtime.Scheme
53+
decoder *Decoder
54+
registry Registry
5455
}
5556

5657
// ensure Webhook implements http.Handler
@@ -119,7 +120,7 @@ func (wh *webhook) handleConvertRequest(ctx context.Context, req *apix.Conversio
119120
if err != nil {
120121
return nil, err
121122
}
122-
err = wh.convertObject(src, dst)
123+
err = wh.convertObject(ctx, src, dst)
123124
if err != nil {
124125
return nil, err
125126
}
@@ -137,7 +138,7 @@ func (wh *webhook) handleConvertRequest(ctx context.Context, req *apix.Conversio
137138
// convertObject will convert given a src object to dst object.
138139
// Note(droot): couldn't find a way to reduce the cyclomatic complexity under 10
139140
// without compromising readability, so disabling gocyclo linter
140-
func (wh *webhook) convertObject(src, dst runtime.Object) error {
141+
func (wh *webhook) convertObject(ctx context.Context, src, dst runtime.Object) error {
141142
srcGVK := src.GetObjectKind().GroupVersionKind()
142143
dstGVK := dst.GetObjectKind().GroupVersionKind()
143144

@@ -149,6 +150,10 @@ func (wh *webhook) convertObject(src, dst runtime.Object) error {
149150
return fmt.Errorf("conversion is not allowed between same type %T", src)
150151
}
151152

153+
if converter, ok := wh.registry.GetConverter(srcGVK.GroupKind()); ok {
154+
return converter.ConvertObject(ctx, src, dst)
155+
}
156+
152157
srcIsHub, dstIsHub := isHub(src), isHub(dst)
153158
srcIsConvertible, dstIsConvertible := isConvertible(src), isConvertible(dst)
154159

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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 conversion
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"strings"
23+
24+
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
"k8s.io/apimachinery/pkg/util/sets"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
29+
)
30+
31+
func MustNewHubSpokeConverter[hubObject runtime.Object](scheme *runtime.Scheme, hub runtime.Object, spokeConverter ...SpokeConverter[hubObject]) Converter {
32+
c, err := NewHubSpokeConverter(scheme, hub, spokeConverter...)
33+
if err != nil {
34+
panic(err)
35+
}
36+
return c
37+
}
38+
39+
func NewHubSpokeConverter[hubObject runtime.Object](scheme *runtime.Scheme, hub runtime.Object, spokeConverter ...SpokeConverter[hubObject]) (Converter, error) {
40+
hubGVK, err := apiutil.GVKForObject(hub, scheme)
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to create hub spoke converter: %w", err)
43+
}
44+
allGVKs, err := objectGVKs(scheme, hub)
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to create hub spoke converter for %s: %w", hubGVK.Kind, err)
47+
}
48+
spokeVersions := sets.New[string]()
49+
for _, gvk := range allGVKs {
50+
if gvk != hubGVK {
51+
spokeVersions.Insert(gvk.Version)
52+
}
53+
}
54+
55+
c := &hubSpokeConverter[hubObject]{
56+
scheme: scheme,
57+
hubGVK: hubGVK,
58+
spokeConverterByGVK: map[schema.GroupVersionKind]SpokeConverter[hubObject]{},
59+
}
60+
61+
spokeConverterVersions := sets.New[string]()
62+
for _, sc := range spokeConverter {
63+
spokeGVK, err := apiutil.GVKForObject(sc.GetSpoke(), scheme)
64+
if err != nil {
65+
return nil, err
66+
}
67+
if hubGVK.GroupKind() != spokeGVK.GroupKind() {
68+
return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+
69+
"spoke converter GroupKind %s does not match hub GroupKind %s",
70+
hubGVK.Kind, spokeGVK.GroupKind(), hubGVK.GroupKind())
71+
}
72+
73+
if _, ok := c.spokeConverterByGVK[spokeGVK]; ok {
74+
return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+
75+
"duplicate spoke converter for version %s",
76+
hubGVK.Kind, spokeGVK.Version)
77+
}
78+
c.spokeConverterByGVK[spokeGVK] = sc
79+
spokeConverterVersions.Insert(spokeGVK.Version)
80+
}
81+
82+
if !spokeConverterVersions.Equal(spokeVersions) {
83+
return nil, fmt.Errorf("failed to create hub spoke converter for %s: "+
84+
"expected spoke converter for %s got spoke converter for %s",
85+
hubGVK.Kind, strings.Join(spokeVersions.UnsortedList(), ","), strings.Join(spokeConverterVersions.UnsortedList(), ","))
86+
}
87+
88+
return c, nil
89+
}
90+
91+
type hubSpokeConverter[hubObject runtime.Object] struct {
92+
scheme *runtime.Scheme
93+
hubGVK schema.GroupVersionKind
94+
spokeConverterByGVK map[schema.GroupVersionKind]SpokeConverter[hubObject]
95+
}
96+
97+
func (c hubSpokeConverter[hubObject]) ConvertObject(ctx context.Context, src, dst runtime.Object) error {
98+
srcGVK := src.GetObjectKind().GroupVersionKind()
99+
dstGVK := dst.GetObjectKind().GroupVersionKind()
100+
101+
srcIsHub := c.hubGVK == srcGVK
102+
dstIsHub := c.hubGVK == dstGVK
103+
_, srcIsConvertible := c.spokeConverterByGVK[srcGVK]
104+
_, dstIsConvertible := c.spokeConverterByGVK[dstGVK]
105+
106+
switch {
107+
case srcIsHub && dstIsConvertible:
108+
return c.spokeConverterByGVK[dstGVK].ConvertHubToSpoke(ctx, src.(hubObject), dst)
109+
case dstIsHub && srcIsConvertible:
110+
return c.spokeConverterByGVK[srcGVK].ConvertSpokeToHub(ctx, src, dst.(hubObject))
111+
case srcIsConvertible && dstIsConvertible:
112+
hubGVK := c.hubGVK
113+
hub, err := c.scheme.New(hubGVK)
114+
if err != nil {
115+
return fmt.Errorf("failed to allocate an instance for gvk %v: %w", hubGVK, err)
116+
}
117+
if err := c.spokeConverterByGVK[srcGVK].ConvertSpokeToHub(ctx, src, hub.(hubObject)); err != nil {
118+
return fmt.Errorf("%T failed to convert to hub version %T : %w", src, hub, err)
119+
}
120+
if err := c.spokeConverterByGVK[dstGVK].ConvertHubToSpoke(ctx, hub.(hubObject), dst); err != nil {
121+
return fmt.Errorf("%T failed to convert from hub version %T : %w", dst, hub, err)
122+
}
123+
}
124+
return fmt.Errorf("%T is not convertible to %T", src, dst)
125+
}
126+
127+
type SpokeConverter[hubObject runtime.Object] interface {
128+
GetSpoke() runtime.Object
129+
ConvertHubToSpoke(ctx context.Context, hub hubObject, spoke runtime.Object) error
130+
ConvertSpokeToHub(ctx context.Context, spoke runtime.Object, hub hubObject) error
131+
}
132+
133+
func NewSpokeConverter[hubObject, spokeObject client.Object](
134+
spoke spokeObject,
135+
convertHubToSpokeFunc func(ctx context.Context, src hubObject, dst spokeObject) error,
136+
convertSpokeToHubFunc func(ctx context.Context, src spokeObject, dst hubObject) error,
137+
) SpokeConverter[hubObject] {
138+
return &spokeConverter[hubObject, spokeObject]{
139+
spoke: spoke,
140+
convertSpokeToHubFunc: convertSpokeToHubFunc,
141+
convertHubToSpokeFunc: convertHubToSpokeFunc,
142+
}
143+
}
144+
145+
type spokeConverter[hubObject, spokeObject runtime.Object] struct {
146+
spoke spokeObject
147+
convertHubToSpokeFunc func(ctx context.Context, src hubObject, dst spokeObject) error
148+
convertSpokeToHubFunc func(ctx context.Context, src spokeObject, dst hubObject) error
149+
}
150+
151+
func (c spokeConverter[hubObject, spokeObject]) GetSpoke() runtime.Object {
152+
return c.spoke
153+
}
154+
155+
func (c spokeConverter[hubObject, spokeObject]) ConvertHubToSpoke(ctx context.Context, hub hubObject, spoke runtime.Object) error {
156+
return c.convertHubToSpokeFunc(ctx, hub, spoke.(spokeObject))
157+
}
158+
159+
func (c spokeConverter[hubObject, spokeObject]) ConvertSpokeToHub(ctx context.Context, spoke runtime.Object, hub hubObject) error {
160+
return c.convertSpokeToHubFunc(ctx, spoke.(spokeObject), hub)
161+
}

0 commit comments

Comments
 (0)