Skip to content

Conversation

@sbueringer
Copy link
Member

@sbueringer sbueringer commented Oct 4, 2025

Goal is to fix the issue described in #3130

I.e. making it possible to implement conversion without introducing a dependency to controller-runtime in API packages.

Here is an example how this can be used to move ConvertTo/ConvertFrom funcs out of the API package: https://github.com/kubernetes-sigs/cluster-api/pull/12820/files#diff-f6619b6bca7abdc3fe36434e753669ddefe9cf8c9cf85278ff815d683a9cde45

Note: As conversion functions now don't have to import controller-runtime anymore they could also be kept in the API package.

@k8s-ci-robot k8s-ci-robot added do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Oct 4, 2025
@k8s-ci-robot k8s-ci-robot added approved Indicates a PR has been approved by an approver from all required OWNERS files. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Oct 4, 2025
@sbueringer sbueringer force-pushed the pr-conversion branch 2 times, most recently from e5b15c0 to ae26fcc Compare October 4, 2025 14:47
@sbueringer sbueringer changed the title [WIP] [POC] ✨ Allow implementation of conversion outside of API packages [WIP] ✨ Allow implementation of conversion outside of API packages Oct 4, 2025
ConvertSpokeToHub(hub, spoke runtime.Object) error
}

func NewConverter[hubObject, spokeObject client.Object](
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me. Shouldn't this take a slice of ConvertToHub(spoke runtime.Object)(hub runtime.object, _ error)? Right now, this can only be used if there are exactly to versions (and in that context, the concept of hub and spoke doesn't really make too much sense)

Copy link
Member Author

