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

operator: Add Keepalived Controller (v2) #3864

Merged
merged 34 commits into from
Sep 15, 2022

Conversation

TeddyAndrieux
Copy link
Collaborator

@TeddyAndrieux TeddyAndrieux commented Sep 7, 2022

(Second version of #3856, this time we have a dedicated CRD for VirtualIPPool)

  • Move ClusterConfig controller logic to pkg to ease "muli controller" approach
  • Add some utils to manage Objects
  • Add Go variable in operator with MetalK8s version
  • Add the new VirtualIPPool API and resource
  • Handle High-Level ConfigMap and DaemonSet creation/update/deletion from VirtualIPPool controller
  • Handle MetalK8s VIP Namespace creation/update/deletion from ClusterConfig controller
  • Handle VirtualIPPool creation/update/deletion from ClusterConfig controller
  • Add (and update) some status on ClusterConfig and VirtualIPPool CRs
  • Add some events on ClusterConfig and VirtualIPPool CRs
  • Add end-to-end tests about VIPs management
  • Add documentation about VIPs management

NOTE: For reviewers, you should review commits one by one

NOTE: Some features will be handled later (in other PRs)

  • Spread constraints on the VIPs
  • CNI reconfiguration (likely once we will have migrated to Tigera operator, to manage the CNI)
  • Ingress server certificate "reconfiguration"

@bert-e
Copy link
Contributor

bert-e commented Sep 7, 2022

Hello teddyandrieux,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Status report is not available.

@bert-e
Copy link
Contributor

bert-e commented Sep 7, 2022

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

@TeddyAndrieux TeddyAndrieux changed the title operator: Add Keepalived Controller v operator: Add Keepalived Controller (v2) Sep 7, 2022
@TeddyAndrieux TeddyAndrieux marked this pull request as ready for review September 7, 2022 16:36
@TeddyAndrieux TeddyAndrieux requested a review from a team as a code owner September 7, 2022 16:36
if err != nil {
r.setVIPConfiguredCondition(
metav1.ConditionFalse,
"NamespaceDeletionError",
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 suggest using an enum for the various conditions in the types definition (hence it can be documented in the CRD as well) and will uses error handling for clients such as the UI

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For VirtualIPPool object, yes we can but for ClusterConfig, I was thinking about only one "global" condition Ready and then the SubReconciler can manage the Conditions they want, but ... 🤔 I can still do an enum, it just means that if a SubReconciler add a Condition they need to update the ENUM.

Ok fine, I will do it

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure it is possible in Golang but can an Enum extend another one ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So I did not define a dedicated Type for it, since it was a mess then because we couldn't have used the metav1.Condition type directly (or we should have built a function to convert from one to the other). But I defined a const close to the CRD per conditions

operator/pkg/controller/virtualippool/controller.go Outdated Show resolved Hide resolved
}
}
// There is no more Virtual Router ID free
return -1
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if using the return tuple (err, result) pattern wouldn't be more idiomatic in go

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe, but not sure it's worth it here, it will make the usage of the function a bit more complex and it's really a case that should not happen (a user that wants to use more than 255 VIPs at the same time 😕 )

return utils.Requeue(err)
}
for _, pool := range pools.Items {
r.retrieveVRID(ctx, pool)
Copy link
Contributor

Choose a reason for hiding this comment

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

Shall we handle the possible error here ?

Copy link
Contributor

Choose a reason for hiding this comment

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

If I understand correctly the sequence here r.retrieveVRID is mutating r.usedVRID so when we use it we have to be aware of the exact sequence we need to follow. IMO we should try to avoid this imperative paradigm. Maybe we can use an intermediate facade 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Shall we handle the possible error here ?

Indeed, I forgot about it, I will add it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree it's a bit weird, but I do not see any "clean" solution to do it

If I understand correctly the sequence here r.retrieveVRID is mutating r.usedVRID

Yes

so when we use it we have to be aware of the exact sequence we need to follow

WDYM ?

To explain a bit the behavior here, we retrieve all the defined pools, we iter on those pools to retrieve the "potential" already defined VRID to "cache" them, so that if we need to pick a new VRID we pick one that is not already used

Maybe we can use an intermediate facade

I'm not sure to understand how it could work, except if we have "something" that basically manages all the VirtualIPPools at once, but it makes things complicated and not really good as well IMHO.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Shall we handle the possible error here ?

Done

// those nodes
number := math.Ceil(float64(len(r.instance.Spec.Addresses)) / float64(len(nodes)))
for node := range nodes {
nodes[node] = int(number)
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 casting to int shouldn't we round ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We already Ceil, not sure round is better than casting to int in this case, but we could cast to int only once when "computing" number, I will change it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I moved the cast to int to do it only once


// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *VirtualIPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I'm not a big fan of naming vars with such short name, reviewing it is not sufficiently explicit in my mind

Suggested change
func (r *VirtualIPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
func (virtualIPPoolReconciler *VirtualIPPoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree, but it's kind of "standard" in Go from what I saw

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, it seems that the short name indicates the object to which the current method applies. In storage-operator, we replaced it with self, similar to what we use in Python. Not sure which is best, but I think it's useful to differenciate this "method holder" from other variables.

@TeddyAndrieux TeddyAndrieux force-pushed the improvement/add-keepalived-controller-v2 branch 10 times, most recently from 3696efd to a55d019 Compare September 13, 2022 06:09
So that we have a dedicated go `package` for this controller (and
variable&co do not overlap between controllers)
This ObjectHandler allow to use a single function to Create, Update and
Delete several objects, it also set some standard metadata like Labels
and Owner.

It use the same "mutate" function logic as the
`controllerutil.CreateOrUpdate` function
Using operator-sdk
```
operator-sdk create api --version v1alpha1 --kind VirtualIPPool --resource --controller --namespaced=True
make generate
make manifests
make metalk8s
```
@TeddyAndrieux TeddyAndrieux force-pushed the improvement/add-keepalived-controller-v2 branch 3 times, most recently from 782a592 to c447811 Compare September 13, 2022 09:28
Copy link
Contributor

@gdemonet gdemonet left a comment

Choose a reason for hiding this comment

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

Just a few minor changes, except for the spreading algorithm which I believe is wrong atm, otherwise LGTM 🙂

operator/api/v1alpha1/virtualippool_types.go Show resolved Hide resolved
// TODO(user): your logic here

return utils.EndReconciliation()
}

func (r *VirtualIPPoolReconciler) retrieveVRID(ctx context.Context, pool metalk8sscalitycomv1alpha1.VirtualIPPool) error {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: the name is slightly misleading, we could expect to get a VRID as a result, but it's actually just storing them in "the cache"... Maybe cacheUsedVRIDs instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, a quick comment explaining why we use this "cache" pattern could avoid questions in the future, or eager attempts to simplify it (IIUC, we need it as a consequence of the "magic create/update/delete with a mutate handler" pattern)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

IIUC, we need it as a consequence of the "magic create/update/delete with a mutate handler" pattern

Not really, we need it, because we do not want to use the same VRID as another VirtualIPPool VIP so we retrieve all the VRID from all the VirtualIPPools

I will rename it and add a comment

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done (already squashed 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.

Yes that part I got, but the fact that we need it attached to the reconciler as a struct field is only because of the mutate, right? Because we refresh this cache on every Reconcile call, no? If we don't, then we may be missing invalidation (when we remove a pool / an address from a pool).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh yes indeed, it could be just a variable in the reconcile function and then we add a "lambda" function so that we give this argument to the mutate, but it will be almost the same. And indeed it's re-computed at every reconcile loop

Comment on lines 229 to 357
// Spread the remaining nodes
for node, nb := range nodes {
for i := 0; i < nb; i++ {
for index := range desired.Addresses {
if desired.Addresses[index].Node == "" {
desired.Addresses[index].Node = node
break
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this loop slightly wrong? Imagine we have 3 nodes, and 4 VIPs to spread. Ceil(4/3) = 2, so we'll have nb = 2. Which means, IIUC, that we'll assign 2 VIPs to the first node, 2 more to the second node, and none to the third 😕

Is there any use case where we'd have a different nb per node selected in a pool? I don't remember any...
If not, I'd suggest we refactor the getNodesList method to only return a list of candidate node names, and compute the number (max per node) only once, in this mutate handler. Then the loops would be switched around:

for i := 0; i < maxAddressesPerNode; i++ {
    for _, node := range nodes {
        // assign one address
    }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Isn't this loop slightly wrong? Imagine we have 3 nodes, and 4 VIPs to spread. Ceil(4/3) = 2, so we'll have nb = 2. Which means, IIUC, that we'll assign 2 VIPs to the first node, 2 more to the second node, and none to the third

🤔 hmm indeed, ok I will fix it

Is there any use case where we'd have a different nb per node selected in a pool? I don't remember any...
If not, I'd suggest we refactor the getNodesList method to only return a list of candidate node names, and compute the number (max per node) only once, in this mutate handler.

The fact is that we do not want to change the address if it's already assigned to one node, see the loop above

		if addr.Node != "" {
			// The node is already defined
			if nodes[addr.Node] == 0 {
				// This node is not available, remove it
				addr.Node = ""
			} else {
				nodes[addr.Node]--
			}
		}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done 2c7edac

Comment on lines 85 to 107
pools := metalk8sscalitycomv1alpha1.VirtualIPPoolList{}
if err := r.client.List(ctx, &pools); err != nil {
return utils.Requeue(err)
}
for _, pool := range pools.Items {
if err := r.retrieveVRID(ctx, pool); err != nil {
return utils.Requeue(err)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

An idea comes to mind, but let's keep it for another PR.

I'm actually concerned by this kind of logic within a Reconcile pass: we should be reconciling a single instance, without having to retrieve all instances again. I think we should be fine because the client already has a cache built in, so there wouldn't be an extra call sent to K8s API (neither for the "list pools" nor for the "get configmap").
Still, it would be cleaner (IMO) to have each Pool reconciliation update the cache (about the VRIDs it's using) without having to recreate it on each pass for each Pool.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree that it's not ideal but I don't see any better approach.

Still, it would be cleaner (IMO) to have each Pool reconciliation update the cache (about the VRIDs it's using) without having to recreate it on each pass for each Pool.

It will not work well, it means the first VirtualIPPool to get reconciled may take one VRID already taken by another Pool, or we need to load all the pools at the startup of the controller but not ideal as well.

TBH, we will never have tonnes of VirtualIPPools at once so I don't think it's really a problem, even if I agree it's not ideal.

operator/pkg/controller/clusterconfig/virtualippool.go Outdated Show resolved Hide resolved
operator/pkg/controller/virtualippool/controller.go Outdated Show resolved Hide resolved
Comment on lines 26 to 28
configuredConditionName = "Configured"
availableConditionName = "Available"
readyConditionName = "Ready"
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if that's the right place, or if we should add it to the description in OpenAPI spec, but it would be nice to explain each condition type and their nuances (e.g. Available = at least one up, Ready = all up).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok I will add some explanation

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done d79b19c

operator/pkg/controller/utils/reconcile_test.go Outdated Show resolved Hide resolved
Re-generate go code and manifests using:
```
make generate
make manifests
make metalk8s
```
Re-generate go code and manifests using:
```
make generate
make manifests
make metalk8s
```
Re-generate go code and manifests using:
```
make generate
make manifests
make metalk8s
```
Since, today, the CNI config and Ingress server certificate are managed
by Salt and we want to use the ClusterConfig Virtual IPs to access the
Workload Plane Ingress, retrieve those configured Virtual IPs in Salt so
that we use them to setup CNI and Ingress server certificate
```
make manifests
make metalk8s
```
```
make manifests
make metalk8s
```
@TeddyAndrieux TeddyAndrieux force-pushed the improvement/add-keepalived-controller-v2 branch from 2e23ee5 to 2ea50ae Compare September 14, 2022 19:32
@TeddyAndrieux
Copy link
Collaborator Author

/approve

@bert-e
Copy link
Contributor

bert-e commented Sep 14, 2022

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • one peer

Peer approvals must include at least 1 approval from the following list:

The following reviewers are expecting changes from the author, or must review again:

The following options are set: approve

Copy link
Contributor

@gdemonet gdemonet left a comment

Choose a reason for hiding this comment

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

💯 :shipit:

@bert-e
Copy link
Contributor

bert-e commented Sep 15, 2022

In the queue

The changeset has received all authorizations and has been added to the
relevant queue(s). The queue(s) will be merged in the target development
branch(es) as soon as builds have passed.

The changeset will be merged in:

  • ✔️ development/124.0

The following branches will NOT be impacted:

  • development/123.0
  • development/2.0
  • development/2.1
  • development/2.10
  • development/2.11
  • development/2.2
  • development/2.3
  • development/2.4
  • development/2.5
  • development/2.6
  • development/2.7
  • development/2.8
  • development/2.9

There is no action required on your side. You will be notified here once
the changeset has been merged. In the unlikely event that the changeset
fails permanently on the queue, a member of the admin team will
contact you to help resolve the matter.

IMPORTANT

Please do not attempt to modify this pull request.

  • Any commit you add on the source branch will trigger a new cycle after the
    current queue is merged.
  • Any commit you add on one of the integration branches will be lost.

If you need this pull request to be removed from the queue, please contact a
member of the admin team now.

The following options are set: approve

@bert-e
Copy link
Contributor

bert-e commented Sep 15, 2022

I have successfully merged the changeset of this pull request
into targetted development branches:

  • ✔️ development/124.0

The following branches have NOT changed:

  • development/123.0
  • development/2.0
  • development/2.1
  • development/2.10
  • development/2.11
  • development/2.2
  • development/2.3
  • development/2.4
  • development/2.5
  • development/2.6
  • development/2.7
  • development/2.8
  • development/2.9

Please check the status of the associated issue None.

Goodbye teddyandrieux.

@bert-e bert-e merged commit 2ea50ae into development/124.0 Sep 15, 2022
@bert-e bert-e deleted the improvement/add-keepalived-controller-v2 branch September 15, 2022 08:30
Comment on lines +103 to +131
// Set a condition on VirtualIPPool
func (v *VirtualIPPool) SetCondition(kind string, status metav1.ConditionStatus, reason string, message string) {
setCondition(v.Generation, &v.Status.Conditions, kind, status, reason, message)
}

// Get a condition from VirtualIPPool
func (v *VirtualIPPool) GetCondition(kind string) *Condition {
return getCondition(v.Status.Conditions, kind)
}

// Set Configured Condition
func (v *VirtualIPPool) SetConfiguredCondition(status metav1.ConditionStatus, reason string, message string) {
v.SetCondition(configuredConditionName, status, reason, message)
}

// Set Available Condition
func (v *VirtualIPPool) SetAvailableCondition(status metav1.ConditionStatus, reason string, message string) {
v.SetCondition(availableConditionName, status, reason, message)
}

// Set Ready Condition
func (v *VirtualIPPool) SetReadyCondition(status metav1.ConditionStatus, reason string, message string) {
v.SetCondition(readyConditionName, status, reason, message)
}

// Get Ready Condition
func (v *VirtualIPPool) GetReadyCondition() *Condition {
return v.GetCondition(readyConditionName)
}

Choose a reason for hiding this comment

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

just a minor note: randomly i saw this PR, not understand much but you can remove the comment, the function/method name are clean enough so self-documented

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agree, but it's good practice in Golang to have a comment for every "exported" method (I'm not sure we do have a comment for all of them here... but ideally we should)

golint would complain if we do not have such comments (we should add golint validation in the CI btw)

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.

5 participants