-
Notifications
You must be signed in to change notification settings - Fork 29
✨ Extend interface with runner #62
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
base: main
Are you sure you want to change the base?
Conversation
I was also thinking extending the provider interface for something like this makes sense. I wonder if it would helpful to have the multicluster manager's existing start function also call the provider's Run func before it sets everything up, and then maybe SetupWithMgr logic could fit somewhere in there too. |
@embik and I been discussing this async. My take was that one might want to control lifecycle of those. I personally find these things left flexible. And result is still the same, as manager inside operates on Like:
means you still need interface exposed. |
650df41
to
93f2c78
Compare
Yeah, the "who is responsible" is definitely tricky. On the other side of things it can be easy to forget to start the provider (which happened to me at one point 😅 ) What would you think of renaming Run() on the provider to something like StartupHook() and have it return a func? Maybe then we get the best of both worlds, a managed startup and clearer responsibility. func (p *scopedManager) Start(ctx context.Context) error {
prov := p.Manager.GetProvider()
if prov != nil && prov.StartupHook != nil {
if err := prov.StartupHook(ctx); err != nil {
return fmt.Errorf("failed to run provider: %w", err)
}
}
return p.Manager.GetLocalManager().Start(ctx)
} |
I think I would be ok. In my current POC I kinda building towards idea that we would need |
IMHO it would make some sense for the manager to start the provider. So at the moment it will always end in this setup: provider := myprovider.New()
mgr, _ := mcmanager.New(ctrl.GetConfigOrDie(), provider, mcmanager.Options{})
provider.SetupWithManager(mgr) // Or something else to set the manager
go mgr.Start(ctx)
go provider.Run(ctx)
// wait Which could instead be: provider := myprovider.New()
mgr, _ := mcmanager.New(ctrl.GetConfigOrDie(), provider, mcmanager.Options{}) // manager sets itself on the provider
mgr.Start(ctx) // manager starts the provider When I read the interfaces for the first time I found it odd that the provider interface had no However I think that was by design as it might be too early to decide the lifecycle of both the provider and manager, as that could stifle design choices. If the lifecycle is less flexible that could result in some designs not being possible. Then again - the lifecycles are already linked somewhat, so it would make sense to include that in the design. StartWithManagerIf the lifecycle is linked like this I'd prefer func (p *Provider) SetupWithManager(mgr mcmanager.Manager) error {
if mgr == nil {
return ErrMgrNil
}
p.mgr = mgr
}
func (p *Provider) Start(ctx context.Context) error {
if p.mgr == nil {
return ErrMgrNil
}
// ...
} or: func (p *Provider) StartWithManager(ctx context.Context, mgr mcmanager.Manager) error {
if mgr == nil {
return ErrMgrNil
}
p.mgr = mgr
// ...
} And providers that would also work without a mgr wouldn't care either way. |
So if I understand correctly, you are suggesting using |
Exactly, looking at some sample code the |
I have a hard time following everyone's position to be honest. I can see two cases (which I think map to what you're suggesting, but we stand a bit in a middle today?)
I think 1 makes it easier for lifecycle and requires less provider-specific management. 2 gives a lot of control and indepedence from the rest of the controllers (could the controller bog down the manager and therefore block the provider from doing its job -- maybe there could be a small chance of deadlock?), but I'm not sure the added complexity is worth it. In conclusion, if we go for 1, the only "external" requirement is for the provider to be a runnable for the manager to manage. |
We don't stand in the middle - the status quo is 2: The provider is entirely independent from the manager. That has some problems, e.g. the user needs to know when the provider expects to be run. Before the manager? After the manger? When should the manager be set on the provider? Before or after starting the manager? E.g. in the multicluster-runtime/pkg/manager/manager.go Lines 201 to 210 in a41032c
Say a provider only has the prov := myprov.New(myprov.Opts{})
mgr, err := mcmanager.New(cfg, prov, mcmanager.Opts{})
prov.SetManager(mgr)
mgr.Add(myrunnable1)
mgr.Add(myrunnable2)
return mgr.Start(ctx) In reality the
Correct - and to prevent the provider starting to provide clusters too early the manager should pass itself when running the provider. When @mjudeikis and I were talking he also suggested that the type Provider interface {
Start(context.Context, Aware)
Get(...)
IndexField(...)
} Providers that act like a controller (e.g. cluster-api or gardener) are already expecting to be passed a manager anyhow because they need their manager to target whatever cluster they pull the data from, not necessarily the cluster they are running in. |
Yeah, this might be an option.
It sounds reasonable. Need to test this in the code. So I think im leaning towards:
Does anybody have anything against this? |
If the method takes a manager I like that. |
I think only issue could be circularity in go. Need to shuffle code and see as I dont have (and I hope I never will need to have) mental model of all packages :) |
We're on the same page! I actually have been pushing towards making it a controller in some PRs because it became quickly clearly we needed a way to make setup and management of a provider simple. I'm not too scared of the circular dependency. I think the provider interface could be put into a separate package alongside helper methods and both the provider implementations and manager would import it. Aware is not too clear to me, thats recent right? What are we aware of? I do like |
multicluster-runtime/pkg/multicluster/multicluster.go Lines 26 to 43 in 00d670c
Since the |
ba8166d
to
d8ebc45
Compare
I updated the code. Moved Having It does not solve the startup ordering problem, but I'm not sure how I feel about that either. I like explicitness. @ntnn @embik @corentone @FourFifthsCode, what's your take on the current iteration? |
@corentone I think you laid out the positions clearly!
I tend to lean toward option 1 myself because I view managers as a framework that helps get things bootstrapped and if I really need something custom I can just use informers directly. The Runnable interface is designed around this. The one thing that is tricky is the circular dependency. Providers with controllers needs access to the manager, but managers need access to the clusters a provider manages 😅 Also not all providers need a controller. |
I will ask differently. Does whats currently in the code would solve your issues/challenges you seen on your end or are you missing something? |
So if we went with manager starting the provider, we could remove the need for manually starting the provider in example's main.go: g.Go(func() error {
return ignoreCanceled(provider.Start(ctx, mgr))
}) The tricky bit though I just realized is that we are pulling in the external controller runtime manager and calling its Start() method, which means we don't have direct access to call our provider's Start method inside of it: One thing we could do is maybe make the provider satisfy the single controller runtime Runnable interface and then add it during the New() func in the multi-cluster manager: func New(config *rest.Config, provider multicluster.Provider, opts manager.Options) (Manager, error) {
mgr, err := manager.New(config, opts)
if err != nil {
return nil, err
}
mgr.Add(provider) // Add this - provider would satisfy the Runnable interface
return WithMultiCluster(mgr, provider)
} |
Well, we can overwrite the
I like the idea of making the providers Runnable and letting the underlying manager take care of this - however then we'd be at step one with the ouroboros that the manager needs a provider and the provider needs a manager and we'd again need something like
But we can let the controller-runtime manager take care of running the provider with a RunnableFunc: func (m *mcManager) Start(ctx context.Context) error {
m.Add(func(ctx context.Context) error) {
return m.provider.Start(ctx, m)
})
return m.Manager.Start(ctx)
} This would also have the advantage the the providers are the last thing starting after the rest of the manager components has started. And should mcr later move to multiple providers per manager that can easily be expanded. |
pkg/manager/manager.go
Outdated
@@ -254,6 +254,12 @@ func (p *scopedManager) Add(r manager.Runnable) error { | |||
|
|||
// Start starts the manager. | |||
func (p *scopedManager) Start(ctx context.Context) error { | |||
err := p.Add(manager.RunnableFunc(func(ctx context.Context) error { |
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.
TBH I dont think I like this :) I get this makes it easier as its always starts the provider, but I like my control :/
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 manager shouldn't add the runnable. It's the provider setup that should add the runnable.
The provider should satisfy the runnable interface (see below) and just be added during init of the provider.
IMO flow should be:
p := providerX.New(/* aware interface */ mgr)
mgr.Add(p)
mgr.Start()
or, better:
p := providerX.NewInManager(/*runner */ mgr, /*aware*/ mgr) // name of func would be kept aligned with existing
mgr.Start()
or, even better if we want to keep Aware and Runner together:
type AwareMgr interface {
Aware
Add(Runnable) error
}
p := providerX.NewInManager(/*runner + aware */ mgr) // name of func would be kept aligned with existing
mgr.Start()
Is there a reason for the manager to be aware of the providers?
// Runnable allows a component to be started.
// It's very important that Start blocks until
// it's done running.
type Runnable interface {
// Start starts running the component. The component will stop running
// when the context is closed. Start blocks until the context is closed or
// an error occurs.
Start(context.Context) error
}
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 manager shouldn't add the runnable. It's the provider setup that should add the runnable. The provider should satisfy the runnable interface (see below) and just be added during init of the provider.
IMO flow should be:
p := providerX.New(/* aware interface */ mgr) mgr.Add(p) mgr.Start()
I like this lot! Feels cleaner! And this still give user control but also easy to setup
Is there a reason for the manager to be aware of the providers?
Yeah, ideally not!
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.
Just leaving a comment here, I would say that given the loops we are encountering otherwise (discussed later), I think treating the provider as a "special" singleton runnable that gets started in the manager code makes a lot of sense. I quite like @ntnn's proposal above.
8c8b82a
to
dfbd740
Compare
So I took a step back. Lets not blow this to the moon. Solve simple problem for now. Lets keep it simple. This adds Everything else like:
In the end we can agree this is advanced controller-manager usage and I expect people know their code. Until AI takes over, they will have to :) For anything else, if you feel your issue is not addressed - please create an issue and lets move discussions there and lets do more of "data driven development" vs "assumptions based driven development" :) |
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 feels to me that the Start should not contain the aware and its the responsibility of the provider to take aware as a parameter during its setup.
The manager doesn't require to know about the provider I think, so we should be able to keep it this way.
The provider can use the manager for the following:
1/ (MUST) for the aware interface, so it can pass new clusters
2/ (OPT, but likely common) for the GetClient(), so it can read local objects
3/ (OPT, but likely common) for the Add(Runnable), so it can be started and managed as a runnable and remove burden on the user
We can figure if we want full composability (3 interfaces), medium (2 interfaces: mgr vs aware) or mandated abstraction (one interface with all).
I'm thinking either full composability or medium composability.
pkg/manager/manager.go
Outdated
@@ -254,6 +254,12 @@ func (p *scopedManager) Add(r manager.Runnable) error { | |||
|
|||
// Start starts the manager. | |||
func (p *scopedManager) Start(ctx context.Context) error { | |||
err := p.Add(manager.RunnableFunc(func(ctx context.Context) error { |
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 manager shouldn't add the runnable. It's the provider setup that should add the runnable.
The provider should satisfy the runnable interface (see below) and just be added during init of the provider.
IMO flow should be:
p := providerX.New(/* aware interface */ mgr)
mgr.Add(p)
mgr.Start()
or, better:
p := providerX.NewInManager(/*runner */ mgr, /*aware*/ mgr) // name of func would be kept aligned with existing
mgr.Start()
or, even better if we want to keep Aware and Runner together:
type AwareMgr interface {
Aware
Add(Runnable) error
}
p := providerX.NewInManager(/*runner + aware */ mgr) // name of func would be kept aligned with existing
mgr.Start()
Is there a reason for the manager to be aware of the providers?
// Runnable allows a component to be started.
// It's very important that Start blocks until
// it's done running.
type Runnable interface {
// Start starts running the component. The component will stop running
// when the context is closed. Start blocks until the context is closed or
// an error occurs.
Start(context.Context) error
}
// Start runs the provider. Implementation of this method should block. | ||
// If you need to pass in manager, it is recommended to implement SetupWithManager(mgr mcmanager.Manager) error method on individual providers. | ||
// It is not part of the provider interface because it is not required for all providers. | ||
Start(context.Context, Aware) error |
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 we're mixing two concepts here.
- The manager that is used for running things (the ACTUAL manager that deals with runnables) And to provide a kubeconfig (thought those two could also be split)
- The Aware part that deals with cluster manager and register.
The start part is ONLY related to runnables. The aware part is a parameter that the provider should hold, provided during setup?.
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.
So what do you suggest here? I dont see mixing here, but I been soaking in this for way too long :)
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 see a mixing either.
The docstring specifies that if the full manager is needed that the provider should implement a different method. And I don't think that there will be many providers that actually need a reference to the full manager.
It only takes an Aware
because it should use that Aware
to engage clusters.
I'd instead argue that this will encourage cleaner design because provider developers will rather ask for a kubeconfig in their setup (either as parameters to their New
or in their Options
...) - which may be the local cluster config but could also be something else - rather than expecting the manager.
@@ -126,14 +125,14 @@ func (p *Provider) Get(_ context.Context, clusterName string) (cluster.Cluster, | |||
return nil, multicluster.ErrClusterNotFound | |||
} | |||
|
|||
// Run starts the provider and blocks. | |||
func (p *Provider) Run(ctx context.Context, mgr mcmanager.Manager) error { |
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.
should we mark it deprecated instead?
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.
Do we really need to?
First, its replaced with very similar method with same functionality
Second, go is good to catch these at compile time so people should just make a change.
Its not like we deprecating a functionality and giving people to find different way of doing things.
That comes back to the problem that providers can start providing clusters before they should. The intent in the That we could solve the lifecycle problem at the same time that a provider could be started too early was an off idea, but I agree with @mjudeikis that we should continue discussing that in another ticket.
The manager has to know about the provider - otherwise the manager cannot get clusters or index fields. |
@corentone I just reverted the change for now. I feel there is scope creating happening :) Lets keep it simple for now:
|
I think this is false statement to start with. Manager need to know about provider. It needs to pass in cluster lifecycle (Engage) into the provider. It by desing. So I dont think your suggestions quite work. Or I dont understand what you are suggesting. If you feel strongly about this, can you propose a change. Not pseudo code, as most pseudo code we have in this PR conversations does not compile, does not work or is wrong :/ |
dfbd740
to
0a9f077
Compare
I think I see where the confusion is on my side. the Manager currently has a GetProvider I didn't realize was there. Hence the manager needs a pointer to the provider and the provider needs a pointer to the manager to call Engage. I found out why we had the I think otherwise my suggestions only hold if the manager is not aware of the providers at all and just gets called with
This is a valid ask and I'll only be able to build it out next week. I'm okay being ignored if a majority agree on a design. If we keep the manager aware of the provider(s), we should clarify:
Couple questions before I hibernate until I can code it :)
Sorry for being high level, hope that helps and not add confusion! |
I think this gets to the core of the issue. When working on the kubeconfig provider, there felt like a good bit of boiler plate that could be common across providers and a few gotchas in regard to startup and circular dependencies, that I think its worthwhile to think about the interfaces and some tweaks could improve the experience. As well as some possible shared code. @mjudeikis sorry for blowing this up when you're wanting to make a simple change, I just thought this would be a good opportunity to address this since it deals with some of the heart of the problem space. I'm good with a follow up issue to continue discussion as well. And that could also address some of the larger scope of the interfaces themselves, however, it does seem like that could impact the changes in this PR in the long run. @corentone some examples on possible new designs of manager/provider interfaces and responsibility I think would be very interesting! |
So the Provider itself now does only
Not an issue with current version.
I dont think we can answer this question as of yet. @ntnn is experimenting here #54 with later one first one here #56 Which I think is out of scope for this PR/issue. And we should move this to those 2 prs above ^
**** I think this is a core question here ****
I don't see a reason why one could not build a singleton provider for this. I don't quite see the reason for it - but why not? :D
|
Lets do this. Once this merges, I will send my AI workers to summarise this and start a new discussion/issue. Maybe even google doc as it feels like big pie to eat in one go :) |
Co-authored-by: Nelo-T. Wallus <[email protected]>
@ntnn: changing LGTM is restricted to collaborators In response to this: 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. |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: mjudeikis, ntnn 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 |
Leaving a comment here to sum up my thoughts: Current code shows IMHO that the manager should start the provider as a runnable, lifecycling the go routines involved is a bit of a hassle right now. Doesn't produce clean code. I liked @ntnn's suggestion half-way through this discussion, I think it's the right approach because manager start is the only place where we know that the manager is up and running and it has access to the This is a good change for 0.22.0, perhaps we could cut 0.22.0-beta.0 with this feature (provider lifecycle being part of the manager) in place. With that being said, this has been a long discussion and this PR is a good building block, so I will approve once I got the clear from @corentone. |
When creating vendor-agnostic providers, start by establishing your provider alongside all applications. This means you need to wire in every supported provider into the depths of the code, as the main interface does, to support
Run
.The majority of providers will be runnable, so I think it makes sense to make it a permanent member of the interface. But
SetupWithManager
- no. And it can be done way earlier in the lifecycle of the program.Now, if you pass in the code
mcmanager.Provider
, you can't start it, as the method is not exported.So you end up with:
vs just having a single interface to back any implementation.