Skip to main content

zlayer_types/api/
cluster.rs

1//! Cluster join / membership wire DTOs.
2//!
3//! Lifted from `zlayer-api::handlers::cluster` so the CLI, the manager UI,
4//! and any other client can describe these requests/responses without
5//! depending on `zlayer-api`. The handler itself stays in `zlayer-api`.
6
7use serde::{Deserialize, Serialize};
8use utoipa::ToSchema;
9
10use crate::api::nodes::GpuInfoSummary;
11use crate::spec::{ArchKind, OsKind};
12
13/// Request body for `POST /api/v1/cluster/join`.
14#[derive(Debug, Clone, Deserialize, ToSchema)]
15pub struct ClusterJoinRequest {
16    /// Base64-encoded join token (contains `auth_secret` for validation)
17    pub token: String,
18    /// Joining node's advertise address (IP)
19    pub advertise_addr: String,
20    /// Joining node's overlay port (`WireGuard`)
21    pub overlay_port: u16,
22    /// Joining node's Raft RPC port
23    pub raft_port: u16,
24    /// Joining node's API server port
25    #[serde(default = "default_api_port")]
26    pub api_port: u16,
27    /// Joining node's `WireGuard` public key
28    pub wg_public_key: String,
29    /// Node mode: "full" or "replicate"
30    #[serde(default = "default_mode")]
31    pub mode: String,
32    /// Services to replicate (only if mode == "replicate")
33    pub services: Option<Vec<String>>,
34    /// Total CPU cores on the joining node
35    #[serde(default)]
36    pub cpu_total: f64,
37    /// Total memory in bytes
38    #[serde(default)]
39    pub memory_total: u64,
40    /// Total disk in bytes
41    #[serde(default)]
42    pub disk_total: u64,
43    /// Detected GPUs
44    #[serde(default)]
45    pub gpus: Vec<GpuInfoSummary>,
46    /// Operating system of the joining agent. `None` = legacy client that did
47    /// not report platform info.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub os: Option<OsKind>,
50    /// CPU architecture of the joining agent. Same legacy semantics as `os`.
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub arch: Option<ArchKind>,
53    /// Joiner's 32-byte X25519 pubkey for sealed-box DEK wrapping.
54    /// Present on Phase-1+ joiners; absent on legacy clients (in which
55    /// case the leader treats the node as not eligible to host
56    /// replicated-secret ciphertext until it re-joins with a pubkey).
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub secrets_pubkey: Option<[u8; 32]>,
59}
60
61#[must_use]
62pub fn default_mode() -> String {
63    "full".to_string()
64}
65
66#[must_use]
67pub fn default_api_port() -> u16 {
68    3669
69}
70
71/// Response body for `POST /api/v1/cluster/join`.
72#[derive(Debug, Clone, Serialize, ToSchema)]
73pub struct ClusterJoinResponse {
74    /// Assigned node UUID
75    pub node_id: String,
76    /// Assigned Raft node ID (monotonic u64)
77    pub raft_node_id: u64,
78    /// Assigned overlay IP for the new node
79    pub overlay_ip: String,
80    /// Per-node slice CIDR assigned by the leader (e.g. "10.200.42.0/28").
81    /// Empty string if the leader is not slice-aware yet.
82    #[serde(default)]
83    pub slice_cidr: String,
84    /// Existing peers in the cluster
85    pub peers: Vec<ClusterPeer>,
86    /// Role assigned to this node: "voter" or "learner"
87    pub role: String,
88    /// Node JWT minted by the leader for this joiner — `roles: ["node"]`,
89    /// `node_id` set. Used to authenticate inter-node calls separately
90    /// from any user identity. `None` on legacy responses.
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub node_jwt: Option<String>,
93    /// Sealed-box-wrapped copy of the cluster DEK addressed to the
94    /// joiner's `secrets_pubkey`. The joiner unwraps with its node X25519
95    /// private key and holds the DEK in zeroized memory. `None` on legacy
96    /// responses or when the joiner did not provide a `secrets_pubkey`.
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub wrapped_dek: Option<Vec<u8>>,
99    /// Cluster DEK generation that `wrapped_dek` was sealed under. Lets
100    /// the joiner detect rotation drift if it re-joins after a revocation
101    /// rotated the cluster DEK. `None` when `wrapped_dek` is `None`.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub dek_generation: Option<u64>,
104}
105
106/// Summary of an existing cluster peer returned in join response.
107#[derive(Debug, Clone, Serialize, ToSchema)]
108pub struct ClusterPeer {
109    /// UUID
110    pub node_id: String,
111    /// Raft node ID
112    pub raft_node_id: u64,
113    /// Advertise address
114    pub advertise_addr: String,
115    /// Overlay port
116    pub overlay_port: u16,
117    /// Raft port
118    pub raft_port: u16,
119    /// `WireGuard` public key
120    pub wg_public_key: String,
121    /// Overlay IP
122    pub overlay_ip: String,
123}
124
125/// Summary of a cluster node for listing.
126#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
127pub struct ClusterNodeSummary {
128    /// UUID or Raft-level ID
129    pub id: String,
130    /// Network address (Raft RPC address)
131    pub address: String,
132    /// Advertise address (public IP)
133    pub advertise_addr: String,
134    /// Current status (e.g. "ready", "draining", "dead")
135    pub status: String,
136    /// Role in the Raft cluster: "leader", "voter", or "learner"
137    pub role: String,
138    /// Join mode: "full" or "replicate"
139    pub mode: String,
140    /// Whether this node is the Raft leader
141    pub is_leader: bool,
142    /// Overlay network IP assigned to this node
143    pub overlay_ip: String,
144    /// Total CPU cores on this node
145    pub cpu_total: f64,
146    /// Current CPU usage (cores)
147    pub cpu_used: f64,
148    /// Total memory in bytes
149    pub memory_total: u64,
150    /// Current memory usage in bytes
151    pub memory_used: u64,
152    /// When the node was registered (Unix timestamp ms)
153    pub registered_at: u64,
154    /// Last heartbeat timestamp (Unix timestamp ms)
155    pub last_heartbeat: u64,
156}