1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16#[non_exhaustive]
17pub enum MachineState {
18 Running,
20 Unhealthy,
22 Exited(i32),
24 Stopped,
26 Absent,
28 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[non_exhaustive]
48pub struct PublishedPort {
49 pub port: u16,
51 pub protocol: Protocol,
53 pub host_port: u16,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[non_exhaustive]
60pub struct MachineStatus {
61 pub name: String,
63 pub role: Role,
65 pub state: MachineState,
67 pub addresses: BTreeMap<String, String>,
69 pub published: Vec<PublishedPort>,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[non_exhaustive]
76pub struct NetworkView {
77 pub name: String,
79 pub cidr: String,
81 pub internet: bool,
83 pub native: Option<String>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[non_exhaustive]
90pub struct OutputValue {
91 pub name: String,
93 pub role: OutputRole,
95 pub sensitive: bool,
97 pub value: Option<String>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[non_exhaustive]
104pub struct Status {
105 pub state: RunState,
107 pub provider: Option<String>,
109 pub machines: Vec<MachineStatus>,
111 pub networks: Vec<NetworkView>,
113 pub outputs: Vec<OutputValue>,
115 pub message: Option<String>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121#[non_exhaustive]
122pub struct Run {
123 pub record: RunRecord,
125 pub outputs: Vec<OutputValue>,
127 pub message: Option<String>,
129}
130
131pub(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 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
155pub(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
162pub(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}