-
Notifications
You must be signed in to change notification settings - Fork 1
support managed control plane anti-affinity groups #69
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,8 +43,10 @@ type OxideMachineSpec struct { | |
| ImageID string `json:"imageID"` | ||
|
|
||
| // AntiAffinityGroups is the list of anti-affinity group names or IDs that the Oxide instance | ||
| // should use. Anti-affinity groups should be created outside the operator, else instance | ||
| // creation will fail. | ||
| // should use. Anti-affinity groups specified should already be created, else instance | ||
| // creation will fail. A control plane anti-affinity group managed via the OxideCluster's | ||
| // ControlPlaneAntiAffinityPolicy field is joined automatically and doesn't need to be listed | ||
| // here. | ||
| // https://docs.oxide.computer/guides/deploying-workloads#_affinity_and_anti_affinity | ||
|
Comment on lines
45
to
50
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| // +optional | ||
| // +listType=set | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,21 @@ spec: | |
| spec: | ||
| description: spec defines the desired state of OxideCluster | ||
| properties: | ||
| controlPlaneAntiAffinityPolicy: | ||
| description: |- | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm unsure whether these are generated so if the Go doc comments syntax messes that up let me know. |
||
| ControlPlaneAntiAffinityPolicy, when set, makes the controller manage an anti-affinity | ||
| group for the cluster's control plane machines so that their instances are spread across | ||
| sleds. The group is created in the cluster's project before the cluster is marked | ||
| provisioned, joined by every control plane instance at creation, and deleted with the | ||
| cluster. "allow" permits co-location when the spread cannot be satisfied; "fail" refuses | ||
| to place the instance instead. Note that the policy is applied when the group is first | ||
| created: the Oxide API doesn't support updating a group's policy, so changing this field | ||
| doesn't affect an existing group, and instances only join the group as they are created. | ||
| https://docs.oxide.computer/guides/deploying-workloads#_affinity_and_anti_affinity | ||
| enum: | ||
| - allow | ||
| - fail | ||
| type: string | ||
| controlPlaneEndpoint: | ||
| description: |- | ||
| ControlPlaneEndpoint represents the host and port of the cluster's control plane. If | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -113,11 +113,7 @@ func (r *OxideClusterReconciler) Reconcile( | |
| ipName := getFloatingIPName(oxideCluster) | ||
|
|
||
| if !oxideCluster.DeletionTimestamp.IsZero() { | ||
| if err := r.ensureFloatingIPDeleted(ctx, oxideClient, projectName, ipName); err != nil { | ||
| return ctrl.Result{}, fmt.Errorf("deleting floating ip: %w", err) | ||
| } | ||
| controllerutil.RemoveFinalizer(oxideCluster, infrav1.ClusterFinalizer) | ||
| return ctrl.Result{}, retErr | ||
| return ctrl.Result{}, r.reconcileDelete(ctx, oxideClient, oxideCluster, projectName, ipName) | ||
| } else if !cluster.DeletionTimestamp.IsZero() { | ||
| log.Info( | ||
| "cluster is being deleted, aborting OxideCluster reconcile", | ||
|
|
@@ -140,10 +136,23 @@ func (r *OxideClusterReconciler) Reconcile( | |
| oxideCluster.Spec.ControlPlaneEndpoint.Port = 6443 | ||
| } | ||
|
|
||
| // Reconcile the control plane anti-affinity group, if enabled. This must happen before the | ||
| // cluster is marked provisioned below: upstream CAPI controllers won't create Machines until | ||
| // then, and instance creation fails if the machine references a group that doesn't exist. | ||
| if err := r.reconcileAntiAffinityGroup( | ||
| ctx, | ||
| oxideClient, | ||
| oxideCluster, | ||
| projectName, | ||
| ); err != nil { | ||
| return ctrl.Result{}, fmt.Errorf("ensuring anti-affinity group: %w", err) | ||
| } | ||
|
|
||
| // We consider the OxideCluster to be Ready when the Oxide infrastructure that it manages (i.e. | ||
| // the floating IP) is provisioned. Note that upstream CAPI controllers won't provision | ||
| // Machine/OxideMachine resources until the OxideCluster is Ready, so we must mark Ready here | ||
| // rather than waiting for OxideMachines to be provisioned and attached. | ||
| // the floating IP and control plane anti-affinity group) is provisioned. Note that upstream | ||
| // CAPI controllers won't provision Machine/OxideMachine resources until the OxideCluster is | ||
| // Ready, so we must mark Ready here rather than waiting for OxideMachines to be provisioned | ||
| // and attached. | ||
| oxideCluster.Status.Initialization.Provisioned = new(true) | ||
| conditions.Set(oxideCluster, metav1.Condition{ | ||
| Type: clusterv1.ReadyCondition, | ||
|
|
@@ -345,6 +354,115 @@ func (r *OxideClusterReconciler) ensureFloatingIPExists( | |
| return ip, nil | ||
| } | ||
|
|
||
| // reconcileDelete deletes the Oxide resources owned by the OxideCluster and removes the | ||
| // finalizer. | ||
| func (r *OxideClusterReconciler) reconcileDelete( | ||
| ctx context.Context, | ||
| oxideClient cloud.OxideClient, | ||
| oxideCluster *infrav1.OxideCluster, | ||
| projectName string, | ||
| ipName string, | ||
| ) error { | ||
| if err := r.ensureFloatingIPDeleted(ctx, oxideClient, projectName, ipName); err != nil { | ||
| return fmt.Errorf("deleting floating ip: %w", err) | ||
| } | ||
| // Delete the control plane anti-affinity group unconditionally rather than only when the | ||
| // policy is currently set, so a group created before the policy was unset is cleaned up too. | ||
| // CAPI deletes the cluster's Machines before its infrastructure, so the group has no members | ||
| // by the time this runs. | ||
|
Comment on lines
+369
to
+372
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has the side effect of deleting an anti-affinity group that may have never been managed by CAPI to begin with. LLM scans flagged similar behavior in other integrations but it's a difficult problem to solve without state or resource tags.
I'm really just asking to understand, not saying we have to change this. It may be a perfectly fine security boundary to say that we expect to manage resources that are prefixed with |
||
| if err := r.ensureAntiAffinityGroupDeleted( | ||
| ctx, | ||
| oxideClient, | ||
| projectName, | ||
| getAntiAffinityGroupName(oxideCluster), | ||
| ); err != nil { | ||
| return fmt.Errorf("deleting anti-affinity group: %w", err) | ||
| } | ||
| controllerutil.RemoveFinalizer(oxideCluster, infrav1.ClusterFinalizer) | ||
| return nil | ||
| } | ||
|
|
||
| // reconcileAntiAffinityGroup ensures the control plane anti-affinity group exists when the | ||
| // cluster opts in via ControlPlaneAntiAffinityPolicy, and is a no-op otherwise. | ||
| func (r *OxideClusterReconciler) reconcileAntiAffinityGroup( | ||
| ctx context.Context, | ||
| oxideClient cloud.OxideClient, | ||
| oxideCluster *infrav1.OxideCluster, | ||
| projectName string, | ||
| ) error { | ||
| if oxideCluster.Spec.ControlPlaneAntiAffinityPolicy == "" { | ||
| return nil | ||
| } | ||
| _, err := r.ensureAntiAffinityGroupExists( | ||
| ctx, | ||
| oxideClient, | ||
| oxideCluster, | ||
| projectName, | ||
| getAntiAffinityGroupName(oxideCluster), | ||
| ) | ||
| return err | ||
| } | ||
|
|
||
| // ensureAntiAffinityGroupExists creates or views the control plane anti-affinity group. An | ||
| // existing group is adopted as is: the Oxide API doesn't support updating a group's policy, so | ||
| // drift between the group and ControlPlaneAntiAffinityPolicy isn't reconciled. | ||
|
Comment on lines
+406
to
+408
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the other side of the delete comment. We could adopt some anti-affinity group that just so happened to be named the same. |
||
| func (r *OxideClusterReconciler) ensureAntiAffinityGroupExists( | ||
| ctx context.Context, | ||
| oxideClient cloud.OxideClient, | ||
| oxideCluster *infrav1.OxideCluster, | ||
| projectName string, | ||
| groupName string, | ||
| ) (*oxide.AntiAffinityGroup, error) { | ||
| group, err := oxideClient.AntiAffinityGroupView(ctx, oxide.AntiAffinityGroupViewParams{ | ||
| Project: oxide.NameOrId(projectName), | ||
| AntiAffinityGroup: oxide.NameOrId(groupName), | ||
| }) | ||
| if err != nil { | ||
| if !errors.Is(err, oxide.ErrObjectNotFound) { | ||
| return nil, fmt.Errorf("fetching existing anti-affinity group: %w", err) | ||
| } | ||
|
|
||
| group, err = oxideClient.AntiAffinityGroupCreate(ctx, oxide.AntiAffinityGroupCreateParams{ | ||
| Project: oxide.NameOrId(projectName), | ||
| Body: &oxide.AntiAffinityGroupCreate{ | ||
| Name: oxide.Name(groupName), | ||
| Description: fmt.Sprintf( | ||
| "Control plane anti-affinity for CAPI cluster %s/%s", | ||
| oxideCluster.Namespace, | ||
| oxideCluster.Name, | ||
| ), | ||
| FailureDomain: oxide.FailureDomainSled, | ||
| Policy: oxide.AffinityPolicy( | ||
| oxideCluster.Spec.ControlPlaneAntiAffinityPolicy, | ||
| ), | ||
| }, | ||
| }) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("creating anti-affinity group: %w", err) | ||
| } | ||
| } | ||
|
|
||
| return group, nil | ||
| } | ||
|
|
||
| // ensureAntiAffinityGroupDeleted deletes the control plane anti-affinity group if it exists. | ||
| func (r *OxideClusterReconciler) ensureAntiAffinityGroupDeleted( | ||
| ctx context.Context, | ||
| oxideClient cloud.OxideClient, | ||
| projectName string, | ||
| groupName string, | ||
| ) error { | ||
| if err := oxideClient.AntiAffinityGroupDelete(ctx, oxide.AntiAffinityGroupDeleteParams{ | ||
| Project: oxide.NameOrId(projectName), | ||
| AntiAffinityGroup: oxide.NameOrId(groupName), | ||
| }); err != nil { | ||
| if !errors.Is(err, oxide.ErrObjectNotFound) { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // ensureFloatingIPDeleted deletes the floating IP if it exists. | ||
| func (r *OxideClusterReconciler) ensureFloatingIPDeleted( | ||
| ctx context.Context, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.