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

🌱 Catalog unit test #251

Closed

Conversation

ankitathomas
Copy link
Contributor

This PR refactors the e2e catalog reconcile test to a unit test checking whether reconcile is triggered on catalog events (create, update, delete events). Reconcile is detected by scanning controller logs for logs from the reconcile loop for all operators.

See: #247

Comment on lines 1031 to 1037
l := funcr.New(func(prefix, args string) {
if prefix == "operator-controller" &&
strings.Contains(args, `"controller"="operator"`) &&
strings.Contains(args, `"msg"="ending"`) {
// filter for only relevant logs
testLogs = append(testLogs, fmt.Sprintf("%s", args))
}
}, funcr.Options{Verbosity: 1})
Copy link
Member

Choose a reason for hiding this comment

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

TIL, this funcr is super cool.

I'm torn though. On one hand, doing things this way means we can pretty much test our controller setup logic as-is. However it seems very brittle because changing our logs makes our tests for a completely unrelated thing break.

Another option is to slightly restructure the SetupWithManager like this:

// SetupWithManager sets up the controller with the Manager.
func (r *OperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
	_, err := NewOperatorController(mgr, r)
	return err
}

// NewOperatorController sets up a controller for Operator objects using the provided reconciler.
func NewOperatorController(mgr ctrl.Manager, r reconcile.Reconciler) (controller.Controller, error) {
	return ctrl.NewControllerManagedBy(mgr).
		For(&operatorsv1alpha1.Operator{}).
		Watches(source.NewKindWithCache(&catalogd.Catalog{}, mgr.GetCache()),
			handler.EnqueueRequestsFromMapFunc(operatorRequestsForCatalog(context.TODO(), mgr.GetClient(), mgr.GetLogger()))).
		Owns(&rukpakv1alpha1.BundleDeployment{}).
		Build(r)
}

And then our tests could use NewOperatorController with a custom reconciler that collects the reconcile.Requests and makes assertions based on that. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switching to a fake reconciler does make the tests considerably faster, but this makes me wonder if we can just test the handler function on its own, just testing if it generates the right set of requests when a catalog changes.

That way, we'd be able to adapt the tests easier to a potential future where we only reconcile affected operators on a catalog update.

Copy link
Member

Choose a reason for hiding this comment

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

We could test the handler function on its own as well. In my mind, this test is about making sure that the entire controller setup works like we expect.

@openshift-merge-robot openshift-merge-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jun 5, 2023
@openshift-merge-robot openshift-merge-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jun 5, 2023
@ankitathomas ankitathomas force-pushed the catalog-unit-test branch 4 times, most recently from ebddb6d to 46a214a Compare June 5, 2023 22:16
@ankitathomas ankitathomas force-pushed the catalog-unit-test branch 2 times, most recently from 5765192 to 38247ae Compare June 5, 2023 22:24
)

var _ = Describe("Operator Controller Catalog Reconciler", func() {
When("a catalog changes on cluster", func() {
Copy link
Member

Choose a reason for hiding this comment

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

Based on the previous suggestion, WDYT about adding an extra layer of hierarchy under the Describe("SetupWithManager") block. This way it'll be setup a bit easer to handle more watches or other configurations we add in the future.

BeforeEach() // setup and start the controller+manager
When("Operator events occur")
    It("reconciles self") // do Operator create, update, delete
When("Catalog events occur")
    BeforeEach() // create a few operators
    It("reconciles all Operators") // do Catalog create, update, delete
    AfterEach() // delete the operators
AfterEach() // cancel the context to stop the manager

Copy link
Contributor Author

@ankitathomas ankitathomas Jun 6, 2023

Choose a reason for hiding this comment

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

I've been thinking about this, but do we want to test controller-runtime objects? Testing for reconcile on catalog events makes sense with catalogs since we're using a custom handler, but with operators, it feels like we're testing whether the manager works, that should be on controller-runtime.

If we are however adding tests for operator events, then we'd also need to add tests for bundledeployment events to see if reconciles happen for the operators in the owner references, which also seems unnecessary to test.

Copy link
Member

Choose a reason for hiding this comment

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

It isn't really testing controller-runtime. It's testing our specific watch setup. I agree that almost all of the code that actually runs is controller-runtime code, but it's all triggered based on our setup of the manager.

I think there's pretty good long term value here from a regression standpoint, especially if we can get the framework setup while things are simple. Anecdotally, the rukpak watches are actually fairly complex due to the way its APIs reference provisioner classes. There has been discussion in the context of oeprator-controller to implement things like upgrade policies via referenced objects, so I think it is inevitable that our watches become more complex over time.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think @joelanford's suggestion here makes a lot of sense for testing that the watches we are setting up are getting triggered as expected. That being said, I don't think it should block this PR specifically unless @ankitathomas really felt like including it. At least the foundation is laid by this PR and we can always increase the robustness of our tests in follow ups

Copy link
Member

@joelanford joelanford left a comment

Choose a reason for hiding this comment

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

Looks really good. Just a few (hopefully easy to implement) suggestions that I think will make things more clear and result in fewer lines of code.

everettraven
everettraven previously approved these changes Jun 8, 2023
Copy link
Contributor

@everettraven everettraven left a comment

Choose a reason for hiding this comment

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

Overall looks great! I just have one nit but IMO shouldn't block this PR. Awesome job @ankitathomas - I definitely learned some things we should add in catalogd :)

internal/controllers/catalog_test.go Outdated Show resolved Hide resolved
)

var _ = Describe("Operator Controller Catalog Reconciler", func() {
When("a catalog changes on cluster", func() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think @joelanford's suggestion here makes a lot of sense for testing that the watches we are setting up are getting triggered as expected. That being said, I don't think it should block this PR specifically unless @ankitathomas really felt like including it. At least the foundation is laid by this PR and we can always increase the robustness of our tests in follow ups

Signed-off-by: Ankita Thomas <ankithom@redhat.com>
Signed-off-by: Ankita Thomas <ankithom@redhat.com>
@ankitathomas ankitathomas force-pushed the catalog-unit-test branch 3 times, most recently from 13928f3 to 7a0c2f8 Compare June 8, 2023 21:53
Copy link
Contributor

@everettraven everettraven left a comment

Choose a reason for hiding this comment

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

Added another nit and agree with @joelanford's suggestions

olm.NewOLMVariableSource(mgr.GetClient()),
),
}).SetupWithManager(mgr); err != nil {
olm.NewOLMVariableSource(mgr.GetClient())),
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Keep the solver.NewDeppySolver closing parenthesis on it's own line. IMO it makes it easier to see where the call stops at a glance

@ankitathomas ankitathomas force-pushed the catalog-unit-test branch 2 times, most recently from 79f38db to d695d11 Compare June 9, 2023 18:26
Expect(err).To(BeNil())

opNames = []string{"prometheus", "project-quay"}
reqChan = make(chan string, 4*len(opNames))
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need the buffer to be 4*len(opNames)? I don't think its a huge deal, but IIUC we could probably reduce this to just len(opNames) since a receiver will be using the channel after each "reconcile" that occurs.

Comment on lines 46 to 47
err = controllers.SetupWithManager(fakeReconciler, mgr)
Expect(err).To(BeNil())
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit (stolen from a nit @joelanford left on one of my PRs but I really liked it so 🤷 ): For functions that just return an error, I think something like:

Suggested change
err = controllers.SetupWithManager(fakeReconciler, mgr)
Expect(err).To(BeNil())
Expect(controllers.SetupWithManager(fakeReconciler, mgr)).To(Succeed())

looks a bit nicer

})
It("reconciles all affected operators on cluster", func() {
By("creating a new catalog")
catalog := &catalogd.Catalog{ObjectMeta: metav1.ObjectMeta{Name: "t"}, Spec: catalogd.CatalogSpec{Source: catalogd.CatalogSource{Type: catalogd.SourceTypeImage, Image: &catalogd.ImageSource{}}}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Make this take up multiple lines - this is a really long line and is more difficult to follow than if it was on multiple lines

internal/controllers/catalog_test.go Show resolved Hide resolved
Signed-off-by: Ankita Thomas <ankithom@redhat.com>
@openshift-merge-robot openshift-merge-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jun 24, 2023
@openshift-merge-robot
Copy link

PR needs rebase.

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/test-infra repository.

@perdasilva
Copy link
Contributor

@ankitathomas can you please rebase?

@ncdc ncdc changed the title Catalog unit test 🌱 Catalog unit test Nov 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants