1use serde::{Deserialize, Serialize};
2
3use crate::instance::InstanceState;
4use crate::node::{NodeInfo, NodeStats};
5use crate::pool::{DesiredCounts, InstanceResources, Role, RuntimePolicy, SecretScope};
6use crate::routing::RoutingTable;
7use crate::signing::SignedPayload;
8use crate::tenant::TenantQuota;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct DesiredState {
17 pub schema_version: u32,
18 pub node_id: String,
19 pub tenants: Vec<DesiredTenant>,
20 #[serde(default)]
21 pub prune_unknown_tenants: bool,
22 #[serde(default)]
23 pub prune_unknown_pools: bool,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct DesiredTenant {
29 pub tenant_id: String,
30 pub network: DesiredTenantNetwork,
31 pub quotas: TenantQuota,
32 #[serde(default)]
33 pub secrets_hash: Option<String>,
34 pub pools: Vec<DesiredPool>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(deny_unknown_fields)]
39pub struct DesiredTenantNetwork {
40 pub tenant_net_id: u16,
41 pub ipv4_subnet: String,
42}
43
44pub const MAX_DESIRED_PER_STATE: u32 = 100;
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct DesiredPool {
50 pub pool_id: String,
51 pub flake_ref: String,
52 pub profile: String,
53 #[serde(default)]
54 pub role: Role,
55 pub instance_resources: InstanceResources,
56 pub desired_counts: DesiredCounts,
57 #[serde(default)]
58 pub runtime_policy: RuntimePolicy,
59 #[serde(default = "default_seccomp")]
60 pub seccomp_policy: String,
61 #[serde(default = "default_compression")]
62 pub snapshot_compression: String,
63 #[serde(default)]
64 pub routing_table: Option<RoutingTable>,
65 #[serde(default)]
66 pub secret_scopes: Vec<SecretScope>,
67}
68
69fn default_seccomp() -> String {
70 "baseline".to_string()
71}
72
73fn default_compression() -> String {
74 "none".to_string()
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82pub struct ReconcileReport {
83 pub tenants_created: Vec<String>,
84 pub tenants_pruned: Vec<String>,
85 pub pools_created: Vec<String>,
86 pub instances_created: u32,
87 pub instances_started: u32,
88 pub instances_warmed: u32,
89 pub instances_slept: u32,
90 pub instances_stopped: u32,
91 #[serde(default)]
92 pub instances_deferred: u32,
93 pub errors: Vec<String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
102pub enum AgentRequest {
103 Reconcile(DesiredState),
105 ReconcileSigned(SignedPayload),
107 NodeInfo,
109 NodeStats,
111 TenantList,
113 InstanceList {
115 tenant_id: String,
116 pool_id: Option<String>,
117 },
118 WakeInstance {
120 tenant_id: String,
121 pool_id: String,
122 instance_id: String,
123 },
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub enum AgentResponse {
129 ReconcileResult(ReconcileReport),
131 NodeInfo(NodeInfo),
133 NodeStats(NodeStats),
135 TenantList(Vec<String>),
137 InstanceList(Vec<InstanceState>),
139 WakeResult { success: bool },
141 Error { code: u16, message: String },
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_agent_request_serde() {
151 let req = AgentRequest::NodeInfo;
152 let json = serde_json::to_string(&req).unwrap();
153 let parsed: AgentRequest = serde_json::from_str(&json).unwrap();
154 assert!(matches!(parsed, AgentRequest::NodeInfo));
155 }
156
157 #[test]
158 fn test_agent_response_error() {
159 let resp = AgentResponse::Error {
160 code: 404,
161 message: "not found".to_string(),
162 };
163 let json = serde_json::to_string(&resp).unwrap();
164 let parsed: AgentResponse = serde_json::from_str(&json).unwrap();
165 match parsed {
166 AgentResponse::Error { code, message } => {
167 assert_eq!(code, 404);
168 assert_eq!(message, "not found");
169 }
170 _ => panic!("Expected Error variant"),
171 }
172 }
173
174 #[test]
175 fn test_desired_state_serde() {
176 let ds = DesiredState {
177 schema_version: 1,
178 node_id: "node-1".to_string(),
179 tenants: vec![],
180 prune_unknown_tenants: false,
181 prune_unknown_pools: false,
182 };
183 let json = serde_json::to_string(&ds).unwrap();
184 let parsed: DesiredState = serde_json::from_str(&json).unwrap();
185 assert_eq!(parsed.schema_version, 1);
186 assert_eq!(parsed.node_id, "node-1");
187 }
188
189 #[test]
190 fn test_reconcile_report_default() {
191 let report = ReconcileReport::default();
192 assert!(report.tenants_created.is_empty());
193 assert!(report.errors.is_empty());
194 assert_eq!(report.instances_created, 0);
195 }
196}