Skip to content

Commit e5b15c0

Browse files
committed
Allow implementation of conversion outside of API packages
1 parent 655fb2c commit e5b15c0

File tree

5 files changed

+168
-14
lines changed

5 files changed

+168
-14
lines changed

pkg/builder/webhook.go

Lines changed: 25 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,12 @@ func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator)
8688
return blder
8789
}
8890

91+
// WithConverters .
92+
func (blder *WebhookBuilder) WithConverters(converter ...conversion.Converter) *WebhookBuilder {
93+
blder.converter = converter
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,18 +295,26 @@ 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 len(blder.converter) > 0 {
299+
if err := blder.mgr.GetConverterRegistry().Register(blder.gvk, blder.converter...); err != nil {
300+
return fmt.Errorf("failed to register converter for %s: %w", blder.gvk.Kind, err)
301+
}
302+
} else {
303+
ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType)
304+
if err != nil {
305+
log.Error(err, "conversion check failed", "GVK", blder.gvk)
306+
return err
307+
}
308+
if !ok {
309+
return nil
298310
}
299-
log.Info("Conversion webhook enabled", "GVK", blder.gvk)
300311
}
301312

313+
if !blder.isAlreadyHandled("/convert") {
314+
blder.mgr.GetWebhookServer().Register("/convert", conversion.NewWebhookHandler(blder.mgr.GetScheme(), blder.mgr.GetConverterRegistry()))
315+
}
316+
log.Info("Conversion webhook enabled", "GVK", blder.gvk)
317+
302318
return nil
303319
}
304320

pkg/manager/internal.go

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

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

133+
conversionRegistry conversion.Registry
134+
132135
// leaderElectionID is the name of the resource that leader election
133136
// will use for holding the leader lock.
134137
leaderElectionID string
@@ -279,6 +282,10 @@ func (cm *controllerManager) GetWebhookServer() webhook.Server {
279282
return cm.webhookServer
280283
}
281284

285+
func (cm *controllerManager) GetConverterRegistry() conversion.Registry {
286+
return cm.conversionRegistry
287+
}
288+
282289
func (cm *controllerManager) GetLogger() logr.Logger {
283290
return cm.logger
284291
}

pkg/manager/manager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"k8s.io/client-go/tools/record"
3535
"k8s.io/utils/ptr"
3636
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
37+
"sigs.k8s.io/controller-runtime/pkg/webhook/conversion"
3738

3839
"sigs.k8s.io/controller-runtime/pkg/cache"
3940
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -95,6 +96,9 @@ type Manager interface {
9596

9697
// GetControllerOptions returns controller global configuration options.
9798
GetControllerOptions() config.Controller
99+
100+
// GetConverterRegistry FIXME.
101+
GetConverterRegistry() conversion.Registry
98102
}
99103

100104
// Options are the arguments for creating a new Manager.
@@ -445,6 +449,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
445449
logger: options.Logger,
446450
elected: make(chan struct{}),
447451
webhookServer: options.WebhookServer,
452+
conversionRegistry: conversion.NewRegistry(cluster.GetScheme()),
448453
leaderElectionID: options.LeaderElectionID,
449454
leaseDuration: *options.LeaseDuration,
450455
renewDeadline: *options.RenewDeadline,

