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}