use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Driver {
Exec,
Docker,
Podman,
RawExec,
Java,
Qemu,
}
impl Default for Driver {
fn default() -> Self {
Self::Exec
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Resources {
pub cpu: u32,
pub memory: u32,
pub disk: Option<u32>,
pub network: Option<u32>,
pub gpu: Option<u32>,
}
impl Resources {
pub fn new(cpu: u32, memory: u32) -> Self {
Self {
cpu,
memory,
disk: None,
network: None,
gpu: None,
}
}
pub fn with_disk(mut self, disk: u32) -> Self {
self.disk = Some(disk);
self
}
pub fn with_network(mut self, network: u32) -> Self {
self.network = Some(network);
self
}
pub fn with_gpu(mut self, gpu: u32) -> Self {
self.gpu = Some(gpu);
self
}
}
impl Default for Resources {
fn default() -> Self {
Self::new(100, 128)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScalingConfig {
pub min: u32,
pub max: u32,
pub desired: u32,
}
impl ScalingConfig {
pub fn new(min: u32, max: u32) -> Self {
Self {
min,
max,
desired: min,
}
}
pub fn with_desired(mut self, desired: u32) -> Self {
self.desired = desired.clamp(self.min, self.max);
self
}
}
impl Default for ScalingConfig {
fn default() -> Self {
Self::new(1, 1)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub name: String,
pub driver: Driver,
pub command: Option<String>,
pub args: Vec<String>,
pub env: HashMap<String, String>,
pub resources: Resources,
pub artifacts: Vec<String>,
pub health_check: Option<HealthCheck>,
pub metadata: HashMap<String, String>,
}
impl Task {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
driver: Driver::default(),
command: None,
args: Vec::new(),
env: HashMap::new(),
resources: Resources::default(),
artifacts: Vec::new(),
health_check: None,
metadata: HashMap::new(),
}
}
pub fn driver(mut self, driver: Driver) -> Self {
self.driver = driver;
self
}
pub fn command(mut self, cmd: impl Into<String>) -> Self {
self.command = Some(cmd.into());
self
}
pub fn args(mut self, args: Vec<impl Into<String>>) -> Self {
self.args = args.into_iter().map(|a| a.into()).collect();
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
pub fn resources(mut self, cpu: u32, memory: u32) -> Self {
self.resources = Resources::new(cpu, memory);
self
}
pub fn with_resources(mut self, resources: Resources) -> Self {
self.resources = resources;
self
}
pub fn artifact(mut self, url: impl Into<String>) -> Self {
self.artifacts.push(url.into());
self
}
pub fn health_check(mut self, check: HealthCheck) -> Self {
self.health_check = Some(check);
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheck {
pub check_type: HealthCheckType,
pub interval_secs: u32,
pub timeout_secs: u32,
pub path: Option<String>,
pub port: Option<u16>,
}
impl HealthCheck {
pub fn http(path: impl Into<String>, port: u16) -> Self {
Self {
check_type: HealthCheckType::Http,
interval_secs: 10,
timeout_secs: 2,
path: Some(path.into()),
port: Some(port),
}
}
pub fn tcp(port: u16) -> Self {
Self {
check_type: HealthCheckType::Tcp,
interval_secs: 10,
timeout_secs: 2,
path: None,
port: Some(port),
}
}
pub fn script() -> Self {
Self {
check_type: HealthCheckType::Script,
interval_secs: 30,
timeout_secs: 5,
path: None,
port: None,
}
}
pub fn interval(mut self, secs: u32) -> Self {
self.interval_secs = secs;
self
}
pub fn timeout(mut self, secs: u32) -> Self {
self.timeout_secs = secs;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthCheckType {
Http,
Tcp,
Script,
Grpc,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskGroup {
pub name: String,
pub tasks: Vec<Task>,
pub scaling: ScalingConfig,
pub restart_policy: RestartPolicy,
pub network: Option<NetworkConfig>,
pub metadata: HashMap<String, String>,
}
impl TaskGroup {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
tasks: Vec::new(),
scaling: ScalingConfig::default(),
restart_policy: RestartPolicy::default(),
network: None,
metadata: HashMap::new(),
}
}
pub fn task(mut self, task: Task) -> Self {
self.tasks.push(task);
self
}
pub fn scaling(mut self, min: u32, max: u32) -> Self {
self.scaling = ScalingConfig::new(min, max);
self
}
pub fn restart_policy(mut self, policy: RestartPolicy) -> Self {
self.restart_policy = policy;
self
}
pub fn network(mut self, config: NetworkConfig) -> Self {
self.network = Some(config);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestartPolicy {
pub attempts: u32,
pub delay_secs: u32,
pub mode: RestartMode,
}
impl Default for RestartPolicy {
fn default() -> Self {
Self {
attempts: 3,
delay_secs: 15,
mode: RestartMode::Fail,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RestartMode {
Fail,
Delay,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
pub mode: NetworkMode,
pub ports: Vec<PortMapping>,
}
impl NetworkConfig {
pub fn bridge() -> Self {
Self {
mode: NetworkMode::Bridge,
ports: Vec::new(),
}
}
pub fn host() -> Self {
Self {
mode: NetworkMode::Host,
ports: Vec::new(),
}
}
pub fn port(mut self, label: impl Into<String>, to: u16) -> Self {
self.ports.push(PortMapping {
label: label.into(),
to,
static_port: None,
});
self
}
pub fn static_port(mut self, label: impl Into<String>, port: u16) -> Self {
self.ports.push(PortMapping {
label: label.into(),
to: port,
static_port: Some(port),
});
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NetworkMode {
Bridge,
Host,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortMapping {
pub label: String,
pub to: u16,
pub static_port: Option<u16>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobState {
Pending,
Running,
Complete,
Failed,
Stopped,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job {
pub id: String,
pub name: String,
pub job_type: JobType,
pub groups: Vec<TaskGroup>,
pub priority: u8,
pub datacenters: Vec<String>,
pub state: JobState,
pub metadata: HashMap<String, String>,
pub created_at: chrono::DateTime<chrono::Utc>,
}
impl Job {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
Self {
id: format!("{}-{}", &name, &Uuid::new_v4().to_string()[..8]),
name,
job_type: JobType::Service,
groups: Vec::new(),
priority: 50,
datacenters: vec!["dc1".to_string()],
state: JobState::Pending,
metadata: HashMap::new(),
created_at: chrono::Utc::now(),
}
}
pub fn job_type(mut self, job_type: JobType) -> Self {
self.job_type = job_type;
self
}
pub fn with_group(mut self, group_name: impl Into<String>, task: Task) -> Self {
let group = TaskGroup::new(group_name).task(task);
self.groups.push(group);
self
}
pub fn group(mut self, group: TaskGroup) -> Self {
self.groups.push(group);
self
}
pub fn priority(mut self, priority: u8) -> Self {
self.priority = priority.min(100);
self
}
pub fn datacenters(mut self, dcs: Vec<impl Into<String>>) -> Self {
self.datacenters = dcs.into_iter().map(|d| d.into()).collect();
self
}
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn task_count(&self) -> usize {
self.groups.iter().map(|g| g.tasks.len()).sum()
}
pub fn desired_count(&self) -> u32 {
self.groups.iter().map(|g| g.scaling.desired).sum()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobType {
Service,
Batch,
System,
Parameterized,
}
impl Default for JobType {
fn default() -> Self {
Self::Service
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_job_builder() {
let job = Job::new("my-service")
.job_type(JobType::Service)
.priority(75)
.with_group(
"api",
Task::new("server")
.driver(Driver::Exec)
.command("/usr/bin/server")
.args(vec!["--port", "8080"])
.resources(500, 256),
);
assert_eq!(job.name, "my-service");
assert_eq!(job.priority, 75);
assert_eq!(job.groups.len(), 1);
assert_eq!(job.groups[0].tasks[0].name, "server");
}
#[test]
fn test_task_group_scaling() {
let group = TaskGroup::new("workers")
.task(Task::new("worker"))
.scaling(2, 10);
assert_eq!(group.scaling.min, 2);
assert_eq!(group.scaling.max, 10);
}
#[test]
fn test_resources_builder() {
let res = Resources::new(1000, 512).with_gpu(2).with_disk(10000);
assert_eq!(res.cpu, 1000);
assert_eq!(res.memory, 512);
assert_eq!(res.gpu, Some(2));
assert_eq!(res.disk, Some(10000));
}
}