pkg/webhook/conversion/conversion.go

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import (
3434
"k8s.io/apimachinery/pkg/runtime"
3535
"k8s.io/apimachinery/pkg/runtime/schema"
3636
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
37+
"sigs.k8s.io/controller-runtime/pkg/client"
38+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
3739
"sigs.k8s.io/controller-runtime/pkg/conversion"
3840
logf "sigs.k8s.io/controller-runtime/pkg/log"
3941
conversionmetrics "sigs.k8s.io/controller-runtime/pkg/webhook/conversion/metrics"
@@ -43,14 +45,110 @@ var (
4345
log = logf.Log.WithName("conversion-webhook")
4446
)
4547

46-
func NewWebhookHandler(scheme *runtime.Scheme) http.Handler {
47-
return &webhook{scheme: scheme, decoder: NewDecoder(scheme)}
48+
type Registry struct {
49+
scheme *runtime.Scheme
50+
convertersByHubGK map[schema.GroupKind]convertersForHub
51+
}
52+
53+
func NewRegistry(scheme *runtime.Scheme) Registry {
54+
return Registry{
55+
scheme: scheme,
56+
convertersByHubGK: map[schema.GroupKind]convertersForHub{},
57+
}
58+
}
59+
60+
type convertersForHub struct {
61+
hubGVK schema.GroupVersionKind
62+
convertersBySpokeGVK map[schema.GroupVersionKind]Converter
63+
}
64+
65+
func (r Registry) Register(hubGVK schema.GroupVersionKind, converters ...Converter) error {
66+
if _, ok := r.convertersByHubGK[hubGVK.GroupKind()]; ok {
67+
return fmt.Errorf("converter already registered for %s", hubGVK.GroupKind())
68+
}
69+
70+
// TODO: validate against schema that all converters have been registred for a type (similar to previous validation)
71+
72+
r.convertersByHubGK[hubGVK.GroupKind()] = convertersForHub{
73+
hubGVK: hubGVK,
74+
convertersBySpokeGVK: map[schema.GroupVersionKind]Converter{},
75+
}
76+
for _, converter := range converters {
77+
converterHubGVK, err := apiutil.GVKForObject(converter.GetHub(), r.scheme)
78+
if err != nil {
79+
return err
80+
}
81+
if hubGVK != converterHubGVK {
82+
return fmt.Errorf("converter GVK does not match builder gvk: FIXME")
83+
}
84+
converterSpokeGVK, err := apiutil.GVKForObject(converter.GetSpoke(), r.scheme)
85+
if err != nil {
86+
return err
87+
}
88+
if hubGVK.GroupKind() != converterSpokeGVK.GroupKind() {
89+
return fmt.Errorf("converter GVK does not match builder gvk: FIXME")
90+
}
91+
r.convertersByHubGK[hubGVK.GroupKind()].convertersBySpokeGVK[converterSpokeGVK] = converter
92+
}
93+
94+
return nil
95+
}
96+
97+
type Converter interface {
98+
GetHub() client.Object
99+
GetSpoke() client.Object
100+
ConvertHubToSpoke(hub, spoke runtime.Object) error
101+
ConvertSpokeToHub(hub, spoke runtime.Object) error
102+
}
103+
104+
func NewConverter[hubObject, spokeObject client.Object](
105+
hub hubObject,
106+
spoke spokeObject,
107+
convertHubToSpokeFunc func(src hubObject, dst spokeObject) error,
108+
convertSpokeToHubFunc func(src spokeObject, dst hubObject) error,
109+
) Converter {
110+
return &converter[hubObject, spokeObject]{
111+
hub: hub,
112+
spoke: spoke,
113+
convertSpokeToHubFunc: convertSpokeToHubFunc,
114+
convertHubToSpokeFunc: convertHubToSpokeFunc,
115+
}
116+
}
117+
118+
var _ Converter = converter[client.Object, client.Object]{}
119+
120+
type converter[hubObject, spokeObject client.Object] struct {
121+
hub hubObject
122+
spoke spokeObject
123+
convertHubToSpokeFunc func(src hubObject, dst spokeObject) error
124+
convertSpokeToHubFunc func(src spokeObject, dst hubObject) error
125+
}
126+
127+
func (c converter[hubObject, spokeObject]) GetHub() client.Object {
128+
return c.hub
129+
}
130+
131+
func (c converter[hubObject, spokeObject]) GetSpoke() client.Object {
132+
return c.spoke
133+
}
134+
135+
func (c converter[hubObject, spokeObject]) ConvertHubToSpoke(hub, spoke runtime.Object) error {
136+
return c.convertHubToSpokeFunc(hub.(hubObject), spoke.(spokeObject))
137+
}
138+
139+
func (c converter[hubObject, spokeObject]) ConvertSpokeToHub(hub, spoke runtime.Object) error {
140+
return c.convertSpokeToHubFunc(spoke.(spokeObject), hub.(hubObject))
141+
}
142+
143+
func NewWebhookHandler(scheme *runtime.Scheme, registry Registry) http.Handler {
144+
return &webhook{scheme: scheme, decoder: NewDecoder(scheme), registry: registry}
48145
}
49146

50147
// webhook implements a CRD conversion webhook HTTP handler.
51148
type webhook struct {
52-
scheme *runtime.Scheme
53-
decoder *Decoder
149+
scheme *runtime.Scheme
150+
decoder *Decoder
151+
registry Registry
54152
}
55153

56154
// ensure Webhook implements http.Handler
@@ -149,6 +247,34 @@ func (wh *webhook) convertObject(src, dst runtime.Object) error {
149247
return fmt.Errorf("conversion is not allowed between same type %T", src)
150248
}
151249

250+
if converters, ok := wh.registry.convertersByHubGK[srcGVK.GroupKind()]; ok {
251+
srcIsHub := converters.hubGVK == srcGVK
252+
dstIsHub := converters.hubGVK == dstGVK
253+
_, srcIsConvertible := converters.convertersBySpokeGVK[srcGVK]
254+
_, dstIsConvertible := converters.convertersBySpokeGVK[dstGVK]
255+
256+
switch {
257+
case srcIsHub && dstIsConvertible:
258+
return converters.convertersBySpokeGVK[dstGVK].ConvertHubToSpoke(src, dst)
259+
case dstIsHub && srcIsConvertible:
260+
return converters.convertersBySpokeGVK[srcGVK].ConvertSpokeToHub(src, dst)
261+
case srcIsConvertible && dstIsConvertible:
262+
hubGVK := converters.hubGVK
263+
hub, err := wh.scheme.New(hubGVK)
264+
if err != nil {
265+
return fmt.Errorf("failed to allocate an instance for gvk %v: %w", hubGVK, err)
266+
}
267+
if err := converters.convertersBySpokeGVK[srcGVK].ConvertSpokeToHub(src, hub); err != nil {
268+
return fmt.Errorf("%T failed to convert to hub version %T : %w", src, hub, err)
269+
}
270+
if err := converters.convertersBySpokeGVK[dstGVK].ConvertHubToSpoke(hub, dst); err != nil {
271+
return fmt.Errorf("%T failed to convert from hub version %T : %w", dst, hub, err)
272+
}
273+
default:
274+
return fmt.Errorf("%T is not convertible to %T", src, dst)
275+
}
276+
}
277+
152278
srcIsHub, dstIsHub := isHub(src), isHub(dst)
153279
srcIsConvertible, dstIsConvertible := isConvertible(src), isConvertible(dst)
154280

pkg/webhook/conversion/conversion_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ var _ = Describe("Conversion Webhook", func() {
5858
Expect(jobsv3.AddToScheme(scheme)).To(Succeed())
5959

6060
decoder = conversion.NewDecoder(scheme)
61-
wh = conversion.NewWebhookHandler(scheme)
61+
wh = conversion.NewWebhookHandler(scheme, conversion.NewRegistry(scheme))
6262
})
6363

6464
doRequest := func(convReq *apix.ConversionReview) *apix.ConversionReview {

0 commit comments

Comments
 (0)