use std::collections::BTreeMap;
use std::path::PathBuf;
use isoloom_manifest::{Manifest, OutputRole, Protocol, Role};
use serde::{Deserialize, Serialize};
use crate::state::RunState;
use crate::store::RunRecord;
#[derive(Debug, Clone, Serialize)]
pub struct ShellCommand {
pub program: String,
pub args: Vec<String>,
pub cwd: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum MachineState {
Running,
Unhealthy,
Exited(i32),
Stopped,
Absent,
Unknown(String),
}
impl std::fmt::Display for MachineState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MachineState::Running => f.write_str("running"),
MachineState::Unhealthy => f.write_str("unhealthy"),
MachineState::Exited(code) => write!(f, "exited({code})"),
MachineState::Stopped => f.write_str("stopped"),
MachineState::Absent => f.write_str("absent"),
MachineState::Unknown(s) => f.write_str(s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PublishedPort {
pub port: u16,
pub protocol: Protocol,
pub host_port: u16,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MachineStatus {
pub name: String,
pub role: Role,
pub state: MachineState,
pub addresses: BTreeMap<String, String>,
pub published: Vec<PublishedPort>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NetworkView {
pub name: String,
pub cidr: String,
pub internet: bool,
pub native: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OutputValue {
pub name: String,
pub role: OutputRole,
pub sensitive: bool,
pub value: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Status {
pub state: RunState,
pub provider: Option<String>,
pub machines: Vec<MachineStatus>,
pub networks: Vec<NetworkView>,
pub outputs: Vec<OutputValue>,
pub message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Started {
pub record: RunRecord,
pub outputs: Vec<OutputValue>,
pub message: Option<String>,
}
pub(crate) fn derive_state(machines: &[MachineStatus], record: Option<&RunRecord>) -> RunState {
let present = machines.iter().any(|m| m.state != MachineState::Absent);
if !present {
return RunState::Absent;
}
let targets: Vec<&MachineStatus> = machines.iter().filter(|m| m.role == Role::Target).collect();
let running = |m: &&MachineStatus| m.state == MachineState::Running;
if record.is_some_and(|r| r.state == RunState::Parked) && !targets.iter().any(running) {
return RunState::Parked;
}
if record.is_some_and(|r| r.state == RunState::Failed) {
return RunState::Failed;
}
if !targets.is_empty() && targets.iter().all(running) {
RunState::Ready
} else {
RunState::Failed
}
}
pub(crate) fn any_target_running(machines: &[MachineStatus]) -> bool {
machines
.iter()
.any(|m| m.role == Role::Target && matches!(m.state, MachineState::Running | MachineState::Unhealthy))
}
pub(crate) fn networks(manifest: &Manifest, native: impl Fn(&isoloom_manifest::Template) -> Option<String>) -> Vec<NetworkView> {
manifest
.networks
.iter()
.map(|n| NetworkView {
name: n.name.to_string(),
cidr: n.cidr.to_string(),
internet: n.internet,
native: n.native.as_ref().and_then(&native),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn machine(role: Role, state: MachineState) -> MachineStatus {
MachineStatus {
name: "m".into(),
role,
state,
addresses: BTreeMap::new(),
published: Vec::new(),
}
}
#[test]
fn a_run_is_ready_when_every_target_runs() {
use MachineState::{Absent, Exited, Running, Stopped};
let all = |ms: Vec<MachineStatus>| derive_state(&ms, None);
assert_eq!(all(vec![machine(Role::Target, Absent), machine(Role::Job, Absent)]), RunState::Absent);
assert_eq!(
all(vec![
machine(Role::Target, Running),
machine(Role::Job, Exited(0)),
machine(Role::Infra, Running)
]),
RunState::Ready
);
assert_eq!(all(vec![machine(Role::Target, Running), machine(Role::Target, Exited(1))]), RunState::Failed);
assert_eq!(all(vec![machine(Role::Target, Stopped)]), RunState::Failed);
assert_eq!(all(vec![machine(Role::Target, MachineState::Unhealthy)]), RunState::Failed);
}
#[test]
fn a_parked_run_with_nothing_running_is_parked() {
let mut record = crate::store::RunRecord::fresh(
crate::name::RunName::new("r").unwrap(),
"/x".into(),
String::new(),
isoloom_manifest::Target::Docker,
isoloom_manifest::Driver::Compose,
);
record.state = RunState::Parked;
let stopped = vec![machine(Role::Target, MachineState::Exited(0))];
assert_eq!(derive_state(&stopped, Some(&record)), RunState::Parked);
assert!(!any_target_running(&stopped));
}
}