Skip to content
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

Add a "controller engine" that manages the lifecycles of controllers #160

Merged
merged 2 commits into from
Apr 23, 2020

Conversation

negz
Copy link
Member

@negz negz commented Apr 23, 2020

Description of your changes

A controller engine is somewhat like a controller "sub-manager", in that it's effectively a group of controllers. Unlike a typical controller manager, the lifecycle of the controllers an engine manages are not coupled to the lifecycle of the engine itself. An engine may be used by a parent controller to start and stop child controllers in accordance with configuration provided by the custom resource that the parent controller watches

Checklist

I have:

  • Run make reviewable to ensure this PR is ready for review.
  • Ensured this PR contains a neat, self documenting set of commits.
  • Updated any relevant documentation, examples, or release notes.
  • Updated the RBAC permissions in clusterrole.yaml to include any new types.

kubernetes-sigs/controller-runtime#863

We'd like to use the above PR, which is not yet included in a controller-runtime
release. Updating controller-runtime requires updating a few other dependencies,
which changed the signature of client-go clientset methods. This commit removes
the only two uses of clientset from crossplane-runtime. pkg/test/integration now
uses a controller-runtime client.Client. pkg/test.Env has been removed, as it no
longer has any known users.

Signed-off-by: Nic Cope <negz@rk0n.org>
@negz negz requested review from muvaf, hasheddan and kasey April 23, 2020 08:46
*/

// Package controller provides utilties for working with controllers.
package controller
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 have mixed feelings about this package name; I like how controller.NewEngine reads but it seems like it will frequently conflict with controller-runtime/pkg/controller.

Copy link
Member

Choose a reason for hiding this comment

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

I prefer the symmetry despite the naming conflicts 👍

Copy link
Member Author

@negz negz Apr 23, 2020

Choose a reason for hiding this comment

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

I've so far worked around this by importing the controller-runtime/pkg/controller as ctrlr (which is close to our convention of importing sigs.k8s.io/controller-runtime as ctrl).

Copy link
Member

Choose a reason for hiding this comment

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

kctrl for controller-runtime and ctrl for ours?

A controller engine is somewhat like a controller "sub-manager", in that it's
effectively a group of controllers. Unlike a typical controller manager, the
lifecycle of the controllers an engine manages are not coupled to the lifecycle
of the engine itself. An engine may be used by a parent controller to start and
stop child controllers in accordance with configuration provided by the custom
resource that the parent controller watches.

Signed-off-by: Nic Cope <negz@rk0n.org>
*/

// Package controller provides utilties for working with controllers.
package controller
Copy link
Member

Choose a reason for hiding this comment

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

I prefer the symmetry despite the naming conflicts 👍

// An Engine manages the lifecycles of controller-runtime controllers (and their
// caches). The lifecycles of the controllers are not coupled to lifecycle of
// the engine, nor to the lifecycle of the controller manager it uses.
type Engine struct {
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that's the idea. I liked the ControllerEngine abstraction around the controller-runtime machinery, and it seems like we'll probably have at least 2-3 controllers that are managing groups of child controllers, so I wanted to clean it up a little and add it here.

@negz
Copy link
Member Author

negz commented Apr 23, 2020

I've plumbed this up to @muvaf's work in crossplane/crossplane#1412 and run it to make sure it works as expected.

Copy link
Member

@muvaf muvaf left a comment

Choose a reason for hiding this comment

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

Thanks for moving this into crossplane-runtime. I'm suspicious whether it will be used by any other than core crossplane but it won't hurt to have it here either.

Could you also explain somewhere as comment why you needed to deal with concurrency issues?

*/

// Package controller provides utilties for working with controllers.
package controller
Copy link
Member

Choose a reason for hiding this comment

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

kctrl for controller-runtime and ctrl for ours?

e.newCtrl = fn
}
}

Copy link
Member

Choose a reason for hiding this comment

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

I see that there isn't WithLogger option. Do you intend to keep engine itself log-less? The passed in reconcilers will have their own logger so it shouldn't be a big concern but just checking.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, at least for now. I'm not worried about adding one, but I noticed the logs we had were mostly just duplicating existing logs from controller-runtime.

go func() {
<-e.mgr.Elected()
e.done(name, errors.Wrap(ca.Start(stop), errCrashCache))
}()
Copy link
Member

Choose a reason for hiding this comment

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

I don't exactly know why but controller-runtime waits for the cache to sync by calling ca.WaitForCacheSync(stop) after running its go routine.

Copy link
Member

Choose a reason for hiding this comment

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

Should be good to go after addressing this.

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 figured we could avoid this because controller.NewUnmanaged calls WaitForCache before it starts the controller.

// The default new cache and new controller functions.
var (
DefaultNewCacheFn NewCacheFn = cache.New
DefaultNewControllerFn NewControllerFn = controller.NewUnmanaged
Copy link
Member

Choose a reason for hiding this comment

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

Why export these and not directly use cache.New and controller.NewUnmanaged directly?

Copy link
Member Author

Choose a reason for hiding this comment

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

Mostly just for documentation purposes - possibly a bit overkill but I figured it would make it obvious to GoDoc readers what the defaults were.


// Err returns any error encountered by the named controller. The returned error
// is always nil if the named controller is running.
func (e *Engine) Err(name string) error {
Copy link
Member

Choose a reason for hiding this comment

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

I see that you're not using Err function in crossplane/crossplane#1440 How should this be used? Check on errors in every reconcile of whoever called Start?

Copy link
Member Author

@negz negz Apr 23, 2020

Choose a reason for hiding this comment

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

Yeah, something like that. I figured it could be used to log or emit a condition if and when we discover that the controller has crashed. could be something like:

if !ctrl.IsRunning(name) {
    if err := ctrl.Err(name); err != nil {
       log.Debug("womp womp", "error", err)
    }
    _ = ctrl.Start(name, o)
}

@negz
Copy link
Member Author

negz commented Apr 23, 2020

Could you also explain somewhere as comment why you needed to deal with concurrency issues?

@muvaf I wanted to allow the cache and controller goroutines a way to surface any error they encountered, and also to close the stop channel if they encountered an error (or just stopped for some other reason). This ensures that if the controller crashes it stops the cache too and vice versa. The map of stop channels can't be shared across goroutines without some kind of lock, and channels also panic if they're closed multiple times, so I ended up playing with a few patterns trying to use sync.Once etc before just giving up and using a good old fashioned mutex.

@negz negz merged commit dfff858 into crossplane:master Apr 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants