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

Test subnet controller #177

Merged
merged 35 commits into from
Oct 28, 2021
Merged

Test subnet controller #177

merged 35 commits into from
Oct 28, 2021

Conversation

LimKianAn
Copy link
Contributor

Closes #176

@LimKianAn LimKianAn self-assigned this Oct 21, 2021
@LimKianAn LimKianAn changed the title Set up suite WIP Test subnet controller Oct 21, 2021
@LimKianAn LimKianAn force-pushed the 176-test-subnet-controller branch from 21be879 to 73eca72 Compare October 21, 2021 14:25
@LimKianAn LimKianAn force-pushed the 176-test-subnet-controller branch from 73eca72 to 06ac011 Compare October 21, 2021 14:31
@LimKianAn LimKianAn force-pushed the 176-test-subnet-controller branch from 1cdad42 to dfb8d81 Compare October 21, 2021 20:30
@LimKianAn LimKianAn requested a review from adracus October 22, 2021 18:54
@LimKianAn LimKianAn changed the title WIP Test subnet controller Test subnet controller Oct 22, 2021
@LimKianAn
Copy link
Contributor Author

@GrigoriyMikhalkin FYI

controllers/network/suite_test.go Show resolved Hide resolved
RegisterFailHandler(Fail)

RunSpecsWithDefaultAndCustomReporters(t,
"Controller Suite",
Copy link
Contributor

Choose a reason for hiding this comment

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

Change the name to "Network Controller Suite"? Because we have multiple subpackages in controllers package.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed

controllers/network/subnet_controller_test.go Show resolved Hide resolved
controllers/network/subnet_controller_test.go Show resolved Hide resolved
"reflect"
"time"

"github.com/onmetal/onmetal-api/apis/common/v1alpha1"
Copy link
Contributor

@GrigoriyMikhalkin GrigoriyMikhalkin Oct 22, 2021

Choose a reason for hiding this comment

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

Let's use goimports for the code with --local github.com/onmetal/onmetal-api flag. what you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. For VSCode users, add the following to settings.json.

    "go.formatTool": "goimports",
    "gopls": {
        "formatting.local": "github.com/onmetal/onmetal-api",
    },

)

