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
server: make DI practical for controllers #4250
Conversation
This is a bit roundabout, but the key issue is that controllers (`Reconciler`s) need the controller client to do their work, which comes from the `ctrl.Manager`, but isn't available until the manager is actually connected to the API server, so it's not suitable for normal DI. A `ControllerBuilder` provides the glue between the manager and reconcilers by providing them with the client and calling the `SetupWithManager` methods on them. The latter of these is a standard method that's bootstrapped by the codegen (but not part of the `Reconciler` interface).
@nicks This is really pretty kludgy but I can't find a cleaner pattern. If you've got ideas, feel free to use this branch to play around. The only other thought I had was to make a some sort of injector that explicitly takes in the client:
Then the manager could just call that with its client (deferring the construction of the controllers until its ready) and do all the work when it's initializing, but that creates all kinds of import cycles since you also need the main app deps. |
func (r *FileWatchController) SetupWithManager(mgr ctrl.Manager) error { | ||
return ctrl.NewControllerManagedBy(mgr). | ||
For(&filewatches.FileWatch{}). | ||
Owns(&filewatches.FileWatch{}). |
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 don't think this is right?
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.
Oops I'll take that off, this controller is in a weird state because it just exists to ensure DI stuff is functional. (It might be worth making a "sample" entity in tilt-apiserver
for testing type purposes)
limitations under the License. | ||
*/ | ||
|
||
package core |
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 was assuming we'd put each controller in its own package
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 - this was mostly an artifact of running kubebuilder/apiserver-boot at some point, I'll move it
ctx, m.cancel = context.WithCancel(ctx) | ||
|
||
// TODO(milas): we should provide a logr.Logger facade for our logger rather than using zap | ||
w := logger.Get(ctx).Writer(logger.DebugLvl) | ||
ctrl.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(w))) | ||
|
||
utilruntime.Must(corev1alpha1.AddToScheme(scheme)) | ||
|
||
mgr, err := ctrl.NewManager(m.config, ctrl.Options{ |
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.
can we dependency-inject the manager?
i think that would avoid a lot of the circular initialization issues you're running into
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.
Unfortunately the manager is the problem -- ctrl.NewManager
creates a client that tries to establish a connection to API server and errors out if it's not up. (I originally tried to split the construction and .Start()
call to do exactly what you're suggesting, which is how I discovered this mess)
Another possibility would be to create a DeferredClient
that just returns errors until provided with a real implementation? That way there's no hard dependency and given that the client is only meant to be used within Reconcile()
it shouldn't ever be able to get called before initialization anyway.
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.
Take a look at cc45e52 and see if you think that's an improvement? It really just lets controllers pretend to get created as expected by getting a ctrlclient.Client
injected. The impl is *deferredClient
that returns errors (or nil
for methods that don't return error
which is less than ideal) until the manager initializes it.
Not sure that the extra indirection is worth saving on needing to call SetClient()
, but at least it hides some of the oddities from each controller implementation.
ah, ok thanks for checking! |
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.
looked at the other commit - i don't have a strong pref of one vs the other
corev1alpha1 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" | ||
) | ||
|
||
func NewScheme() *runtime.Scheme { |
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 think GenericConfig in apiserver.Config should have a scheme on it now?
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.
Ah, yep, it's on ExtraConfig
. I'm hitting a funky issue though, not sure if we can share schemes between the actual API server and controller manager:
error initializing *filewatch.Controller controller: multiple group-version-kinds associated with type *v1alpha1.FileWatch, refusing to guess at one
The two versions are __internal
and v1alpha1
. The former comes from the API server's scheme registration:
https://github.com/tilt-dev/tilt-apiserver/blob/095950403c2cbc559e2f201da1c22e66b425e202/pkg/server/builder/resource/types.go#L119-L120
(Since it's kind of weird/indirect, when there are multi versions of an entity, the storage version is always going to get the __internal
version in API server scheme.)
LMK if I'm missing something or doing something wrong - I am not familiar with the depths of the API server
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 don't know, let's just check this in and i can debug what's going on...
This prevents the HUD server blocking forever since the controller manager is still connected.
@nicks Want to call out latest commit - I changed subscribers to get torn down in reverse order. Integration tests were getting stuck shutting down API server before controller manager because the controller manager was presumably holding onto a connection to the API server that it was waiting to be drained. tilt/internal/store/subscriber.go Lines 103 to 105 in 855ad35
|
sgtm
…On Fri, Feb 26, 2021 at 4:22 PM Milas Bowman ***@***.***> wrote:
@nicks <https://github.com/nicks> Want to call out latest commit - I
changed subscribers to get torn down in *reverse* order. Integration
tests were getting stuck shutting down API server before controller manager
because the controller manager was presumably holding onto a connection to
the API server that it was waiting to be drained.
https://github.com/tilt-dev/tilt/blob/855ad3583335cd82d6c6de705a73d1a46fdb8f77/internal/store/subscriber.go#L103-L105
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#4250 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACEA4PL5UWDQ3ICQJKMLPDTBAGHVANCNFSM4YHGYKAQ>
.
|
This is a bit roundabout, but the key issue is that controllers
(
Reconciler
s) need the controller client to do their work, whichcomes from the
ctrl.Manager
, but isn't available until the manageris actually connected to the API server, so it's not suitable for
normal DI.
A
ControllerBuilder
provides the glue between the manager andreconcilers by providing them with the client and calling the
SetupWithManager
methods on them. The latter of these is a standardmethod that's bootstrapped by the codegen (but not part of the
Reconciler
interface).