Skip to main content

zlayer_types/api/
internal.rs

1//! Internal API DTOs for scheduler-to-agent communication.
2//!
3//! These types describe the request/response payloads for the internal
4//! endpoints used by the distributed scheduler to trigger operations on
5//! agents. They use a shared secret for authentication rather than JWT
6//! tokens.
7
8use serde::{Deserialize, Serialize};
9use utoipa::ToSchema;
10
11/// Re-export the wire-level scale request from `crate::cluster`.
12///
13/// `InternalScaleRequest` was moved to `zlayer_types::cluster` so the same
14/// Rust type can be shared between the HTTP fan-out path (in
15/// `zlayer-scheduler::cluster`) and the `/internal/scale` handler in
16/// `zlayer-api`. This re-export preserves the original
17/// `zlayer_types::api::internal::InternalScaleRequest` path for downstream
18/// callers (and `pub use ...::internal::*` consumers).
19pub use crate::cluster::{InternalScaleRequest, ScaleAssignment};
20
21/// Response from internal scale operation
22#[derive(Debug, Serialize, ToSchema)]
23pub struct InternalScaleResponse {
24    /// Whether the operation succeeded
25    pub success: bool,
26    /// Service name that was scaled
27    pub service: String,
28    /// New replica count
29    pub replicas: u32,
30    /// Optional message
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub message: Option<String>,
33    /// When set, this agent refused the scale because it cannot run the
34    /// workload's OS (H-7 `RouteToPeer` policy). The value is the OCI-canonical
35    /// OS string the workload requires (`linux` / `windows` / `darwin`). The
36    /// scheduler catches this and re-dispatches to a cluster peer whose
37    /// `NodeState.os` matches.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub reroute_to_os: Option<String>,
40}
41
42/// Request to add a `WireGuard` peer to the local overlay transport.
43///
44/// Sent by the leader to existing nodes when a new node joins the cluster,
45/// so that all nodes learn about the new peer without waiting for periodic
46/// reconciliation.
47#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
48pub struct InternalAddPeerRequest {
49    /// New peer's `WireGuard` public key (base64)
50    pub wg_public_key: String,
51    /// New peer's overlay IP (e.g. "10.200.0.3")
52    pub overlay_ip: String,
53    /// New peer's `WireGuard` endpoint (e.g. "203.0.113.5:51820")
54    pub endpoint: String,
55    /// When set, this peer is for the named service's *dedicated* overlay
56    /// rather than the global cluster overlay.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub service: Option<String>,
59    /// Service subnet to plumb into the dedicated peer's `AllowedIPs`.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub service_subnet: Option<String>,
62    /// NAT-traversal candidates the joining node advertises (host /
63    /// server-reflexive / relay addresses it can be reached on). The leader
64    /// fans these out to existing nodes via `/internal/add-peer`; each node
65    /// hands them to overlayd so `NatTraversal::connect_to_peer` can
66    /// hole-punch / relay toward the new peer when its direct endpoint does not
67    /// establish a `WireGuard` handshake. Empty for a pre-NAT sender;
68    /// `#[serde(default)]` keeps such payloads decoding.
69    #[serde(default, skip_serializing_if = "Vec::is_empty")]
70    pub candidates: Vec<crate::nat_wire::NatCandidateWire>,
71}
72
73/// Response from internal add-peer operation
74#[derive(Debug, Serialize, ToSchema)]
75pub struct InternalAddPeerResponse {
76    /// Whether the operation succeeded
77    pub success: bool,
78    /// Optional message
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub message: Option<String>,
81}
82
83/// Op type for the secrets Raft state machine.
84///
85/// Replicated through openraft alongside the existing scheduler ops.
86/// `zlayer-consensus` carries the bytes; `zlayer-secrets`'s `raft_sm.rs`
87/// applies them. The variants intentionally mirror the structure of
88/// [`crate::storage::NodeIdentity`], [`crate::storage::WrappedDek`], and
89/// [`crate::storage::ReplicatedSecret`] so the wire shape is identical to
90/// the stored shape.
91#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
92#[serde(rename_all = "snake_case")]
93pub enum SecretsRaftOp {
94    /// Register a new node. Triggers an automatic re-wrap of the current
95    /// DEK so the new node can decrypt secrets going forward.
96    RegisterNode {
97        /// Identity payload (uuid, X25519 pubkey, WG pubkey, `joined_at`).
98        identity: crate::storage::NodeIdentity,
99    },
100
101    /// Soft-revoke a node. Followers stop including it in DEK wraps; the
102    /// next `RotateDek` excludes it permanently.
103    RevokeNode {
104        /// Cluster-wide node UUID being revoked.
105        node_id: String,
106    },
107
108    /// Rotate the cluster DEK. The leader proposes a new generation with
109    /// fresh per-node wraps; followers re-encrypt every `ReplicatedSecret`
110    /// from the previous generation to the new one.
111    RotateDek {
112        /// New wrapped-DEK envelope (generation + per-node wraps).
113        new_wraps: crate::storage::WrappedDek,
114    },
115
116    /// Insert or update a secret. The ciphertext is encrypted under the
117    /// `dek_generation` recorded inside the payload.
118    PutSecret {
119        /// The full replicated secret record.
120        secret: crate::storage::ReplicatedSecret,
121    },
122
123    /// Remove a secret entirely. Hard delete — re-encryption skips it.
124    DeleteSecret {
125        /// `"{scope}:{name}"` storage key, same shape as elsewhere.
126        storage_key: String,
127    },
128
129    /// Revoke a specific issued join token (cannot be unrevoked).
130    ///
131    /// The token is identified by `token_hash`, which is the lowercase
132    /// hex SHA-256 of the full token envelope b64 string (same hash form
133    /// regardless of token format — Ed25519-signed envelope, HS256-JWT,
134    /// or future EdDSA-JWT). The entry auto-expires at `expires_at` so
135    /// the revocation table stays bounded by the un-expired token horizon.
136    RevokeToken {
137        /// Lowercase hex SHA-256 of the full token b64 envelope string.
138        token_hash: String,
139        /// Wall-clock instant at which the revocation entry may be pruned.
140        /// Should match the token's own `exp` claim so the entry is no
141        /// longer needed once the token would have expired anyway.
142        #[schema(value_type = String, format = "date-time")]
143        expires_at: chrono::DateTime<chrono::Utc>,
144    },
145
146    /// Import a foreign cluster's trust bundle so its tokens can be
147    /// accepted by validators on this cluster.
148    ///
149    /// Idempotent: re-importing the same `cluster_domain` overwrites
150    /// the previous entry. Keyed by `cluster_domain` to enforce one
151    /// trust relationship per foreign cluster.
152    ImportTrustBundle {
153        /// The bundle to record in `SecretsState::trusted_bundles`.
154        bundle: crate::api::cluster::TrustBundle,
155    },
156
157    /// Remove a previously-imported trust bundle.
158    ///
159    /// No-op if `cluster_domain` was not present. Used by the operator
160    /// when revoking trust in a federated cluster.
161    RemoveTrustBundle {
162        /// Cluster domain of the bundle to remove.
163        cluster_domain: String,
164    },
165
166    /// Set the cluster-wide JWT algorithm policy.
167    ///
168    /// Replicated through Raft so every node enforces the same policy
169    /// within one commit. Idempotent — re-applying with the same value
170    /// is a no-op.
171    SetJwtAlgorithm {
172        /// New policy.
173        algorithm: crate::api::cluster::JwtAlgorithm,
174    },
175
176    /// Mark `{data_dir}/join_secret` as wiped on every node.
177    ///
178    /// Operator-driven cleanup after migrating to `eddsa`. The actual
179    /// file-system delete happens locally on each node when this op
180    /// applies; the state machine records the wipe timestamp so
181    /// re-applies are no-ops. Idempotent.
182    WipeJoinSecret,
183}