@sbueringer sbueringer Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some general context. Current state of conversion:

  • Hub has to be implemented on the hup API type
  • ConvertTo / ConvertFrom has to be implemented on all spoke API types (there's validation for that in CR)
  • So overall the following is needed (example from CAPI):
    • v1beta2 API package:
      • func (*Cluster) Hub() {}
    • v1alpha3 / v1alpha4 / v1beta1 API packages:
      • func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error
      • func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error

My main goals are:

  • Be able to implement ConvertTo/ConvertFrom outside of API packages
  • Of course accordingly ConvertTo/ConvertFrom can't be methods anymore
  • Because they are not methods anymore I need a new way to register the funcs (but I still want to be able to verify that all necessary conversions have been provided)
  • Make the conversion funcs more type-safe:
    • Today:
      • func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error
      • func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error
    • With this PR: (func names don't matter, only the parameter)
      • func ConvertClusterV1Beta1ToHub(src *clusterv1beta1.Cluster, dst *clusterv1.Cluster) error
      • func ConvertClusterHubToV1Beta1(src *clusterv1.Cluster, dst *clusterv1beta1.Cluster) error
  • Minimal migration effort for folks that already implemented ConvertTo/ConvertFrom methods and want to move to the new model

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think generics are a nice way to make the conversion funcs more type safe (similar to how this works with source.Kind, e.g.: https://github.com/kubernetes-sigs/cluster-api/blob/a9fbe115c8adf0de2c6a27f4f436ccdf657aa884/internal/controllers/clusterresourceset/clusterresourceset_controller.go#L100)

The webhook builder itself cannot become generic with type parameters for the hub type and an arbitrary amount of spoke types. So I thought I'll use an additioanl type that takes care of the generics and then implements an interface that allows to pass it into the builder and use it later.

A concrete example how this can be used based on Cluster API:

	return ctrl.NewWebhookManagedBy(mgr).
		For(&clusterv1.Cluster{}).
		WithDefaulter(webhook).
		WithValidator(webhook).
		WithConverters(
			conversion.NewConverter(&clusterv1.Cluster{}, &clusterv1beta1.Cluster{}, ConvertClusterHubToV1Beta1, ConvertClusterV1Beta1ToHub),
			conversion.NewConverter(&clusterv1.Cluster{}, &clusterv1alpha4.Cluster{}, ConvertClusterHubToV1Alpha4, ConvertClusterV1Alpha4ToHub),
			conversion.NewConverter(&clusterv1.Cluster{}, &clusterv1alpha3.Cluster{}, ConvertClusterHubToV1Alpha3, ConvertClusterV1Alpha3ToHub),
		).
		Complete()
  • The generic NewConverter func provides a conversion between the hub and one spoke type
  • The hubObject and spokeObject parameter ensure:
    • WithConverters can validate that all necessary conversions have been provided (not fully implemented yet in the PR, but it can check hub and spoke types against the type of the webhook + all GK's registered in the scheme)
    • that the passed in conversion functions have the right type parameters

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this take a slice of ConvertToHub(spoke runtime.Object)(hub runtime.object, _ error)?

It could, but it would be less type-safe (side-note: I would prefer to keep hub an input parameter, so that CR stays responible for creating an instance of the hub type).

Right now, this can only be used if there are exactly two versions (and in that context, the concept of hub and spoke doesn't really make too much sense)

No, see example one comment above. NewConverter would be just for one hub-spoke conversion. Users have to pass in all the necessary conversions (similar to how they previously had to implement ConvertTo/ConvertFrom on all spoke types)

return nil
}

type Converter interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this should be how it works internally, why do we need to have the concept of hub and spoke visible in the external interface and the webhook be aware of it? All it really wants is a convert(from, to runtime.Object) error.

Also without really having context on our current conversion machinery, why don't we use the scheme, it allows to register and call conversion funcs (which internally can be built on a hub and spoke system, but that is nothing the scheme or anything else that wants to convert really cares about)

Copy link
Member Author

@sbueringer sbueringer Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this should be how it works internally, why do we need to have the concept of hub and spoke visible in the external interface and the webhook be aware of it? All it really wants is a convert(from, to runtime.Object) error.

We could also say just take a convert(from, to runtime.Object) func, but then we have no validation on our side and users have to implement the logic in our convertObject func on there side. I would prefer if users still only have to implement ~ the ConvertTo/ConvertFrom funcs for all their hub-spoke combinations and we take care of the rest.

Also without really having context on our current conversion machinery, why don't we use the scheme, it allows to register and call conversion funcs (which internally can be built on a hub and spoke system, but that is nothing the scheme or anything else that wants to convert really cares about)

Today conversions with CR work the following way:

I would prefer if we could avoid mixing these two layers by putting all of these funcs into the scheme.
I also just realized that we should start passing context.Context into the ConvertTo/ConvertFrom funcs, that would not be possible with the scheme (it only takes type ConversionFunc func(a, b interface{}, scope Scope) error funcs)

If I understand correctly if we would want to delegate the conversion entirely to the scheme we would have to register conversion funcs for all combinations in the scheme, e.g.

  • v1beta2 <=> v1beta1, v1beta2 <=> v1alpha4, v1beta2 <=> v1alpha3
  • v1beta1 <=> v1alpha4, v1beta1 <=> v1alpha3
  • v1alpha4 <=> v1alpha3

Instead of just:

  • v1beta2 <=> v1beta1, v1beta2 <=> v1alpha4, v1beta2 <=> v1alpha3

I believe that's why the hub-spoke model was implemented as it is today. I would really prefer if users writing conversion code only have to implement the hub-spoke conversions and not conversion funcs for all combinations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if users still only have to implement ~ the ConvertTo/ConvertFrom funcs for all their hub-spoke combinations and we take care of the rest.

I am not disagreeing with that, but IMHO there should be a separation of concerns. The webhook just takes some sort of converter interface that is not bound to the hub/spoke concept and separately, there is a HubSpokeConverter that fullfills that webhook interface. The hub spoke concept doesn't really seem like something the webhook as a user of conversion should be aware of and arguably if someone wanted to optimize for conversion being fast as opposed to minimizing the number of conversions to write, the hub and spoke approach would be the wrong one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a simpler way to phrase my point: This interface and just this interface should not have any trace of the hub/spoke concept. The actual implementation of this interface absolutely can and probably should.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I'll take another look how this could be adjusted accordingly

Copy link
Member Author

@sbueringer sbueringer Oct 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alvaroaleman Please take another look if that goes in the right direction. I think it does. Once it's good from your side, I'll add unit tests and some other smaller improvements (error handling, ...)

Example usage in Cluster API

	return ctrl.NewWebhookManagedBy(mgr).
		For(&clusterv1.Cluster{}).
		WithDefaulter(webhook).
		WithValidator(webhook).
		WithConverter(conversion.MustNewHubSpokeConverter(mgr.GetScheme(), &clusterv1.Cluster{},
			conversion.NewSpokeConverter(&clusterv1beta1.Cluster{}, ConvertClusterHubToV1Beta1, ConvertClusterV1Beta1ToHub),
			conversion.NewSpokeConverter(&clusterv1alpha4.Cluster{}, ConvertClusterHubToV1Alpha4, ConvertClusterV1Alpha4ToHub),
			conversion.NewSpokeConverter(&clusterv1alpha3.Cluster{}, ConvertClusterHubToV1Alpha3, ConvertClusterV1Alpha3ToHub),
		)).
		Complete()

(I know I could avoid having to specify the spoke type in NewSpokeConverter and maybe even the hub type in MustNewHubSpokeConverter but I think it's better to be explicit like that, compared to inferring the generic types only from the conversion funcs)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I like the new interfaces 👍 To me the explicit type in the NewSpokeConverter seems somewhat redundant especially since it has to match the one from the conversion func but its not a super strong opinion.

Maybe we could avoid the MustNewHubSpokeConverter by taking a func(converter.Converter, error) in the builder? Again, no super strong opinion

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me the explicit type in the NewSpokeConverter seems somewhat redundant especially since it has to match the one from the conversion func but its not a super strong opinion.

Yup, it is redundant, but I prefer to be explicit here.

Maybe we could avoid the MustNewHubSpokeConverter by taking a func(converter.Converter, error) in the builder? Again, no super strong opinion

Good idea, done

@k8s-ci-robot k8s-ci-robot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Oct 19, 2025
@sbueringer sbueringer changed the title [WIP] ✨ Allow implementation of conversion outside of API packages ✨ Allow implementation of conversion outside of API packages Oct 19, 2025
@k8s-ci-robot k8s-ci-robot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 19, 2025
@k8s-ci-robot
Copy link
Contributor

@sbueringer: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-controller-runtime-apidiff 8846a37 link false /test pull-controller-runtime-apidiff

Full PR test history. Your PR dashboard. Please help us cut down on flakes by linking to an open issue when you hit one in your PR.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@sbueringer
Copy link
Member Author

sbueringer commented Oct 19, 2025

@alvaroaleman Should be ready for a full review now

Copy link
Member

@alvaroaleman alvaroaleman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Oct 20, 2025
@k8s-ci-robot
Copy link
Contributor

LGTM label has been added.

Git tree hash: 04fe4eb0a6eb9f98c623398f472c4fc9b83436e6

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: alvaroaleman, sbueringer

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:
  • OWNERS [alvaroaleman,sbueringer]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@alvaroaleman
Copy link
Member

/hold

@k8s-ci-robot k8s-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 20, 2025
@sbueringer sbueringer changed the title ✨ Allow implementation of conversion outside of API packages ⚠ Allow implementation of conversion outside of API packages Oct 20, 2025
@sbueringer
Copy link
Member Author

/hold cancel

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 20, 2025
@k8s-ci-robot k8s-ci-robot merged commit e22eab0 into kubernetes-sigs:main Oct 20, 2025
18 of 19 checks passed
@sbueringer
Copy link
Member Author

sbueringer commented Oct 20, 2025

cc @camilamacedo86 there are no best practices around how exactly this should be used (regarding package structure). But this is the preferred way to implement conversion going forward as it allows to avoid controller-runtime dependencies in API packages

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants