1use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum Driver {
17 Exec,
19 Docker,
21 Podman,
23 RawExec,
25 Java,
27 Qemu,
29}
30
31impl Default for Driver {
32 fn default() -> Self {
33 Self::Exec
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Resources {
40 pub cpu: u32,
42 pub memory: u32,
44 pub disk: Option<u32>,
46 pub network: Option<u32>,
48 pub gpu: Option<u32>,
50}
51
52impl Resources {
53 pub fn new(cpu: u32, memory: u32) -> Self {
55 Self {
56 cpu,
57 memory,
58 disk: None,
59 network: None,
60 gpu: None,
61 }
62 }
63
64 pub fn with_disk(mut self, disk: u32) -> Self {
66 self.disk = Some(disk);
67 self
68 }
69
70 pub fn with_network(mut self, network: u32) -> Self {
72 self.network = Some(network);
73 self
74 }
75
76 pub fn with_gpu(mut self, gpu: u32) -> Self {
78 self.gpu = Some(gpu);
79 self
80 }
81}
82
83impl Default for Resources {
84 fn default() -> Self {
85 Self::new(100, 128)
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct ScalingConfig {
92 pub min: u32,
94 pub max: u32,
96 pub desired: u32,
98}
99
100impl ScalingConfig {
101 pub fn new(min: u32, max: u32) -> Self {
103 Self {
104 min,
105 max,
106 desired: min,
107 }
108 }
109
110 pub fn with_desired(mut self, desired: u32) -> Self {
112 self.desired = desired.clamp(self.min, self.max);
113 self
114 }
115}
116
117impl Default for ScalingConfig {
118 fn default() -> Self {
119 Self::new(1, 1)
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct Task {
126 pub name: String,
128 pub driver: Driver,
130 pub command: Option<String>,
132 pub args: Vec<String>,
134 pub env: HashMap<String, String>,
136 pub resources: Resources,
138 pub artifacts: Vec<String>,
140 pub health_check: Option<HealthCheck>,
142 pub metadata: HashMap<String, String>,
144}
145
146impl Task {
147 pub fn new(name: impl Into<String>) -> Self {
149 Self {
150 name: name.into(),
151 driver: Driver::default(),
152 command: None,
153 args: Vec::new(),
154 env: HashMap::new(),
155 resources: Resources::default(),
156 artifacts: Vec::new(),
157 health_check: None,
158 metadata: HashMap::new(),
159 }
160 }
161
162 pub fn driver(mut self, driver: Driver) -> Self {
164 self.driver = driver;
165 self
166 }
167
168 pub fn command(mut self, cmd: impl Into<String>) -> Self {
170 self.command = Some(cmd.into());
171 self
172 }
173
174 pub fn args(mut self, args: Vec<impl Into<String>>) -> Self {
176 self.args = args.into_iter().map(|a| a.into()).collect();
177 self
178 }
179
180 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
182 self.env.insert(key.into(), value.into());
183 self
184 }
185
186 pub fn resources(mut self, cpu: u32, memory: u32) -> Self {
188 self.resources = Resources::new(cpu, memory);
189 self
190 }
191
192 pub fn with_resources(mut self, resources: Resources) -> Self {
194 self.resources = resources;
195 self
196 }
197
198 pub fn artifact(mut self, url: impl Into<String>) -> Self {
200 self.artifacts.push(url.into());
201 self
202 }
203
204 pub fn health_check(mut self, check: HealthCheck) -> Self {
206 self.health_check = Some(check);
207 self
208 }
209
210 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
212 self.metadata.insert(key.into(), value.into());
213 self
214 }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct HealthCheck {
220 pub check_type: HealthCheckType,
222 pub interval_secs: u32,
224 pub timeout_secs: u32,
226 pub path: Option<String>,
228 pub port: Option<u16>,
230}
231
232impl HealthCheck {
233 pub fn http(path: impl Into<String>, port: u16) -> Self {
235 Self {
236 check_type: HealthCheckType::Http,
237 interval_secs: 10,
238 timeout_secs: 2,
239 path: Some(path.into()),
240 port: Some(port),
241 }
242 }
243
244 pub fn tcp(port: u16) -> Self {
246 Self {
247 check_type: HealthCheckType::Tcp,
248 interval_secs: 10,
249 timeout_secs: 2,
250 path: None,
251 port: Some(port),
252 }
253 }
254
255 pub fn script() -> Self {
257 Self {
258 check_type: HealthCheckType::Script,
259 interval_secs: 30,
260 timeout_secs: 5,
261 path: None,
262 port: None,
263 }
264 }
265
266 pub fn interval(mut self, secs: u32) -> Self {
268 self.interval_secs = secs;
269 self
270 }
271
272 pub fn timeout(mut self, secs: u32) -> Self {
274 self.timeout_secs = secs;
275 self
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
281pub enum HealthCheckType {
282 Http,
284 Tcp,
286 Script,
288 Grpc,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct TaskGroup {
295 pub name: String,
297 pub tasks: Vec<Task>,
299 pub scaling: ScalingConfig,
301 pub restart_policy: RestartPolicy,
303 pub network: Option<NetworkConfig>,
305 pub metadata: HashMap<String, String>,
307}
308
309impl TaskGroup {
310 pub fn new(name: impl Into<String>) -> Self {
312 Self {
313 name: name.into(),
314 tasks: Vec::new(),
315 scaling: ScalingConfig::default(),
316 restart_policy: RestartPolicy::default(),
317 network: None,
318 metadata: HashMap::new(),
319 }
320 }
321
322 pub fn task(mut self, task: Task) -> Self {
324 self.tasks.push(task);
325 self
326 }
327
328 pub fn scaling(mut self, min: u32, max: u32) -> Self {
330 self.scaling = ScalingConfig::new(min, max);
331 self
332 }
333
334 pub fn restart_policy(mut self, policy: RestartPolicy) -> Self {
336 self.restart_policy = policy;
337 self
338 }
339
340 pub fn network(mut self, config: NetworkConfig) -> Self {
342 self.network = Some(config);
343 self
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct RestartPolicy {
350 pub attempts: u32,
352 pub delay_secs: u32,
354 pub mode: RestartMode,
356}
357
358impl Default for RestartPolicy {
359 fn default() -> Self {
360 Self {
361 attempts: 3,
362 delay_secs: 15,
363 mode: RestartMode::Fail,
364 }
365 }
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
370pub enum RestartMode {
371 Fail,
373 Delay,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct NetworkConfig {
380 pub mode: NetworkMode,
382 pub ports: Vec<PortMapping>,
384}
385
386impl NetworkConfig {
387 pub fn bridge() -> Self {
389 Self {
390 mode: NetworkMode::Bridge,
391 ports: Vec::new(),
392 }
393 }
394
395 pub fn host() -> Self {
397 Self {
398 mode: NetworkMode::Host,
399 ports: Vec::new(),
400 }
401 }
402
403 pub fn port(mut self, label: impl Into<String>, to: u16) -> Self {
405 self.ports.push(PortMapping {
406 label: label.into(),
407 to,
408 static_port: None,
409 });
410 self
411 }
412
413 pub fn static_port(mut self, label: impl Into<String>, port: u16) -> Self {
415 self.ports.push(PortMapping {
416 label: label.into(),
417 to: port,
418 static_port: Some(port),
419 });
420 self
421 }
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
426pub enum NetworkMode {
427 Bridge,
429 Host,
431 None,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct PortMapping {
438 pub label: String,
440 pub to: u16,
442 pub static_port: Option<u16>,
444}
445
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
448pub enum JobState {
449 Pending,
451 Running,
453 Complete,
455 Failed,
457 Stopped,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct Job {
464 pub id: String,
466 pub name: String,
468 pub job_type: JobType,
470 pub groups: Vec<TaskGroup>,
472 pub priority: u8,
474 pub datacenters: Vec<String>,
476 pub state: JobState,
478 pub metadata: HashMap<String, String>,
480 pub created_at: chrono::DateTime<chrono::Utc>,
482}
483
484impl Job {
485 pub fn new(name: impl Into<String>) -> Self {
487 let name = name.into();
488 Self {
489 id: format!("{}-{}", &name, &Uuid::new_v4().to_string()[..8]),
490 name,
491 job_type: JobType::Service,
492 groups: Vec::new(),
493 priority: 50,
494 datacenters: vec!["dc1".to_string()],
495 state: JobState::Pending,
496 metadata: HashMap::new(),
497 created_at: chrono::Utc::now(),
498 }
499 }
500
501 pub fn job_type(mut self, job_type: JobType) -> Self {
503 self.job_type = job_type;
504 self
505 }
506
507 pub fn with_group(mut self, group_name: impl Into<String>, task: Task) -> Self {
509 let group = TaskGroup::new(group_name).task(task);
510 self.groups.push(group);
511 self
512 }
513
514 pub fn group(mut self, group: TaskGroup) -> Self {
516 self.groups.push(group);
517 self
518 }
519
520 pub fn priority(mut self, priority: u8) -> Self {
522 self.priority = priority.min(100);
523 self
524 }
525
526 pub fn datacenters(mut self, dcs: Vec<impl Into<String>>) -> Self {
528 self.datacenters = dcs.into_iter().map(|d| d.into()).collect();
529 self
530 }
531
532 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
534 self.metadata.insert(key.into(), value.into());
535 self
536 }
537
538 pub fn task_count(&self) -> usize {
540 self.groups.iter().map(|g| g.tasks.len()).sum()
541 }
542
543 pub fn desired_count(&self) -> u32 {
545 self.groups.iter().map(|g| g.scaling.desired).sum()
546 }
547}
548
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
551pub enum JobType {
552 Service,
554 Batch,
556 System,
558 Parameterized,
560}
561
562impl Default for JobType {
563 fn default() -> Self {
564 Self::Service
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571
572 #[test]
573 fn test_job_builder() {
574 let job = Job::new("my-service")
575 .job_type(JobType::Service)
576 .priority(75)
577 .with_group(
578 "api",
579 Task::new("server")
580 .driver(Driver::Exec)
581 .command("/usr/bin/server")
582 .args(vec!["--port", "8080"])
583 .resources(500, 256),
584 );
585
586 assert_eq!(job.name, "my-service");
587 assert_eq!(job.priority, 75);
588 assert_eq!(job.groups.len(), 1);
589 assert_eq!(job.groups[0].tasks[0].name, "server");
590 }
591
592 #[test]
593 fn test_task_group_scaling() {
594 let group = TaskGroup::new("workers")
595 .task(Task::new("worker"))
596 .scaling(2, 10);
597
598 assert_eq!(group.scaling.min, 2);
599 assert_eq!(group.scaling.max, 10);
600 }
601
602 #[test]
603 fn test_resources_builder() {
604 let res = Resources::new(1000, 512).with_gpu(2).with_disk(10000);
605
606 assert_eq!(res.cpu, 1000);
607 assert_eq!(res.memory, 512);
608 assert_eq!(res.gpu, Some(2));
609 assert_eq!(res.disk, Some(10000));
610 }
611}