Skip to content

Add command: cluster enroll to streamline node migration - #1110

Merged
cgalibern merged 4 commits into
opensvc:mainfrom
cgalibern:feat/cluster-enroll
Sep 11, 2026
Merged

cgalibern merged 4 commits into
opensvc:mainfrom
cgalibern:feat/cluster-enroll

Conversation

@cgalibern

Copy link
Copy Markdown
Contributor

Summary

This pull request introduces functionality to enroll a foreign node into an existing cluster, enabling seamless migration of a node from one cluster to another from the target cluster's perspective. It includes the following key changes:

  • Command Addition: Adds the cluster enroll command, allowing operators to initiate the enrollment process via an API call.

    • Token Handling: Supports --token-file for secure token provision, avoiding risks of exposing sensitive information via the process table.
    • Join Feedback: Implements a robust feedback mechanism using NodeAlive to ensure the node has successfully joined the cluster.
  • API Enhancements:

    • Adds a POST /cluster/enroll endpoint for initiating the join request.
    • Introduces a POST /node/name/{nodename}/daemon/action/join endpoint to accept and process the enrollment action on the candidate node.
  • Cluster Join Updates:

    • Introduces --addr for explicitly specifying the target node API location when resolving nodenames is not feasible.
    • Allows token retrieval from the OSVC_JOIN_TOKEN environment variable as an alternative to passing it via the command line.

Two limits show up once the join is driven by a daemon rather than by a
sysadmin at a shell.

The api location was always built from the --node value, so joining a
cluster whose nodenames this node can not resolve was impossible.
Add --addr to give that location explicitly, in the
[<scheme>://]<addr>[:<port>] format the client already parses, leaving
--node as the nodename the join protocol carries.

The token could only come from --token, and a token on the command line
is readable by any user through the process table. Read OSVC_JOIN_TOKEN
when --token is unset, and drop the required mark on --token so either
source satisfies the command.
Moving a node from one cluster to another meant a shell on that node,
running om cluster join with a token pasted from the target cluster.
Nothing let an operator drive the move from the cluster the node joins.

Add two endpoints:

  * POST /cluster/enroll, on a node of the target cluster. Its body
    carries the location of the node to enroll and an access token with
    the join role created on it. The ca claim of that token is the trust
    anchor for its certificate: its cluster is not ours, a proxy client
    only reaches a cluster node, and only a token carrying the join role
    holds the claim.

  * POST /node/name/{nodename}/daemon/action/join, on the node to
    enroll, granted to the join role like the endpoint adding a node to
    our own cluster nodes. It validates the order and forks a background
    om cluster join, because the join stops the daemon serving the
    request.

The enroll handler reads cluster.nodes on the candidate before it
creates anything. That request doubles as the preflight: it proves the
node is reachable, that the token is valid there, and that its ca claim
does anchor its certificate. A join token suffices for that read, which
assertGuest already accepts. A candidate that still has peers is refused
with a 409, because nothing in the join flow tells them to drop it from
their cluster.nodes and they would keep it forever. The single node
precondition also means cluster.nodes holds exactly the nodename the
join events will carry, which the response returns so the caller can
filter on it.

The order carries the location the candidate must reach us at, so it
does not have to resolve our nodename. The join_addr parameter gives it
explicitly and is refused when our certificate is not valid for its
host: VerifyHostname is the function the tls client of the candidate
runs, so we predict its verdict instead of letting a mismatch surface
later, inside the join forked on that node, where the operator sees
nothing. Without the parameter, the location is picked from the names our
certificate is valid for. A name designating us is preferred, since the
certificate is a cluster wide object whose plain names are usually those
of the node that bootstrapped the cluster; a wildcard is expanded with
our own label, which makes such a certificate designate each node of the
cluster in turn. The loopback address the certificate always carries is
skipped: the candidate cannot reach us there. The port comes from what
our listener actually bound when the name designates us, and from the
cluster configuration otherwise, a peer having bound a port of its own
that we do not know.

The token reaches the forked join through the environment, never through
its command line.

The candidate re-checks its own node count: the enroll side only sees a
snapshot taken over the network, and this endpoint is reachable on its
own.
Add o[mx] cluster enroll, driving POST /cluster/enroll from a node of
the target cluster. It lives in commoncmd because it only calls the api,
so both binaries get it, unlike cluster join and cluster leave which act
on the local node.

--token-file is preferred over --token: a token on the command line is
kept in the shell history and readable through the process table.

--wait, on by default, waits for NodeAlive rather than JoinSuccess.
JoinSuccess only says the api node updated its cluster.nodes, which
happens before the enrolled node has drained, stopped, reconfigured or
restarted. NodeAlive says its heartbeat beats here, which is what proves
the join step is completed.

The event reader is opened before the post, since JoinSuccess can be
published before the response reaches us, and the events are matched
client side because the nodename is only known from that response.
NodeAlive is labeled with the node observing the heartbeat, not the
beating one, so that match is on the payload.

A JoinSuccess never followed by a NodeAlive is reported as its own
failure rather than a timeout: the cluster lists the node, its heartbeat
is not beating, and the node is where to look.
@cgalibern cgalibern closed this Sep 9, 2026
@cgalibern cgalibern reopened this Sep 9, 2026
@cgalibern cgalibern changed the title Add "cluster enroll" command to streamline node migration Add command: cluster enroll to streamline node migration Sep 9, 2026
@cgalibern cgalibern closed this Sep 9, 2026
@cgalibern cgalibern reopened this Sep 9, 2026
@cgalibern cgalibern closed this Sep 11, 2026
@cgalibern cgalibern reopened this Sep 11, 2026
@cgalibern cgalibern closed this Sep 11, 2026
@cgalibern cgalibern reopened this Sep 11, 2026
@cgalibern cgalibern closed this Sep 11, 2026
@cgalibern cgalibern reopened this Sep 11, 2026
@cgalibern cgalibern closed this Sep 11, 2026
@cgalibern cgalibern reopened this Sep 11, 2026
@cgalibern cgalibern closed this Sep 11, 2026
@cgalibern cgalibern reopened this Sep 11, 2026
@cgalibern cgalibern closed this Sep 11, 2026
@cgalibern cgalibern reopened this Sep 11, 2026
@cgalibern
cgalibern merged commit f1dc422 into opensvc:main Sep 11, 2026
9 checks passed
@cgalibern
cgalibern deleted the feat/cluster-enroll branch September 15, 2026 08:33
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.

1 participant