-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
⚠️ Source, Event, Predicate, Handler: Add generics support #2783
⚠️ Source, Event, Predicate, Handler: Add generics support #2783
Conversation
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: alvaroaleman 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:
Approvers can indicate their approval by writing |
Just did a first high-level review. Overall pretty nice! |
e9c3ab9
to
a0359e5
Compare
type UpdateEvent struct { | ||
// TypedUpdateEvent is an event where a Kubernetes object was updated. TypedUpdateEvent should be generated | ||
// by a source.Source and transformed into a reconcile.Request by an handler.TypedEventHandler. | ||
type TypedUpdateEvent[T any] struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me that having a typed EventHandler here is not as convenient as to have a method which will infer the type of the object. Client-go works with 2 interfaces under the hood, passed in a method if I’m not mistaken. Can this be implemented this way but with generics?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean a constructor for events and unexporting the current types? That would be a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather leave Event structures for non generic types without any change, and introduce a new handler for generic types. I did it this way: https://github.com/Danil-Grigorev/controller-runtime/blob/4d770047de3a10110f5ded3bce46c178d68d0500/pkg/handler/eventhandler.go#L61-L73
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the advantage of that? That means sources have to be able to deal with either or or there needs to be some sort of adapter to convert, both of which is very inconvenient. How does this improve the overall UX?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly for the ability to infer type, I’d say. Anytime that you specify [anything]
before the method, the syntax highlighting breaks. Might be only my personal observation, env, etc. The goal is that as controller-runtime is a library, exposing a list of interfaces to extend (in case you need this), having one which unconditionally requires to use generics might make it hard for newcomers to implement custom logic. With a method one can always use just *corev1.Pod
as an arg to custom EventHandler implementation and won’t have to use advanced features immediately. And wrapping EventHandle into constructors to eliminate the issue didn’t make sense to me. I can’t imagine how a structure for events may grow beyond just hosting old/new objects, which is low enough to consider a method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal is that as controller-runtime is a library, exposing a list of interfaces to extend (in case you need this), having one which unconditionally requires to use generics might make it hard for newcomers to implement custom logic.
Both your and my approach leave the currently-exported interfaces in place, they have the same shape in both approaches. The difference in your approach is that because EventHandler
is not an alias to TypedEventHandler
, a Source
can only support one of the two, because for the go type system those are different types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m trying to find a reason why I abandoned aliasing in the first place. Yes, it seems to be working, and having to specify a typedEvent[*Pod] is a minor inconvenience. My guess would be that the issue discussion lead to overcomplicating the solution. But I see a difference with using Source().Prepare()
that the current builder logic was not refactored in my implementation. This means there will not be issues with existing functionality
04bc2a6
to
b71b256
Compare
pkg/handler/enqueue_mapped.go
Outdated
// | ||
// For TypedUpdateEvents which contain both a new and old object, the transformation function is run on both | ||
// objects and both sets of Requests are enqueue. | ||
func EnqueueRequestsFromTypedMapFunc[T any](fn TypedMapFunc[T]) TypedEventHandler[T] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is one problem as I can see. There should be an adapter method for EventHandler
for a non-generic controller using type specific generic map method. Something like:
func EnqueueRequestsFromTypedMapFunc[T client.Object](fn TypedMapFunc[T]) EventHandler
The problem now is that EventHandler = TypedEventHandler[client.Object]
and you can’t type cast from one generic type to another, regardless of the fact that both implement client.Object
. That’s where the simpler form of type aliasing broke for me, and where this (obviously simpler) design is a breaking change. Sometimes you can’t pair generics with a project due to reasons like the one we are facing - using builder or any sort of nested structures. Go generics just don’t work where there is a type ambiguity. But using generics should not be either/or decision either, it just needs a way to make a split at some point where it no longer works or does not make sense, I think.
As for real world example where this is important - I rebased CAPI testing PR on your fork + new builder methods, all works well except one common method. It is the only blocker for it to compile and work, but it is a deal breaker if this has to be continued (cc @sbueringer):
return r.Tracker.Watch(ctx, remote.WatchInput{
Name: "machinepool-watchNodes",
Cluster: util.ObjectKey(cluster),
Watcher: &controller.ControllerAdapter{Controller: r.controller},
Kind: &corev1.Node{},
EventHandler: handler.EnqueueRequestsFromMapFunc(r.nodeToMachinePool), // Does not compile because expects TypedMapFunc[client.Object] and gets TypedMapFunc[*corev1.Node]
})
func (r *MachinePoolReconciler) nodeToMachinePool(ctx context.Context, node *corev1.Node) []reconcile.Request {
And there is no workaround except for having TypedEventHandler
and EventHandler
being separate interfaces with different methods working on client.Object
and T
simultaneously. Here is how I did it originally. I didn’t see a point in having TypedEvent
structure either, as a simplification. Another user story for this - one less controller-runtime
dependency for reusable business logic around event mapping exported in a package at the API level.
Edit: probably not that definite as I thought in CAPI scenario, as methods defined there for tracker are only used with remote tracker, so can still use client.Object
. Still I think this is a limitation worth knowing about/decided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cluster-API was also bumped here and it doesn't look like the problem you are describing happened there: kubernetes-sigs/cluster-api#10460
The problem now is that EventHandler = TypedEventHandler[client.Object] and you can’t type cast from one generic type to another,
You can not type cast but you can use them interchangably. I still do not quite understand what this breaks that your approach doesn't. I am not very familiar with cluster-api, can you give a simpler example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cluster-API was also bumped here and it doesn't look like the problem you are describing happened there: kubernetes-sigs/cluster-api#10460
I think this didn't happen here because Christian rebased on this PR, and not on "your fork + new builder methods". Otherwise I don't follow, to be honest :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Danil-Grigorev Can you provide more details how this func is broken? (and push a branch somewhere where it is broken)
(I looked at kubernetes-sigs/cluster-api#10464 and that one is not broken, but I assume you're describing some other version of the code)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One detail that maybe is what you are referring to but I am not sure: It makes a difference if you do
type foo = bar[something]
or
type foo bar[something]
The latter means that you do not get the methods of bar
and as a result, foo
can not be used in place of bar[something]
But yeah, I also don't really understand the example given so I don't know if this might be what you mean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, as I mentioned, I just had to stick with non generic nodeToMachinePool
method which kept using client.Object
and that probably it wasn’t the right example, because nodeToMachinePool
is only used once - in the tracker setup.
My concern is about more complex scenarios, when the same method has to be used with generic implementation elsewhere. It can’t be done without an adapter or code duplication purely for type casting. Which might lead to decision against using generics due to added complexity in having use methods interchangeably.
The example I provided is good in outlining the second problem related to golang/go#49085 as in builder pattern UX changes. We can’t make Tracker
a generic method with specialization on *corev1.Node
for the CAPI use case to make it simple, because the Tracker
is stored in MachinePoolReconciler
which works with MachinePools
- it would look strange and may block MachinePoolReconciler
from being a generic method on MachinePoolReconciler[*MachinePool]
for example if needed.
This is theoretical, I’m struggling to find a real world example which would explain the point better, still looking though. I think controller-runtime
should take a couple more steps further to make the library UX better aligned with generics, despite the golang/go#49085 limitation, and get the design of how it looks in the end with possible edge scenarios covered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only case I am aware of where in type foo = bar[something]
foo
and bar
can not be used interchangably are slices: a []foo
can not be used in place of []bar[something]
but you can convert between them by iterating
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is theoretical, I’m struggling to find a real world example which would explain the point better, still looking though. I think controller-runtime should take a couple more steps further to make the library UX better aligned with generics, despite the golang/go#49085 limitation, and get the design of how it looks in the end with possible edge scenarios covered.
I am not disagreeing. But I do not think that your approach is better at that than the aliasing. The only thing that you added is the func builder.For(stuff) source.Source
/func builder.Owns(stuff) source.Source
. I think the only case where this might be useful is if you have predicates, as you can have proper types in there as the Handler
is implicit and I am not fully convinced that warrants including such helpers in controller-runtime.
That being said, this change does nothing that would keep us from in the future adding such helpers or improving the UX further if golang/go#49085 gets resolved or we think of other ways to work around it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me. Tried this once again and this approach seems to work:
func convert[T any](fn TypedMapFunc[T]) MapFunc {
return func(ctx context.Context, obj client.Object) (req []reconcile.Request) {
if typed, ok := obj.(T); !ok {
return req
} else {
return fn(ctx, typed)
}
}
}
func EnqueueRequestsFromTypedMapFunc[T any](fn TypedMapFunc[T]) EventHandler {
return &enqueueRequestsFromMapFunc[client.Object]{
toRequests: convert(fn),
}
}
b71b256
to
a7917e0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good
a7917e0
to
b49b1bf
Compare
return err | ||
} | ||
srcKind.Type = typeForSrc | ||
projected, err := blder.project(w.obj, w.objectProjection) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Projection might not work for different sources.
This is only used to construct a source.Kind.
I disagree. Both what we did previously and what is done over there is IMHO hacky and will not work after the Source was started. Deferring the creation and creating it with the right args rather than manipulating it later on is IMHO much cleaner.
@@ -43,19 +43,19 @@ func NewEventHandler(ctx context.Context, queue workqueue.RateLimitingInterface, | |||
} | |||
|
|||
// EventHandler adapts a handler.EventHandler interface to a cache.ResourceEventHandler interface. | |||
type EventHandler struct { | |||
type EventHandler[T client.Object] struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can also use EventHandler[T any]
without changes to allow watching non-kubernetes resources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is internal and only used by source.Kind
which only works with client.Object
, so there is no reason to have a different type constraint here
Breaking changes are in: kubernetes-sigs/controller-runtime#2783
* Bump sigs.k8s.io/controller-runtime from 0.17.3 to 0.18.4 Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.17.3 to 0.18.4. - [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases) - [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md) - [Commits](kubernetes-sigs/controller-runtime@v0.17.3...v0.18.4) --- updated-dependencies: - dependency-name: sigs.k8s.io/controller-runtime dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> * Bump k8s, drop support for go 1.21 * Fix build for controller-runtime breaking changes Breaking changes are in: kubernetes-sigs/controller-runtime#2783 --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joel Takvorian <[email protected]>
- implement GetOperatorStateWithQuorum() for v1helpers.OperatorClient interface which was added in openshift/library-go here [0] - applyconfigv1.NetworkStatus().WithConditions() now requires metav1.ConditionApplyConfiguration instead of metav1.Condition, which came in with the client-go 1.30 rebase here [1] - using new lib location for feature gates "openshift/api/features" - refactor controller-runtime Watch() calls to handle new generics support [2] - moved RotatedSigningCASecret.JiraComponent to RotatedSigningCASecret.AdditionalAnnotations.JiraComponent from here [3] - added dummy path to apimachinery validation.IsValidIP() as it was added as an argument here [4]. not relevant to the the call in this case. - bumped k8s.io/component-base to v0.31.0-alpha.2 to get past a breaking issue with prometheus using a breaking type from 0.30.2 - the version of googleapis/api/expr/v1alpha1 that is brought in with github.com/google/cel-go with anything newer than v0.17.8 no longer includes GetConstExpr() so pinning that lib to v0.17.8 [0] openshift/library-go@bd5e34c [1] openshift/client-go@792100e#diff-233949a4a2a7ca43d091c935b04748464200784e5377366d574945d3fd06ed89R76 [2] kubernetes-sigs/controller-runtime#2783 [3] openshift/library-go@df7ff42 [4] openshift/kubernetes-apimachinery@89b9414 Signed-off-by: Jamo Luhrsen <[email protected]>
- implement GetOperatorStateWithQuorum() for v1helpers.OperatorClient interface which was added in openshift/library-go here [0] - applyconfigv1.NetworkStatus().WithConditions() now requires metav1.ConditionApplyConfiguration instead of metav1.Condition, which came in with the client-go 1.30 rebase here [1] - using new lib location for feature gates "openshift/api/features" - refactor controller-runtime Watch() calls to handle new generics support [2] - moved RotatedSigningCASecret.JiraComponent to RotatedSigningCASecret.AdditionalAnnotations.JiraComponent from here [3] - added dummy path to apimachinery validation.IsValidIP() as it was added as an argument here [4]. not relevant to the the call in this case. - bumped k8s.io/component-base to v0.31.0-alpha.2 to get past a breaking issue with prometheus using a breaking type from 0.30.2 - the version of googleapis/api/expr/v1alpha1 that is brought in with github.com/google/cel-go with anything newer than v0.17.8 no longer includes GetConstExpr() so pinning that lib to v0.17.8 [0] openshift/library-go@bd5e34c [1] openshift/client-go@792100e#diff-233949a4a2a7ca43d091c935b04748464200784e5377366d574945d3fd06ed89R76 [2] kubernetes-sigs/controller-runtime#2783 [3] openshift/library-go@df7ff42 [4] openshift/kubernetes-apimachinery@89b9414 Signed-off-by: Jamo Luhrsen <[email protected]>
- implement GetOperatorStateWithQuorum() for v1helpers.OperatorClient interface which was added in openshift/library-go here [0] - applyconfigv1.NetworkStatus().WithConditions() now requires metav1.ConditionApplyConfiguration instead of metav1.Condition, which came in with the client-go 1.30 rebase here [1] - using new lib location for feature gates "openshift/api/features" - refactor controller-runtime Watch() calls to handle new generics support [2] - moved RotatedSigningCASecret.JiraComponent to RotatedSigningCASecret.AdditionalAnnotations.JiraComponent from here [3] - added dummy path to apimachinery validation.IsValidIP() as it was added as an argument here [4]. not relevant to the the call in this case. - bumped k8s.io/component-base to v0.31.0-alpha.2 to get past a breaking issue with prometheus using a breaking type from 0.30.2 - the version of googleapis/api/expr/v1alpha1 that is brought in with github.com/google/cel-go with anything newer than v0.17.8 no longer includes GetConstExpr() so pinning that lib to v0.17.8 [0] openshift/library-go@bd5e34c [1] openshift/client-go@792100e#diff-233949a4a2a7ca43d091c935b04748464200784e5377366d574945d3fd06ed89R76 [2] kubernetes-sigs/controller-runtime#2783 [3] openshift/library-go@df7ff42 [4] openshift/kubernetes-apimachinery@89b9414 Signed-off-by: Jamo Luhrsen <[email protected]>
- Bump Golang to 1.22: - `CGO_ENABLED=1` is required for `go test -race` (golang/go#51235). - Bump k8s.io/* modules to 0.30.3 and OpenShift API to the latest. - Bump Controller runtime bumped to 0.18.5: - Controller's `Watch` function now has a single generic source parameter (kubernetes-sigs/controller-runtime#2783). - Manager's `CertDir` option removed, now using the dedicated webhook server option (kubernetes-sigs/controller-runtime#2422). - Cache's `Namespaces` option replaced by `DefaultNamespaces` (kubernetes-sigs/controller-runtime#2421).
- Bump Golang to 1.22: - Update go.mod - Update Dockerfiles - Add `CGO_ENABLED=1` to `go test -race` (golang/go#51235). - Bump k8s.io/* modules to 0.30.3 and OpenShift API to the latest. - Bump Controller runtime bumped to 0.18.5: - Controller's `Watch` function now has a single generic source parameter (kubernetes-sigs/controller-runtime#2783). - Manager's `CertDir` option removed, now using the dedicated webhook server option (kubernetes-sigs/controller-runtime#2422). - Cache's `Namespaces` option replaced by `DefaultNamespaces` (kubernetes-sigs/controller-runtime#2421).
- Bump Golang to 1.22: - Update go.mod - Update Dockerfiles - Add `CGO_ENABLED=1` to `go test -race` (golang/go#51235). - Bump k8s.io/* modules to 0.30.3 and OpenShift API to the latest. - Bump Controller runtime bumped to 0.18.5: - Controller's `Watch` function now has a single generic source parameter (kubernetes-sigs/controller-runtime#2783). - Manager's `CertDir` option removed, now using the dedicated webhook server option (kubernetes-sigs/controller-runtime#2422). - Cache's `Namespaces` option replaced by `DefaultNamespaces` (kubernetes-sigs/controller-runtime#2421). - Regenerate CRD and bundle manifests: - ExternalDNS API uses `metav1.LabelSelector` for the label filtering. It was updated with `+listType=atomic` marker which resulted in the addition of `x-kubernetes-list-type: atomic` to CRD.
- Bump Golang to 1.22: - Update go.mod - Update Dockerfiles - Add `CGO_ENABLED=1` to `go test -race` (golang/go#51235). - Bump k8s.io/* modules to 0.30.3 and OpenShift API to the latest. - Bump Controller runtime bumped to 0.18.5: - Controller's `Watch` function now has a single generic source parameter (kubernetes-sigs/controller-runtime#2783). - Manager's `CertDir` option removed, now using the dedicated webhook server option (kubernetes-sigs/controller-runtime#2422). - Cache's `Namespaces` option replaced by `DefaultNamespaces` (kubernetes-sigs/controller-runtime#2421). - Regenerate CRD and bundle manifests: - ExternalDNS API uses `metav1.LabelSelector` for the label filtering. It was updated with `+listType=atomic` marker which resulted in the addition of `x-kubernetes-list-type: atomic` to CRD.
- Bump Golang to 1.22: - Update go.mod - Update Dockerfiles - Add `CGO_ENABLED=1` to `go test -race` (golang/go#51235). - Bump k8s.io/* modules to 0.30.3 and OpenShift API to the latest. - Bump Controller runtime bumped to 0.18.5: - Controller's `Watch` function now has a single generic source parameter (kubernetes-sigs/controller-runtime#2783). - Manager's `CertDir` option removed, now using the dedicated webhook server option (kubernetes-sigs/controller-runtime#2422). - Cache's `Namespaces` option replaced by `DefaultNamespaces` (kubernetes-sigs/controller-runtime#2421). - Regenerate CRD and bundle manifests: - ExternalDNS API uses `metav1.LabelSelector` for the label filtering. It was updated with `+listType=atomic` marker which resulted in the addition of `x-kubernetes-list-type: atomic` to CRD. - Bump `kustomize` to v5 to fix a conflict caused by k8s.io bumps: - `kyaml` unable to use the bumped `github.com/google/gnostic-models/openapiv2` package.
- Bump Golang to 1.22: - Update go.mod - Update Dockerfiles - Add `CGO_ENABLED=1` to `go test -race` (golang/go#51235). - Bump k8s.io/* modules to 0.30.3 and OpenShift API to the latest. - Bump Controller runtime bumped to 0.18.5: - Controller's `Watch` function now has a single generic source parameter (kubernetes-sigs/controller-runtime#2783). - Manager's `CertDir` option removed, now using the dedicated webhook server option (kubernetes-sigs/controller-runtime#2422). - Cache's `Namespaces` option replaced by `DefaultNamespaces` (kubernetes-sigs/controller-runtime#2421). - Regenerate CRD and bundle manifests: - ExternalDNS API uses `metav1.LabelSelector` for the label filtering. It was updated with `+listType=atomic` marker which resulted in the addition of `x-kubernetes-list-type: atomic` to CRD. - Bump `kustomize` to v5 to fix a conflict caused by k8s.io bumps: - `kyaml` unable to use the bumped `github.com/google/gnostic-models/openapiv2` package.
- Bump Golang to 1.22: - Update go.mod - Update Dockerfiles - Add `CGO_ENABLED=1` to `go test -race` (golang/go#51235). - Bump k8s.io/* modules to 0.30.3 and OpenShift API to the latest. - Bump Controller runtime bumped to 0.18.5: - Controller's `Watch` function now has a single generic source parameter (kubernetes-sigs/controller-runtime#2783). - Manager's `CertDir` option removed, now using the dedicated webhook server option (kubernetes-sigs/controller-runtime#2422). - Cache's `Namespaces` option replaced by `DefaultNamespaces` (kubernetes-sigs/controller-runtime#2421). - Regenerate CRD and bundle manifests: - ExternalDNS API uses `metav1.LabelSelector` for the label filtering. It was updated with `+listType=atomic` marker which resulted in the addition of `x-kubernetes-list-type: atomic` to CRD. - Bump `kustomize` to v5 to fix a conflict caused by k8s.io bumps: - `kyaml` unable to use the bumped `github.com/google/gnostic-models/openapiv2` package.
This change adds support for generics into the
Event
,Predicate
andHandler
chain. The motivation for this is explained in #2214, but basically the currentclient.Object
typing frequently requires type assertions when using custom handlers or predicates and is completely inappropriate when building a controller that is not based on Kubernetes objects.It aims to minimize breaking changes as much as possible. It does so in two parts:
Controller
which makes no sensecontroller.Watch
to take a wrapper type that encapsulates the generic part and hides it from the controller. This is pretty much the same, but more complicatedHandler
andPredicate
effectively becomes optional for aSource
which I think makes sense, as a custom source may decide it is easier not to implement these. The existing concept is also visibly shoehorned insofar as that Event/PredicateHandler have Create/Update/Delete and Generic variants, whereas the first three are only used in the context of a kube watch and the latter only outside of itTyped
variant to all constructors/types in the latter three packages and making theKind
andChannel
source constructors generic. This means that this change is 100% backwards-compatible in thebuilder
This change will require users to adjust their code only if:
controller.Watch
rather than the builderChannel
sourcebuilder.WatchRawSource
with...builder.WatchesOption
source.Source
The above is IMHO likely a relatively small fraction of our users and can be assumed to have advanced knowledge of controller-runtime.
As a side-effect of keeping the
builder
compatible, users of it will not see this feature. Finding a way to support this in the builder outside ofWatchesRawSource
is left as a future task and will likely not have a solution with a nice UX due to golang/go#49085Fixes #2214