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