Skip to content

Commit 8846a37

Browse files
committed
Allow implementation of conversion outside of API packages
1 parent b8f1137 commit 8846a37

File tree

8 files changed

+621
-249
lines changed

8 files changed

+621
-249
lines changed

pkg/builder/webhook.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type WebhookBuilder struct {
4545
customPath string
4646
customValidatorCustomPath string
4747
customDefaulterCustomPath string
48+
converterConstructor func(*runtime.Scheme) (conversion.Converter, error)
4849
gvk schema.GroupVersionKind
4950
mgr manager.Manager
5051
config *rest.Config
@@ -86,6 +87,13 @@ func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator)
8687
return blder
8788
}
8889

90+
// WithConverter takes a func that constructs a converter.Converter.
91+
// The Converter will then be used by the conversion endpoint for the type passed into For().
92+
func (blder *WebhookBuilder) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder {
93+
blder.converterConstructor = converterConstructor
94+
return blder
95+
}
96+
8997
// WithLogConstructor overrides the webhook's LogConstructor.
9098
func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder {
9199
blder.logConstructor = logConstructor
@@ -287,17 +295,30 @@ func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook {
287295
}
288296

289297
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()))
298+
if blder.converterConstructor != nil {
299+
converter, err := blder.converterConstructor(blder.mgr.GetScheme())
300+
if err != nil {
301+
return err
298302
}
299-
log.Info("Conversion webhook enabled", "GVK", blder.gvk)
303+
304+
if err := blder.mgr.GetConverterRegistry().RegisterConverter(blder.gvk.GroupKind(), converter); err != nil {
305+
return err
306+
}
307+
} else {
308+
ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType)
309+
if err != nil {
310+
log.Error(err, "conversion check failed", "GVK", blder.gvk)
311+
return err
312+
}
313+
if !ok {
314+
return nil
315+
}
316+
}
317+
318+
if !blder.isAlreadyHandled("/convert") {
319+
blder.mgr.GetWebhookServer().Register("/convert", conversion.NewWebhookHandler(blder.mgr.GetScheme(), blder.mgr.GetConverterRegistry()))
300320
}
321+
log.Info("Conversion webhook enabled", "GVK", blder.gvk)
301322

302323
return nil
303324
}

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+
// converterRegistry stores conversion.Converter for the conversion endpoint.
135+
converterRegistry 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) GetConverterRegistry() conversion.Registry {
292+
return cm.converterRegistry
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.GetConverterRegistry())
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"k8s.io/client-go/tools/record"
3737
"k8s.io/utils/ptr"
3838
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
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"
@@ -97,6 +98,10 @@ type Manager interface {
9798

9899
// GetControllerOptions returns controller global configuration options.
99100
GetControllerOptions() config.Controller
101+
102+
// GetConverterRegistry returns the converter registry that is used to store conversion.Converter
103+
// for the conversion endpoint.
104+
GetConverterRegistry() conversion.Registry
100105
}
101106

102107
// Options are the arguments for creating a new Manager.
@@ -450,6 +455,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
450455
logger: options.Logger,
451456
elected: make(chan struct{}),
452457
webhookServer: options.WebhookServer,
458+
converterRegistry: conversion.NewRegistry(),
453459
leaderElectionID: options.LeaderElectionID,
454460
leaseDuration: *options.LeaseDuration,
455461
renewDeadline: *options.RenewDeadline,

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

0 commit comments

Comments
 (0)