Skip to content

docs(adr): add ADR for Active/Standby HA controller (#293)#405

Open
sumanthd032 wants to merge 2 commits into
kubeslice:masterfrom
sumanthd032:docs/ha-active-standby
Open

docs(adr): add ADR for Active/Standby HA controller (#293)#405
sumanthd032 wants to merge 2 commits into
kubeslice:masterfrom
sumanthd032:docs/ha-active-standby

Conversation

@sumanthd032

Copy link
Copy Markdown

Add the design record for multi-cluster Active/Standby HA of the kubeslice-controller: one Active hub holds a Lease and does all writes while a Standby mirrors its state and watches the Lease, promoting itself on Active failure.

Fixes #293

How Has This Been Tested?

N/A - documentation-only change (ADR). No code paths are modified, so there is nothing to run. Reviewed by rendering the Markdown, verifying the two sequence diagrams, and checking each decision against the required-answers table in issue #293.

Checklist:

  • The title of the PR states what changed and the related issues number (used for the release note).
  • Does this PR requires documentation updates?
  • I've updated documentation as required by this PR.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have tested it for all user roles.
  • I have added all the required unit test cases.

Does this PR introduce a breaking change for other components like worker-operator?

No. This is a design document only and changes no runtime behavior.

Add the design record for multi-cluster Active/Standby HA of the
kubeslice-controller: one Active hub holds a Lease and does all writes
while a Standby mirrors its state and watches the Lease, promoting itself
on Active failure.

Signed-off-by: Sumanth D <sumanthd032@gmail.com>
@sumanthd032 sumanthd032 marked this pull request as ready for review July 3, 2026 12:04
Copilot AI review requested due to automatic review settings July 3, 2026 12:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds an Architecture Decision Record (ADR) documenting the proposed multi-cluster Active/Standby HA design for the KubeSlice controller, capturing key decisions and expected operational behavior as a prerequisite for the downstream implementation work.

Changes:

  • Introduces an ADR describing the Active/Standby hub architecture, goals/non-goals, and core components.
  • Documents explicit design decisions for lease placement, credential bootstrap, fencing, promotion triggers, state mirroring, worker reconnection, and split-brain handling.
  • Adds Mermaid-based architecture + sequence diagrams and a table enumerating mirrored resources.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/adr/ha-active-standby-adr.md Outdated
Comment thread docs/adr/ha-active-standby-adr.md
@sumanthd032 sumanthd032 marked this pull request as draft July 3, 2026 17:15
@sumanthd032 sumanthd032 marked this pull request as ready for review July 3, 2026 17:18
@gourishkb gourishkb requested a review from rajendra-avesha July 6, 2026 13:28
@sumanthd032

Copy link
Copy Markdown
Author

@pnavali @gourishkb - review comments addressed and the ADR is updated. Please do a final review before merging. Thanks!

@pnavali pnavali requested review from gourishkb and removed request for that-backend-guy July 8, 2026 11:07
Signed-off-by: Sumanth D <sumanthd032@gmail.com>
@sumanthd032 sumanthd032 force-pushed the docs/ha-active-standby branch from d2b4318 to 43dcc20 Compare July 8, 2026 17:18

@gourishkb gourishkb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good structure overall — the scope note correctly dismissing --leader-elect as orthogonal is exactly right, and the decision to mirror Secrets/RBAC/Namespaces (not just CRDs) is the kind of detail that prevents a promoted hub from being stuck waiting for credential propagation. A few things need resolution before this gates implementation.

Must clarify before #294/#295/#297 start

  1. status.activeController ambiguity. Decision 7 and the diagram describe this field as living on the Active hub's Cluster CR (ac_cluster in the flowchart), yet the worker is supposed to watch it to learn the new Active. But the worker can't reach the hub's API when the hub is down — that's the exact failure scenario. Please clarify: does the new Active write this signal to a CR on each worker cluster after promotion (so the worker watches a local CR), or does it live on the hub (and if so, how does the worker read it when the hub is unreachable)? This needs to be precise because worker-operator #467 depends on it.

  2. Namespace: kubeslice-controller vs kubeslice-system. Decision 2 places the active-hub-kubeconfig Secret in namespace kubeslice-controller, but the controller deploys into kubeslice-system. Please verify and use the correct namespace consistently throughout the ADR.

  3. Open Issue #2 (finalizers / stuck Terminating objects) must be resolved before any code merges. This isn't something that can be addressed post-MVP. If StateMirror writes objects with finalizers (e.g., Cluster, SliceConfig) and those objects are then deleted on the Active, the mirror propagates the deletion to the Standby. But on the Standby, the finalizer controllers are gated (IsLeader()=false), so the object sits in Terminating forever. This leaks state and corrupts the Standby's view of the world. The ADR should propose a resolution — stripping finalizers from mirrored copies, or skipping the deletion mirror and relying on a periodic prune-and-resync — before implementation begins.

Should fix

  1. Background goroutine gating is underspecified. Decision 4 mentions gating reconcilers and says "background writers (e.g. the VpnKeyRotation timer) start only when IsLeader()". But main.go also starts other background processes: the metrics server, health-check server, leader-election renewer. Which processes run on both Active and Standby, and which are gated? The ADR should enumerate all goroutines started by main.go and explicitly mark each as "always runs", "Active only", or "Standby only". This directly determines how --ha-mode changes main.go's startup path.

  2. ENABLE_WEBHOOKS=false on Standby — security note. Disabling webhooks on the Standby so StateMirror can write objects without triggering validation is pragmatic, but it means that if an operator mistakenly applies a SliceConfig directly to the Standby while it is in Standby role, that invalid config will be accepted. Please add a note in the ADR that the Standby's API server should ideally be unreachable from external clients (network policy or no exposed route), and that ENABLE_WEBHOOKS=false is a deployment-level guard, not a security boundary.

Minor

  1. The recovery path ("a recovered failed hub re-joins as Standby by manual redeploy") is mentioned in one sentence. This deserves a paragraph in the Operational Model or Non-Goals section — specifically: what state does the recovered hub have, how does an operator wipe and redeploy it as Standby, and what happens if the operator skips the wipe step and redeploys without clearing the old Lease.

end

sb_watcher -. "watch renewTime (remote client)" .-> ac_lease
sb_mirror -. "mirror CRDs + certs + RBAC + SAs + Secrets" .-> active

@gourishkb gourishkb Jul 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This kubeconfig Secret is placed in kubeslice-controller namespace, but the controller runs in kubeslice-system. Retractedkubeslice-controller is the correct namespace for the hub controller; kubeslice-system is the worker-side namespace. The namespace in Decision 2 is correct as written.

localClient client.Client // own cluster — renew the Lease
remoteClient client.Client // Standby only — watch the Active's Lease
mode HAMode // Active | Standby | Standalone
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Decision 4 says every reconciler has an IsLeader() gate, but doesn't say the check is per-call (not cached at startup). Issue #294 explicitly requires this. Please add: "IsLeader() is evaluated on every invocation of Reconcile(), not once at startup, because leadership can change while the controller is running."

|---|---|
| `Project`, `Cluster`, `SliceConfig`, `ServiceExportConfig`, `SliceQoSConfig`, `VpnKeyRotation` | `controller.kubeslice.io` |
| `WorkerSliceConfig`, `WorkerSliceGateway`, `WorkerServiceImport` | `worker.kubeslice.io` |
| Gateway VPN cert `Secret`s (OpenVPN/WireGuard), CA/issuer `Secret`s, worker kubeconfig `Secret`s | `core/v1` |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Open Issue #2 (finalizers on mirrored objects causing stuck Terminating) needs a proposed resolution before any code review for #295 will be approved. The simplest fix: StateMirror strips all finalizers from mirrored copies before writing them to the Standby. Since reconcilers are gated on the Standby, finalizer controllers will never run there anyway — so carrying finalizers on the mirror is only harmful. Please add this as a decision (or an explicit resolution of this open issue) before implementation starts.

**Exception:** hub-issued SA **tokens** are not mirrored. A token signed by the Active is invalid on the Standby, so the mirror carries the `ServiceAccount` + RBAC (the identity and authorization) and each hub mints its own token (Decision 7).

### 7. How do workers find the new Active after failover?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The status.activeController signal lives on the Active hub's Cluster CR in the diagram, but the worker needs to read this when the hub is down. Please clarify the actual delivery path: does the promoted Standby write a signal to a CR on each worker cluster (so the worker watches locally), or is there a different mechanism? This is the most critical implementation detail in #467 — worker-operator #467 cannot be designed until this is resolved.

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.

Design: Active/Standby HA for KubeSlice Controller (ADR)

3 participants