1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use super::secret::SecretRef;
6use super::volume::{VolumeClaim, VolumeSpec};
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(rename_all = "snake_case")]
10pub enum JobType {
11 Service,
12 Batch,
13 System,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17#[serde(rename_all = "snake_case")]
18pub enum JobStatus {
19 Pending,
20 Running,
21 Dead,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25#[serde(rename_all = "snake_case")]
26pub enum DriverType {
27 Exec,
28 Oci,
29 Nix,
30 NixBuild,
31 Kasou,
32 Kube,
33 Wasi,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37#[serde(rename_all = "snake_case")]
38pub enum RestartMode {
39 OnFailure,
40 Always,
41 Never,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Job {
46 pub id: String,
47 pub version: u64,
48 pub job_type: JobType,
49 pub status: JobStatus,
50 pub submitted_at: DateTime<Utc>,
51 pub groups: Vec<TaskGroup>,
52 #[serde(default)]
53 pub constraints: Vec<Constraint>,
54 #[serde(default)]
55 pub meta: HashMap<String, String>,
56 #[serde(default)]
58 pub spec_hash: Option<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct TaskGroup {
63 pub name: String,
64 #[serde(default = "default_count")]
65 pub count: u32,
66 pub tasks: Vec<Task>,
67 #[serde(default)]
68 pub restart_policy: RestartPolicy,
69 #[serde(default)]
70 pub resources: Resources,
71 pub network: Option<NetworkConfig>,
72 #[serde(default)]
74 pub secrets: Vec<SecretRef>,
75 #[serde(default)]
77 pub volumes: Vec<VolumeSpec>,
78 #[serde(default)]
80 pub service_name: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct RestartPolicy {
85 #[serde(default = "default_restart_mode")]
86 pub mode: RestartMode,
87 #[serde(default = "default_restart_attempts")]
88 pub attempts: u32,
89 #[serde(default = "default_restart_interval")]
90 pub interval_secs: u64,
91 #[serde(default = "default_restart_delay")]
92 pub delay_secs: u64,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct Task {
97 pub name: String,
98 pub driver: DriverType,
99 pub config: TaskConfig,
100 #[serde(default)]
101 pub env: HashMap<String, String>,
102 #[serde(default)]
103 pub resources: Resources,
104 #[serde(default)]
105 pub health_checks: Vec<HealthCheck>,
106 #[serde(default)]
108 pub volume_claims: Vec<VolumeClaim>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(tag = "type", rename_all = "snake_case")]
113pub enum TaskConfig {
114 Exec {
115 command: String,
116 #[serde(default)]
117 args: Vec<String>,
118 working_dir: Option<String>,
119 },
120 Oci {
121 image: String,
122 #[serde(default)]
123 ports: HashMap<String, String>,
124 #[serde(default)]
125 volumes: HashMap<String, String>,
126 entrypoint: Option<Vec<String>>,
127 command: Option<Vec<String>>,
128 },
129 Nix {
130 flake_ref: String,
131 #[serde(default)]
132 args: Vec<String>,
133 },
134 NixBuild {
138 flake_ref: String,
140 #[serde(default)]
142 system: Option<String>,
143 #[serde(default)]
145 extra_args: Vec<String>,
146 #[serde(default)]
148 attic_cache: Option<String>,
149 },
150 Kasou {
151 kernel: String,
153 initrd: String,
155 #[serde(default)]
157 cmdline: String,
158 #[serde(default)]
160 disks: Vec<String>,
161 mac_address: Option<String>,
163 #[serde(default = "default_kasou_cpus")]
165 cpus: u32,
166 #[serde(default = "default_kasou_memory")]
168 memory_mib: u64,
169 },
170 Wasi {
172 wasm_path: String,
174 #[serde(default)]
176 capabilities: WasiCapabilities,
177 #[serde(default)]
179 mounts: std::collections::HashMap<String, String>,
180 #[serde(default)]
182 allowed_services: Vec<String>,
183 },
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, Default)]
188pub struct WasiCapabilities {
189 #[serde(default)]
190 pub filesystem: bool,
191 #[serde(default)]
192 pub network: bool,
193 #[serde(default)]
194 pub clocks: bool,
195 #[serde(default)]
196 pub random: bool,
197 #[serde(default)]
198 pub stdout: bool,
199 #[serde(default)]
200 pub stderr: bool,
201}
202
203fn default_kasou_cpus() -> u32 {
204 2
205}
206fn default_kasou_memory() -> u64 {
207 2048
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, Default)]
211pub struct Resources {
212 #[serde(default)]
213 pub cpu_mhz: u64,
214 #[serde(default)]
215 pub memory_mb: u64,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Constraint {
220 pub attribute: String,
221 #[serde(default = "default_operator")]
222 pub operator: String,
223 pub value: String,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct NetworkConfig {
228 #[serde(default)]
229 pub ports: Vec<PortMapping>,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct PortMapping {
234 pub label: String,
235 pub value: u16,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
239#[serde(tag = "type", rename_all = "snake_case")]
240pub enum HealthCheck {
241 Http {
242 port: u16,
243 path: String,
244 #[serde(default = "default_health_interval")]
245 interval_secs: u64,
246 #[serde(default = "default_health_timeout")]
247 timeout_secs: u64,
248 },
249 Exec {
250 command: String,
251 #[serde(default = "default_health_interval")]
252 interval_secs: u64,
253 #[serde(default = "default_health_timeout")]
254 timeout_secs: u64,
255 },
256 Tcp {
257 port: u16,
258 #[serde(default = "default_health_interval")]
259 interval_secs: u64,
260 #[serde(default = "default_health_timeout")]
261 timeout_secs: u64,
262 },
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct JobSpec {
268 pub id: String,
269 #[serde(default = "default_job_type")]
270 pub job_type: JobType,
271 pub groups: Vec<TaskGroup>,
272 #[serde(default)]
273 pub constraints: Vec<Constraint>,
274 #[serde(default)]
275 pub meta: HashMap<String, String>,
276}
277
278impl JobSpec {
279 pub fn into_job(self) -> Job {
280 let spec_hash = Some(self.content_hash());
281 Job {
282 id: self.id,
283 version: 1,
284 job_type: self.job_type,
285 status: JobStatus::Pending,
286 submitted_at: Utc::now(),
287 groups: self.groups,
288 constraints: self.constraints,
289 meta: self.meta,
290 spec_hash,
291 }
292 }
293
294 pub fn content_hash(&self) -> String {
296 use sha2::{Digest, Sha256};
297 let canonical = serde_json::to_string(self).unwrap_or_default();
298 let hash = Sha256::digest(canonical.as_bytes());
299 format!("{:x}", hash)
300 }
301}
302
303fn default_count() -> u32 {
304 1
305}
306
307fn default_restart_mode() -> RestartMode {
308 RestartMode::OnFailure
309}
310
311fn default_restart_attempts() -> u32 {
312 3
313}
314
315fn default_restart_interval() -> u64 {
316 300
317}
318
319fn default_restart_delay() -> u64 {
320 5
321}
322
323fn default_operator() -> String {
324 "=".to_string()
325}
326
327fn default_health_interval() -> u64 {
328 10
329}
330
331fn default_health_timeout() -> u64 {
332 5
333}
334
335fn default_job_type() -> JobType {
336 JobType::Service
337}
338
339impl Default for RestartPolicy {
340 fn default() -> Self {
341 Self {
342 mode: default_restart_mode(),
343 attempts: default_restart_attempts(),
344 interval_secs: default_restart_interval(),
345 delay_secs: default_restart_delay(),
346 }
347 }
348}