Refactor common functionality in controllers#892
Refactor common functionality in controllers#892google-prow-robot merged 5 commits intoknative:masterfrom mdemirhan:contrefactor
Conversation
…educe the repetition in various controllers.
| informer := base.ElaInformerFactory.Elafros().V1alpha1().Configurations() | ||
| revisionInformer := base.ElaInformerFactory.Elafros().V1alpha1().Revisions() | ||
|
|
||
| base.Init(controllerAgentName, "Configurations", informer.Informer()) |
There was a problem hiding this comment.
This to me indicates there's a circular dependency between the controller and the base. Having the construction of the base happen in two phases (NewController+base.Init) can be error prone and potentially restrictive.
If possible I would instead to separate the concerns strictly by using consumer interfaces to loosely couple the base and the specific crd logic. I think of this similar to how go's http package is meant to be used. Create an http.Server and provide it http.Handlers. With the handlers not needing to know about the underlying server.
Thus I could imagine creating a controller and hooking in the necessary CRD logic as a handler
There was a problem hiding this comment.
I haven't looked too closely at it, but the operator framework stuff does something similar, it seems:
https://github.com/operator-framework/operator-sdk/blob/master/doc/user-guide.md#define-the-handler
There was a problem hiding this comment.
Not sure why there is a circular dependency. There is a base struct that implements the common functionality and is driven by the concrete implementation. It is not that far off from http handlers, as the concrete implementation provides a handler (syncHandler) to the base and base simply keeps calling it. If you have an example of how you would implement your proposal in this context, that will help me understand your proposal better.
| kubeclientset kubernetes.Interface | ||
| elaclientset clientset.Interface | ||
| buildclientset buildclientset.Interface | ||
| base *controller.ControllerBase |
There was a problem hiding this comment.
FYI: by not naming the nested struct explicitly you won't need to use c.base everywhere
ie.
type Controller struct {
*controller.ControllerBase
...
}
then c.base.Run would be c.Run
| base *controller.ControllerBase, | ||
| buildClientSet buildclientset.Interface, | ||
| config *rest.Config, | ||
| controllerConfig controller.Config) controller.Interface { |
There was a problem hiding this comment.
I have a preference for returning the concrete type instead of controller.Interface - especially given that the tests end up casting it.
There was a problem hiding this comment.
I would like to keep this PR scoped to reducing code duplication. I don't want to mix this with other changes we want in the controllers.
|
/assign @grantr |
|
There was some discussion about using generators to help with the boilerplate here: https://elafros.slack.com/archives/CA4DNJ9A4/p1525180892000341 The two that were mentioned: |
|
I am not sure if it is worth writing generators for the four controllers we have. If the direction is that, I will abandon this PR and create an issue tracking this. My aim with this one was very simple: minimize the copy paste between our controller code by simply pulling that functionality in a common file. The reason I attempted this was because I was moving the controller code to a structured logging library and I had to make the same change in four different places. I also want to keep this PR to be more practical - while generators, ...etc might be the way to go, if we are not going to do that anytime soon, we might as well take a baby step first. Let me know what you think. |
|
I'm fine with this PR going in (though I haven't reviewed it yet) but FYI I plan to attempt a similar refactoring using kubebuilder. It's unclear at this point whether kubebuilder will be an improvement. |
|
I think we will want to continue to explore ways to refactor things to reduce boilerplate (and frankly make better use of encapsulation vs. having so much inlined into the controllers themselves). Generally, I do think this is an improvement, but IDK that anyone is going to confuse it for the final improvement(tm). I think kubebuilder is definitely a thread worth pulling on (and perhaps if not now, it will be ready later). Does that sounds fair? |
|
Yup - I'm all for doing this incrementally. |
| } | ||
|
|
||
| type ControllerBase struct { | ||
| // kubeClient allows us to talk to the k8s for core APIs |
There was a problem hiding this comment.
nits here, but the names in the comments do not match the actual variable. Here and below.
| // simultaneously in two different workers. | ||
| WorkQueue workqueue.RateLimitingInterface | ||
|
|
||
| initialized bool |
There was a problem hiding this comment.
does this need to be synchronized?
There was a problem hiding this comment.
Bazed on Matt's feedback, I removed this two stage initialization and we no longer need this one.
There was a problem hiding this comment.
Dave's and Matt's feedback to be precise :)
| buildclientset buildclientset.Interface, | ||
| kubeInformerFactory kubeinformers.SharedInformerFactory, | ||
| elaInformerFactory informers.SharedInformerFactory, | ||
| base *controller.ControllerBase, |
There was a problem hiding this comment.
I'd recommend against changing the signature in this change. This will let us fold the Init functionality into a constructor we call from inside here.
| ctrl.Config{}, | ||
| ).(*Controller) | ||
| base := ctrl.NewControllerBase(kubeClient, elaClient, kubeInformer, elaInformer) | ||
| controller = NewController(base, buildClient, &rest.Config{}, ctrl.Config{}).(*Controller) |
There was a problem hiding this comment.
This and other files can be reverted once the signature doesn't change :)
|
|
||
| // NewControllerBase instantiates a new instance of ControllerBase implementing | ||
| // the common & boilerplate code between our controllers. | ||
| func NewControllerBase( |
There was a problem hiding this comment.
nit: here and the struct, drop Controller since it's already in the package name.
I think controller.Base and controller.NewBase are cleaner.
|
/approve Will let @grantr LGTM to confirm I'm not wildly misinterpreting his comment above :) |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: mattmoor, mdemirhan The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
Thanks all! QQ: Is it possible to override the auto merge behavior? Auto merged PRs have really bad git commit messages (a union of all individual commit messages, so things like "addressed PR comments", "added more documentation", ...etc that are part of iteration but not necessarily should be part of the commit message are all part of the commit message). |
|
The only way I know of is applying a |
|
Alternately, you can |
All of our controllers have some boilerplate initialization and running code all copied and pasted between files. This PR puts the boilerplate & common code into controller.go.
This PR is still not complete. I did the refactoring and applied it only to two controllers (config and revision) to see how it fits. Remaining controllers need to be refactored as well, but I prefer doing it only after I get an initial feedback (in case there is strong pushback to the methodology, I don't want to waste too much time refactoring others as well :)).
PS: I also removed metrics from one of the controllers and i Plan to remove the remaining ones as well. Those metrics were meant to just showcase how metrics can be generated from our code, but they are not very useful metrics for developers or us. We have a much better usecase of metrics in autoscaler controller code and we no longer need this sample-ish code in our controllers.