Skip to main content

tatara_core/domain/
job.rs

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    /// SHA-256 hash of the serialized JobSpec, used for drift detection.
57    #[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    /// Secrets to inject into tasks at allocation time.
73    #[serde(default)]
74    pub secrets: Vec<SecretRef>,
75    /// Volumes to create for this task group.
76    #[serde(default)]
77    pub volumes: Vec<VolumeSpec>,
78    /// If set, register this group in the service catalog under this name.
79    #[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    /// Volume mount claims for this task.
107    #[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    /// `nix build` — produces a store path in the Nix store.
135    /// Used for building derivations (packages, Docker images) rather than running them.
136    /// Optionally pushes the result to an Attic binary cache.
137    NixBuild {
138        /// Flake reference (e.g., "github:pleme-io/blackmatter-akeyless#akeyless-backend-auth")
139        flake_ref: String,
140        /// Target system (e.g., "x86_64-linux"). If set, passed as --system.
141        #[serde(default)]
142        system: Option<String>,
143        /// Additional nix build flags (e.g., ["--impure"])
144        #[serde(default)]
145        extra_args: Vec<String>,
146        /// Attic cache name to push the result to (e.g., "main")
147        #[serde(default)]
148        attic_cache: Option<String>,
149    },
150    Kasou {
151        /// Path to kernel image for direct boot.
152        kernel: String,
153        /// Path to initrd.
154        initrd: String,
155        /// Kernel command line.
156        #[serde(default)]
157        cmdline: String,
158        /// Disk image paths (first is root, rest are data/seed).
159        #[serde(default)]
160        disks: Vec<String>,
161        /// MAC address (colon-separated, e.g., "5a:94:ef:ab:cd:12").
162        mac_address: Option<String>,
163        /// Number of vCPUs.
164        #[serde(default = "default_kasou_cpus")]
165        cpus: u32,
166        /// Memory in MiB.
167        #[serde(default = "default_kasou_memory")]
168        memory_mib: u64,
169    },
170    /// WASI component — sandboxed, portable workload via wasmtime.
171    Wasi {
172        /// Path to .wasm component (can be Nix store path).
173        wasm_path: String,
174        /// WASI capabilities to grant.
175        #[serde(default)]
176        capabilities: WasiCapabilities,
177        /// Filesystem mounts (host_path → guest_path).
178        #[serde(default)]
179        mounts: std::collections::HashMap<String, String>,
180        /// Allowed network services.
181        #[serde(default)]
182        allowed_services: Vec<String>,
183    },
184}
185
186/// WASI capability grants for sandboxed workloads.
187#[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/// A submitted job specification (before scheduling).
266#[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    /// Compute a SHA-256 hash of the canonical JSON representation of this spec.
295    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}