var _ = Describe("subnet controller", func() {
Context("reconcileExists", 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 would merge it with "Reconcile" context.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My idea was:
Context: function name
It: what the function does
By: steps in the function

Like here it's more like a function name to me.

Sill open to discussion.

Copy link
Contributor

Choose a reason for hiding this comment

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

The whole point of using Ginkgo is to implement behavior driven testing. So we don't need to use function names in organizational units(i.e. Context, It, etc). Instead we mention behavior. Like Context("Reconcile"), which, at least usually, is top level definition of testable behavior in controller tests.

Copy link
Contributor

Choose a reason for hiding this comment

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

P.S. I also, would say, that unit testing is undesirable, as theoretically it can be broken without breaking desired behavior. It's always preferred to test general behavior of the component)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Normally function names describe behaviors in short and people do use function names in Ginkgo's constructs, like here, here and here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean by this?

it can be broken without breaking desired behavior

Copy link
Contributor

@GrigoriyMikhalkin GrigoriyMikhalkin Oct 25, 2021

Choose a reason for hiding this comment

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

What do you mean by this?

it can be broken without breaking desired behavior

For example, some refactoring is done, part of functionality moved to another function called from calling function. Unit test will be broken, general behavior will not.

Copy link
Contributor

Choose a reason for hiding this comment

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

Point is, we test behavior not functions)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A function name should describe the behaviour in short. I don't see a conflict here. Yet, since we don't have finalizers here and don't have to do some clean-up after the deletion, the context reconcileExists is indeed too thin.

Copy link
Contributor

@adracus adracus left a comment

Choose a reason for hiding this comment

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

Some required changes. In general, try to avoid Ginkgo assertions that yield bool: If they fail, you'll get an error message like

expected 'true' to be 'false'

or else, which isn't very helpful. The richer Ginkgo assertions give back understandable messages which makes troubleshooting a lot easier. If you ever can't get around boolean assertions, make sure to include a hint in the matcher, like

Expect(myResult).To(BeTrue(), "the 'my-operation' operation did not succeed")


Eventually(func() bool {
rngGot := &nw.IPAMRange{}
Expect(k8sClient.Get(ctx, objectKey(ipamRange), rngGot))
Copy link
Contributor

Choose a reason for hiding this comment

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

.To(Succeed()) assertion is missing

Eventually(func() bool {
rngGot := &nw.IPAMRange{}
Expect(k8sClient.Get(ctx, objectKey(ipamRange), rngGot))
return rngGot.Spec.CIDRs == nil
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd do

return rngGot.Spec.CIDRs

And then as assertion, instead of BeTrue BeEmpty or BeNil. In case the slice is not empty, there will be a better error message what the contained items are.

It("finishes reconciliation early if the instance is being deleted", func() {
subnet := newSubnet("early-finished")
subnet.DeletionTimestamp = now()
ipamRange := newIPAMRange(subnet)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should not create the IPAMRange ourselves and instead wait for the Subnet controller to create its IPAMRange object (it does so via server-side apply).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK! I didn't get this bit and thought the IPAMRange is already generated and only get patched by Subnet controller. Thanks!

Context("reconcileExists", func() {
It("finishes reconciliation early if the instance is being deleted", func() {
subnet := newSubnet("early-finished")
subnet.DeletionTimestamp = now()
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of creating it w/ a deletion timestamp set to now, we should create it, wait for the IPAMRange to be created and then delete it to see if it's gone afterwards (it should be gone due to us correctly setting up owner references).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Due to the limit of envtest only the owner reference of the owned IPAMRange is checked.

subnet := newSubnet("no-parant")
ipamRange := newIPAMRange(subnet)

Expect(k8sClient.Create(ctx, ipamRange)).Should(Succeed())
Copy link
Contributor

Choose a reason for hiding this comment

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

We shouldn't create the IPAMRange manually and instead let the controller do this.

Expect(k8sClient.Create(ctx, parent)).Should(Succeed())
Expect(k8sClient.Create(ctx, child)).Should(Succeed())

By("patching the spec of the owned IPAMRange")
Copy link
Contributor

Choose a reason for hiding this comment

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

By("waiting for the parent IPAMRange to have an allocation by the child IPAMRange")

Comment on lines 99 to 112
return func() bool {
parentRng := rngGot.Spec.Parent
parentSubnet := child.Spec.Parent
if parentRng == nil || parentSubnet == nil {
return false
}

// Check if the IPAMRange is patched
return parentRng.Name == nw.SubnetIPAMName(parentSubnet.Name) &&
rngGot.Spec.CIDRs == nil &&
reflect.DeepEqual(rngGot.Spec.Requests[0], nw.IPAMRangeRequest{CIDR: &child.Spec.Ranges[0].CIDR})

}()
}, timeout, interval).Should(BeTrue())
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of this block, just use

  return rngGot.Spec.Requests
}, timeout, interval).Should(ContainElement(child.Spec.Ranges[0].CIDR})

}()
}, timeout, interval).Should(BeTrue())

By("patching the status of the Subnet")
Copy link
Contributor

Choose a reason for hiding this comment

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

By("waiting for the subnet to report status 'up'") + modify the assertion as described in the parent-less example.

func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)

RunSpecsWithDefaultAndCustomReporters(t,
Copy link
Contributor

Choose a reason for hiding this comment

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

Use RunSpecs and remove the custom reporter. That feature won't be available in the latest Ginkgo version anymore.

var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

ctx, cancel = context.WithCancel(context.TODO())
Copy link
Contributor

Choose a reason for hiding this comment

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

context.TODO() -> context.Background()

@LimKianAn LimKianAn requested a review from adracus October 26, 2021 09:15
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())

err = (&IPAMRangeReconciler{
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a question not a call to action) Is IPAMRangeReconciler needed for testing SubnetReconciler?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the moment, no, but eventually, yes. The reconciler of Subnet will check the status of the IPAMRange and update the Subnet accordingly. (here) There might be a bug here which I will address in another issue.

Copy link
Contributor

@adracus adracus left a comment

Choose a reason for hiding this comment

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

Some minor changes, otherwise looks pretty good!


ipamRng := newIPAMRange(subnet)
Eventually(func() error {
return k8sClient.Get(ctx, objectKey(ipamRng), ipamRng)
Copy link
Contributor

Choose a reason for hiding this comment

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

Two problems here:

  1. If the Get ever fails, the ipamRng object might be reset, deleting its metadata, resulting in the objectKey returning an empty client.ObjectKey, resulting in always getting Not Found. The key should be created outside the func().
  2. On errors other than apierrors.IsNotFound, the test should error early, as that's a non-recoverable error.
    Please make sure to check all Get + Eventually combinations.

ipamRng := newIPAMRange(subnet)
Eventually(func() error {
return k8sClient.Get(ctx, objectKey(ipamRng), ipamRng)
}, timeout, interval).Should(BeNil())
Copy link
Contributor

Choose a reason for hiding this comment

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

BeNil() -> Succeed()

@LimKianAn LimKianAn requested a review from adracus October 27, 2021 09:54
@adracus adracus merged commit e83d486 into main Oct 28, 2021
@adracus adracus deleted the 176-test-subnet-controller branch October 28, 2021 09:14
@afritzler afritzler added the enhancement New feature or request label Jan 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request network size/L
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Test subnet controller
4 participants