1use 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#[derive(Debug, Clone, Serialize)]
18pub struct ShellCommand {
19 pub program: String,
21 pub args: Vec<String>,
23 pub cwd: Option<PathBuf>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30#[non_exhaustive]
31pub enum MachineState {
32 Running,
34 Unhealthy,
36 Exited(i32),
38 Stopped,
40 Absent,
42 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[non_exhaustive]
62pub struct PublishedPort {
63 pub port: u16,
65 pub protocol: Protocol,
67 pub host_port: u16,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[non_exhaustive]
74pub struct MachineStatus {
75 pub name: String,
77 pub role: Role,
79 pub state: MachineState,
81 pub addresses: BTreeMap<String, String>,
83 pub published: Vec<PublishedPort>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[non_exhaustive]
90pub struct NetworkView {
91 pub name: String,
93 pub cidr: String,
95 pub internet: bool,
97 pub native: Option<String>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[non_exhaustive]
104pub struct OutputValue {
105 pub name: String,
107 pub role: OutputRole,
109 pub sensitive: bool,
111 pub value: Option<String>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[non_exhaustive]
118pub struct Status {
119 pub state: RunState,
121 pub provider: Option<String>,
123 pub machines: Vec<MachineStatus>,
125 pub networks: Vec<NetworkView>,
127 pub outputs: Vec<OutputValue>,
129 pub message: Option<String>,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135#[non_exhaustive]
136pub struct Started {
137 pub record: RunRecord,
139 pub outputs: Vec<OutputValue>,
141 pub message: Option<String>,
143}
144
145pub(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 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
169pub(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
176pub(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}