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}
63
64/// Response from internal add-peer operation
65#[derive(Debug, Serialize, ToSchema)]
66pub struct InternalAddPeerResponse {
67    /// Whether the operation succeeded
68    pub success: bool,
69    /// Optional message
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub message: Option<String>,
72}
73
74/// Op type for the secrets Raft state machine.
75///
76/// Replicated through openraft alongside the existing scheduler ops.
77/// `zlayer-consensus` carries the bytes; `zlayer-secrets`'s `raft_sm.rs`
78/// applies them. The variants intentionally mirror the structure of
79/// [`crate::storage::NodeIdentity`], [`crate::storage::WrappedDek`], and
80/// [`crate::storage::ReplicatedSecret`] so the wire shape is identical to
81/// the stored shape.
82#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
83#[serde(rename_all = "snake_case")]
84pub enum SecretsRaftOp {
85    /// Register a new node. Triggers an automatic re-wrap of the current
86    /// DEK so the new node can decrypt secrets going forward.
87    RegisterNode {
88        /// Identity payload (uuid, X25519 pubkey, WG pubkey, `joined_at`).
89        identity: crate::storage::NodeIdentity,
90    },
91
92    /// Soft-revoke a node. Followers stop including it in DEK wraps; the
93    /// next `RotateDek` excludes it permanently.
94    RevokeNode {
95        /// Cluster-wide node UUID being revoked.
96        node_id: String,
97    },
98
99    /// Rotate the cluster DEK. The leader proposes a new generation with
100    /// fresh per-node wraps; followers re-encrypt every `ReplicatedSecret`
101    /// from the previous generation to the new one.
102    RotateDek {
103        /// New wrapped-DEK envelope (generation + per-node wraps).
104        new_wraps: crate::storage::WrappedDek,
105    },
106
107    /// Insert or update a secret. The ciphertext is encrypted under the
108    /// `dek_generation` recorded inside the payload.
109    PutSecret {
110        /// The full replicated secret record.
111        secret: crate::storage::ReplicatedSecret,
112    },
113
114    /// Remove a secret entirely. Hard delete — re-encryption skips it.
115    DeleteSecret {
116        /// `"{scope}:{name}"` storage key, same shape as elsewhere.
117        storage_key: String,
118    },
119
120    /// Revoke a specific issued join token (cannot be unrevoked).
121    ///
122    /// The token is identified by `token_hash`, which is the lowercase
123    /// hex SHA-256 of the full token envelope b64 string (same hash form
124    /// regardless of token format — Ed25519-signed envelope, HS256-JWT,
125    /// or future EdDSA-JWT). The entry auto-expires at `expires_at` so
126    /// the revocation table stays bounded by the un-expired token horizon.
127    RevokeToken {
128        /// Lowercase hex SHA-256 of the full token b64 envelope string.
129        token_hash: String,
130        /// Wall-clock instant at which the revocation entry may be pruned.
131        /// Should match the token's own `exp` claim so the entry is no
132        /// longer needed once the token would have expired anyway.
133        #[schema(value_type = String, format = "date-time")]
134        expires_at: chrono::DateTime<chrono::Utc>,
135    },
136
137    /// Import a foreign cluster's trust bundle so its tokens can be
138    /// accepted by validators on this cluster.
139    ///
140    /// Idempotent: re-importing the same `cluster_domain` overwrites
141    /// the previous entry. Keyed by `cluster_domain` to enforce one
142    /// trust relationship per foreign cluster.
143    ImportTrustBundle {
144        /// The bundle to record in `SecretsState::trusted_bundles`.
145        bundle: crate::api::cluster::TrustBundle,
146    },
147
148    /// Remove a previously-imported trust bundle.
149    ///
150    /// No-op if `cluster_domain` was not present. Used by the operator
151    /// when revoking trust in a federated cluster.
152    RemoveTrustBundle {
153        /// Cluster domain of the bundle to remove.
154        cluster_domain: String,
155    },
156
157    /// Set the cluster-wide JWT algorithm policy.
158    ///
159    /// Replicated through Raft so every node enforces the same policy
160    /// within one commit. Idempotent — re-applying with the same value
161    /// is a no-op.
162    SetJwtAlgorithm {
163        /// New policy.
164        algorithm: crate::api::cluster::JwtAlgorithm,
165    },
166
167    /// Mark `{data_dir}/join_secret` as wiped on every node.
168    ///
169    /// Operator-driven cleanup after migrating to `eddsa`. The actual
170    /// file-system delete happens locally on each node when this op
171    /// applies; the state machine records the wipe timestamp so
172    /// re-applies are no-ops. Idempotent.
173    WipeJoinSecret,
174}