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

🌱 Reduce cyclomatic complexity of ReconcileLoadBalancer #1904

Merged
merged 1 commit into from
Feb 28, 2024

Conversation

mdbooth
Copy link
Contributor

@mdbooth mdbooth commented Feb 22, 2024

This function had become genuinely too complex over time, to the point that even the linter was starting to complain about it when making almost any change.

This change refactors ReconcileLoadBalancer into several smaller logical functions which are much easier to read and reason about. It also revealed some trivial optimisations:

  • Only fetch Octavia providers if we need them to create a new loadbalancer
  • Only calculate allowed CIDRs once
  • Don't re-fetch a loadbalancer to check it's active if it's already active

Apart from just the code cleanup, this has the benefit of not requiring these changes to be mixed into other PRs when the complexity exceeds the linter's threshold.

/hold

@k8s-ci-robot k8s-ci-robot added do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Feb 22, 2024
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: mdbooth

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 /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Feb 22, 2024
Copy link

netlify bot commented Feb 22, 2024

Deploy Preview for kubernetes-sigs-cluster-api-openstack ready!

Name Link
🔨 Latest commit e8ccd21
🔍 Latest deploy log https://app.netlify.com/sites/kubernetes-sigs-cluster-api-openstack/deploys/65de485fd2f4b0000862af65
😎 Deploy Preview https://deploy-preview-1904--kubernetes-sigs-cluster-api-openstack.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@k8s-ci-robot k8s-ci-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Feb 22, 2024

// getAPIServerVIPAddress gets the VIP address for the API server from wherever it is specified.
// Returns an empty string if the VIP address is not specified and it should be allocated automatically.
func getAPIServerVIPAddress(openStackCluster *infrav1.OpenStackCluster) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

we could unit test it but I'm not going to nitpick on that. Just make sure the existing tests cover the output of that function and I'm fine.

Copy link
Contributor

Choose a reason for hiding this comment

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

I wrote it.


// getAPIServerFloatingIP gets the floating IP from wherever it is specified.
// Returns an empty string if the floating IP is not specified and it should be allocated automatically.
func getAPIServerFloatingIP(openStackCluster *infrav1.OpenStackCluster) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

Copy link
Contributor

Choose a reason for hiding this comment

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

I wrote it.

// getCanonicalAllowedCIDRs gets a filtered list of CIDRs which should be allowed to access the API server loadbalancer.
// Invalid CIDRs are filtered from the list and emil a warning event.
// It returns a canonical representation that can be directly compared with other canonicalized lists.
func getCanonicalAllowedCIDRs(openStackCluster *infrav1.OpenStackCluster) []string {
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

Copy link
Contributor

Choose a reason for hiding this comment

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

I wrote it.

}

