Skip to main content

isothrum_runner/
status.rs

1//! What a run looks like from outside: its state, machines, networks and outputs. Roles,
2//! addresses and networks come from the manifest; the engine only says what runs and which host
3//! ports it got.
4
5use std::collections::BTreeMap;
6
7use isoloom_manifest::{Manifest, OutputRole, Protocol, Role};
8use serde::{Deserialize, Serialize};
9
10use crate::state::RunState;
11use crate::store::RunRecord;
12
13/// A machine's state, as its engine reports it.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16#[non_exhaustive]
17pub enum MachineState {
18    /// Running.
19    Running,
20    /// Running, but failing its health check.
21    Unhealthy,
22    /// Exited with this code (a job that finished, or a machine that died).
23    Exited(i32),
24    /// Created or stopped, not running.
25    Stopped,
26    /// Not there.
27    Absent,
28    /// Something else, as the engine says it.
29    Unknown(String),
30}
31
32impl std::fmt::Display for MachineState {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            MachineState::Running => f.write_str("running"),
36            MachineState::Unhealthy => f.write_str("unhealthy"),
37            MachineState::Exited(code) => write!(f, "exited({code})"),
38            MachineState::Stopped => f.write_str("stopped"),
39            MachineState::Absent => f.write_str("absent"),
40            MachineState::Unknown(s) => f.write_str(s),
41        }
42    }
43}
44
45/// A published port: where a machine's port answers on the host.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[non_exhaustive]
48pub struct PublishedPort {
49    /// The machine's port.
50    pub port: u16,
51    /// `tcp` or `udp`.
52    pub protocol: Protocol,
53    /// The host port.
54    pub host_port: u16,
55}
56
57/// One machine of the manifest, as it stands.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[non_exhaustive]
60pub struct MachineStatus {
61    /// Its name.
62    pub name: String,
63    /// What it is for.
64    pub role: Role,
65    /// Its state.
66    pub state: MachineState,
67    /// Its static addresses, by network.
68    pub addresses: BTreeMap<String, String>,
69    /// Its published ports.
70    pub published: Vec<PublishedPort>,
71}
72
73/// A network of the lab.
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[non_exhaustive]
76pub struct NetworkView {
77    /// Its name in the spec.
78    pub name: String,
79    /// Its block.
80    pub cidr: String,
81    /// Whether it reaches the internet.
82    pub internet: bool,
83    /// Its name where the target created it (`flag-portal_lab`), when known.
84    pub native: Option<String>,
85}
86
87/// An output's value.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[non_exhaustive]
90pub struct OutputValue {
91    /// Its name.
92    pub name: String,
93    /// What it is for (`entry` is the Open button).
94    pub role: OutputRole,
95    /// Whether the value is sensitive.
96    pub sensitive: bool,
97    /// Its value, when it can be read.
98    pub value: Option<String>,
99}
100
101/// A run as it stands.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[non_exhaustive]
104pub struct Status {
105    /// Where it stands.
106    pub state: RunState,
107    /// The Vagrant provider it uses, when it has one.
108    pub provider: Option<String>,
109    /// Every machine of the manifest.
110    pub machines: Vec<MachineStatus>,
111    /// The lab's networks.
112    pub networks: Vec<NetworkView>,
113    /// Its outputs, read live.
114    pub outputs: Vec<OutputValue>,
115    /// The lab's message, filled, once the run is ready.
116    pub message: Option<String>,
117}
118
119/// A run that came up (or resumed).
120#[derive(Debug, Clone, PartialEq, Eq)]
121#[non_exhaustive]
122pub struct Run {
123    /// Its record.
124    pub record: RunRecord,
125    /// Its outputs.
126    pub outputs: Vec<OutputValue>,
127    /// The lab's message, filled (a sensitive output never substituted).
128    pub message: Option<String>,
129}
130
131/// The state of a run whose machines are `machines`, given its record. Ready when every target
132/// machine runs; parked when its record says so and none runs; absent when nothing is there.
133pub(crate) fn derive_state(machines: &[MachineStatus], record: Option<&RunRecord>) -> RunState {
134    let present = machines.iter().any(|m| m.state != MachineState::Absent);
135    if !present {
136        return RunState::Absent;
137    }
138    let targets: Vec<&MachineStatus> = machines.iter().filter(|m| m.role == Role::Target).collect();
139    let running = |m: &&MachineStatus| m.state == MachineState::Running;
140    if record.is_some_and(|r| r.state == RunState::Parked) && !targets.iter().any(running) {
141        return RunState::Parked;
142    }
143    // A start that failed after its machines came up (a Terraform apply whose router never
144    // reported its address, a set-up step that failed) left them running: the run isn't ready.
145    if record.is_some_and(|r| r.state == RunState::Failed) {
146        return RunState::Failed;
147    }
148    if !targets.is_empty() && targets.iter().all(running) {
149        RunState::Ready
150    } else {
151        RunState::Failed
152    }
153}
154
155/// Whether any target machine of `machines` runs.
156pub(crate) fn any_target_running(machines: &[MachineStatus]) -> bool {
157    machines
158        .iter()
159        .any(|m| m.role == Role::Target && matches!(m.state, MachineState::Running | MachineState::Unhealthy))
160}
161
162/// The manifest's networks, their native names filled by `native`.
163pub(crate) fn networks(manifest: &Manifest, native: impl Fn(&isoloom_manifest::Template) -> Option<String>) -> Vec<NetworkView> {
164    manifest
165        .networks
166        .iter()
167        .map(|n| NetworkView {
168            name: n.name.to_string(),
169            cidr: n.cidr.to_string(),
170            internet: n.internet,
171            native: n.native.as_ref().and_then(&native),
172        })
173        .collect()
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    fn machine(role: Role, state: MachineState) -> MachineStatus {
181        MachineStatus {
182            name: "m".into(),
183            role,
184            state,
185            addresses: BTreeMap::new(),
186            published: Vec::new(),
187        }
188    }
189
190    #[test]
191    fn a_run_is_ready_when_every_target_runs() {
192        use MachineState::{Absent, Exited, Running, Stopped};
193        let all = |ms: Vec<MachineStatus>| derive_state(&ms, None);
194        assert_eq!(all(vec![machine(Role::Target, Absent), machine(Role::Job, Absent)]), RunState::Absent);
195        assert_eq!(
196            all(vec![
197                machine(Role::Target, Running),
198                machine(Role::Job, Exited(0)),
199                machine(Role::Infra, Running)
200            ]),
201            RunState::Ready
202        );
203        assert_eq!(all(vec![machine(Role::Target, Running), machine(Role::Target, Exited(1))]), RunState::Failed);
204        assert_eq!(all(vec![machine(Role::Target, Stopped)]), RunState::Failed);
205        assert_eq!(all(vec![machine(Role::Target, MachineState::Unhealthy)]), RunState::Failed);
206    }
207
208    #[test]
209    fn a_parked_run_with_nothing_running_is_parked() {
210        let mut record = crate::store::RunRecord::fresh(
211            crate::name::RunName::new("r").unwrap(),
212            "/x".into(),
213            String::new(),
214            isoloom_manifest::Target::Docker,
215            isoloom_manifest::Driver::Compose,
216        );
217        record.state = RunState::Parked;
218        let stopped = vec![machine(Role::Target, MachineState::Exited(0))];
219        assert_eq!(derive_state(&stopped, Some(&record)), RunState::Parked);
220        assert!(!any_target_running(&stopped));
221    }
222}