func (s *Service) getOrCreateLoadBalancer(openStackCluster *infrav1.OpenStackCluster, loadBalancerName, subnetID, clusterName, vipAddress, provider string) (*loadbalancers.LoadBalancer, error) {
// getOrCreateAPILoadBalancer returns an existing API loadbalancer if it already exists, or creates a new one if it does not.
func (s *Service) getOrCreateAPILoadBalancer(openStackCluster *infrav1.OpenStackCluster, clusterName string) (*loadbalancers.LoadBalancer, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

Copy link
Contributor

Choose a reason for hiding this comment

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

I wrote it.

@@ -230,7 +307,41 @@ func (s *Service) getOrCreateLoadBalancer(openStackCluster *infrav1.OpenStackClu
return lb, nil
}

func (s *Service) getOrCreateListener(openStackCluster *infrav1.OpenStackCluster, listenerName, lbID string, port int) (*listeners.Listener, error) {
// reconcileAPILoadBalancerListener ensures that the listener on the given port exists and is configured correctly.
func (s *Service) reconcileAPILoadBalancerListener(lb *loadbalancers.LoadBalancer, openStackCluster *infrav1.OpenStackCluster, clusterName string, port int) error {
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 have unit test coverage for this one?

Copy link
Contributor

Choose a reason for hiding this comment

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

no we don't but it's not an easy one so I've filled #1906 so we can get back to it at some point.

Copy link
Contributor

@EmilienM EmilienM left a comment

Choose a reason for hiding this comment

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

Awesome change, I just have questions on unit tests coverage.

@mdbooth
Copy link
Contributor Author

mdbooth commented Feb 26, 2024

/cc @MaysaMacedo as this touches some of your code.

@mdbooth
Copy link
Contributor Author

mdbooth commented Feb 26, 2024

I 100% agree about unit test coverage. However, this is basically just code motion so I can separate these changes from the other changes I'm making to the same code without triggering the gocyclo linter. IOW this is just yak shaving for me right now.

If you fancied taking the yak shears and writing some good unit tests while I get back to the API stuff I'd add to the beers I owe you.

@k8s-ci-robot k8s-ci-robot added size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. and removed size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Feb 26, 2024
@EmilienM
Copy link
Contributor

/lgtm
I'll let another reviewer take a look.

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Feb 27, 2024
@mdbooth
Copy link
Contributor Author

mdbooth commented Feb 27, 2024

/hold cancel

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Feb 27, 2024
return false, fmt.Errorf("load balancer %q with id %s is not active after timeout: %v", loadBalancerName, lb.ID, err)
if lb.ProvisioningStatus != loadBalancerProvisioningStatusActive {
var err error
lb, err = s.waitForLoadBalancerActive(lb.ID)
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need to return the lb again if it was already retrieved at line 79?
Looks like all the places that need the LB after this line 91 look for the provider, the ID or VipPortID of the LB, so I believe returning it again is unnecessarily.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it's a different object, returned from the most recent query. It's probably not necessary, but I figured that as we already had the latest state we might as well use it.

e.g. If we don't do this and for whatever reason we happened to look at ProvisioningStatus again, we would see that it's still not active, because we're still referencing the original version of the object before it became active.

return fmt.Errorf("APIServerLoadBalancer is not yet available in OpenStackCluster.Status")
}

allowedCIDRs := openStackCluster.Status.APIServerLoadBalancer.AllowedCIDRs
Copy link
Contributor

Choose a reason for hiding this comment

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

has the allowedCIDRs been updated in the openStackCluster previously?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. It was set directly in ReconcileLoadBalancer before reconciling any listeners.

}

// getCanonicalAllowedCIDRs gets a filtered list of CIDRs which should be allowed to access the API server loadbalancer.
// Invalid CIDRs are filtered from the list and emil a warning event.
Copy link
Contributor

Choose a reason for hiding this comment

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

typo emil


return false, nil
// Filter invalid CIDRs and convert any IPs into CIDRs.
Copy link
Contributor

Choose a reason for hiding this comment

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

couldn't we keep using validateIPs function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a toss-up, but I thought it was too small to be a separate function with only one caller. This function is small enough now, I think it's clearer to inline it here.

Also it doesn't just validate, it also filters, which you wouldn't know just from its name so you'd have to go read the function intentionally to notice that.

@MaysaMacedo
Copy link
Contributor

/hold

@mdbooth holding just to give a chance for you to check my comments, feel free to unhold once you checked them

@k8s-ci-robot k8s-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Feb 27, 2024
Copy link
Contributor Author

@mdbooth mdbooth left a comment

Choose a reason for hiding this comment

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

Thanks, @MaysaMacedo. I fixed the typo and responded to the other comments. Let me know what you think.

return false, fmt.Errorf("load balancer %q with id %s is not active after timeout: %v", loadBalancerName, lb.ID, err)
if lb.ProvisioningStatus != loadBalancerProvisioningStatusActive {
var err error
lb, err = s.waitForLoadBalancerActive(lb.ID)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it's a different object, returned from the most recent query. It's probably not necessary, but I figured that as we already had the latest state we might as well use it.

e.g. If we don't do this and for whatever reason we happened to look at ProvisioningStatus again, we would see that it's still not active, because we're still referencing the original version of the object before it became active.

return fmt.Errorf("APIServerLoadBalancer is not yet available in OpenStackCluster.Status")
}

allowedCIDRs := openStackCluster.Status.APIServerLoadBalancer.AllowedCIDRs
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. It was set directly in ReconcileLoadBalancer before reconciling any listeners.


return false, nil
// Filter invalid CIDRs and convert any IPs into CIDRs.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a toss-up, but I thought it was too small to be a separate function with only one caller. This function is small enough now, I think it's clearer to inline it here.

Also it doesn't just validate, it also filters, which you wouldn't know just from its name so you'd have to go read the function intentionally to notice that.

@k8s-ci-robot k8s-ci-robot removed the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Feb 27, 2024
@MaysaMacedo
Copy link
Contributor

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Feb 27, 2024
This function had become genuinely too complex over time, to the point
that even the linter was starting to complain about it when making
almost any change.

This change refactors ReconcileLoadBalancer into several smaller logical
functions which are much easier to read and reason about. It also
revealed some trivial optimisations:
* Only fetch Octavia providers if we need them to create a new
  loadbalancer
* Only calculate allowed CIDRs once
* Don't re-fetch a loadbalancer to check it's active if it's already
  active

Co-Authored-By: Emilien Macchi <emacchi@redhat.com>
@k8s-ci-robot k8s-ci-robot removed the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Feb 27, 2024
@EmilienM
Copy link
Contributor

Restoring Maysa's LGTM, fixing unit tests and removing /hold.
/lgtm
/hold cancel

@k8s-ci-robot k8s-ci-robot added lgtm "Looks good to me", indicates that a PR is ready to be merged. and removed do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. labels Feb 28, 2024
@k8s-ci-robot k8s-ci-robot merged commit 05d4e9b into kubernetes-sigs:main Feb 28, 2024
9 checks passed
allowedCIDRs = listener.AllowedCIDRs
}

if openStackCluster.Status.Router != nil && len(openStackCluster.Status.Router.IPs) > 0 {
Copy link

Choose a reason for hiding this comment

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

This fixes a bug we just hit when specifying both internalNetwork.networkFilter and apiServerLoadBalancer.allowedCIDRs. Thanks for adding this nil guard!

@EmilienM EmilienM deleted the ReconcileLoadBalancer branch May 27, 2024 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files.
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

None yet

5 participants