Skip to main content

isothrum_runner/
lab.rs

1//! One manifest opened under one run name: the operations an embedder calls. `operation` is what
2//! every operation does around its own work, `up` starts a run, `existing` holds the operations on
3//! a run that exists (down, park, resume, check, provision).
4
5mod existing;
6mod operation;
7mod seal;
8#[cfg(test)]
9mod tests;
10mod up;
11
12use std::collections::BTreeMap;
13use std::path::Path;
14
15use isoloom_manifest::{Manifest, RunVar};
16
17use crate::Result;
18use crate::driver::AnyDriver;
19use crate::error::Error;
20use crate::events::Emitter;
21use crate::events::WarningCode;
22use crate::executor::{Cx, Stale};
23use crate::hooks::{Edge, LabView};
24use crate::manifest::LoadedManifest;
25use crate::name::RunName;
26use crate::options::RunOptions;
27use crate::outputs;
28use crate::params::params_for_existing;
29use crate::runner::Runner;
30use crate::scope::Scope;
31use crate::secrets::Secrets;
32use crate::state::Op;
33use crate::status::{self, OutputValue, Status};
34
35/// A run: a manifest under a name, on a runner.
36pub struct Lab<'r> {
37    pub(crate) runner: &'r Runner,
38    loaded: LoadedManifest,
39    name: RunName,
40    pub(crate) driver: AnyDriver,
41    /// Values of the manifest's connection slots, given once ([`Lab::with_connection`]).
42    connection: Secrets,
43}
44
45impl std::fmt::Debug for Lab<'_> {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        f.debug_struct("Lab")
48            .field("name", &self.name)
49            .field("manifest", &self.loaded.path)
50            .finish_non_exhaustive()
51    }
52}
53
54impl<'r> Lab<'r> {
55    pub(crate) fn new(runner: &'r Runner, loaded: LoadedManifest, name: RunName) -> Result<Lab<'r>> {
56        if !loaded.manifest.hosts.contains(&runner.config.host) {
57            let target = serde_json::to_value(loaded.manifest.target).unwrap_or_default();
58            return Err(Error::Unsupported {
59                what: format!("target {target} on this host"),
60            });
61        }
62        let driver = AnyDriver::for_manifest(&loaded.manifest)?;
63        Ok(Lab {
64            runner,
65            loaded,
66            name,
67            driver,
68            connection: Secrets::new(),
69        })
70    }
71
72    /// The lab with `secrets`, the values of its connection slots (an ESXi host's password),
73    /// delivered to every operation: status and park, which take no options, too. Kept in memory
74    /// only, never stored; a value an operation's options give wins.
75    #[must_use]
76    pub fn with_connection(mut self, secrets: Secrets) -> Lab<'r> {
77        self.connection = secrets;
78        self
79    }
80
81    /// `opts` with the connection's values its secrets lack.
82    pub(crate) fn connected(&self, opts: &RunOptions) -> RunOptions {
83        let mut out = opts.clone();
84        for (name, value) in self.connection.iter() {
85            if out.secrets.get(name).is_none() {
86                out.secrets.insert(name, value.clone());
87            }
88        }
89        out
90    }
91
92    /// The manifest.
93    #[must_use]
94    pub fn manifest(&self) -> &Manifest {
95        &self.loaded.manifest
96    }
97
98    /// The run's name.
99    #[must_use]
100    pub fn name(&self) -> &RunName {
101        &self.name
102    }
103
104    /// The manifest's file.
105    #[must_use]
106    pub fn path(&self) -> &Path {
107        &self.loaded.path
108    }
109
110    /// The manifest's folder (`${run.dir}`).
111    pub(crate) fn dir(&self) -> &Path {
112        &self.loaded.dir
113    }
114
115    /// The manifest's SHA-256.
116    pub(crate) fn loaded_sha256(&self) -> &str {
117        &self.loaded.sha256
118    }
119
120    /// The run as it stands: read-only, never locked, and never an event.
121    ///
122    /// # Errors
123    ///
124    /// Fails when the engine can't be asked (the status step fails or runs out of time) or the
125    /// run's record can't be read.
126    pub async fn status(&self) -> Result<Status> {
127        let record = self.runner.store.record(&self.name)?;
128        let provider = record.as_ref().and_then(|r| r.provider.clone());
129        let params = params_for_existing(
130            self.manifest(),
131            record.as_ref().map(|r| &r.params),
132            &BTreeMap::new(),
133            &self.scope(provider.clone()),
134        )?;
135        let silent = |_: &crate::events::Event| {};
136        let events = Emitter::new(self.name.as_str(), Op::Check, self.runner.config.clock.as_ref(), &silent);
137        let secrets = self.connection.clone();
138        let mut cx = self.cx(params, &secrets, &events, provider.clone())?;
139        if let Some(r) = &record {
140            Self::bind_record(&mut cx, r);
141        }
142        self.driver.prepare(&cx)?;
143        self.relay(&mut cx).await?;
144        let observed = self.observe(&mut cx).await?;
145        cx.vars.set_published(observed.published.clone());
146        cx.vars.set_hosts(observed.hosts.clone());
147        let state = match self.runner.op_of(&self.name).and_then(Op::during) {
148            Some(during) => during,
149            None => status::derive_state(&observed.machines, record.as_ref()),
150        };
151        let outputs = outputs::read(self.manifest(), &cx.vars, &observed.outputs, &observed.sensitive);
152        outputs::bind(&mut cx.vars, &outputs);
153        let message = (state == crate::RunState::Ready)
154            .then(|| outputs::message(self.manifest(), &cx.vars, &outputs))
155            .flatten();
156        Ok(Status {
157            state,
158            provider,
159            machines: observed.machines,
160            networks: self.networks(&cx),
161            outputs,
162            message,
163        })
164    }
165
166    /// The run's outputs, read live.
167    ///
168    /// # Errors
169    ///
170    /// Fails as [`Lab::status`] does.
171    pub async fn outputs(&self) -> Result<Vec<OutputValue>> {
172        Ok(self.status().await?.outputs)
173    }
174
175    /// What `params`, `secrets` and `events` give this run's steps.
176    pub(crate) fn cx<'a>(
177        &'a self,
178        params: BTreeMap<String, String>,
179        secrets: &'a Secrets,
180        events: &'a Emitter<'a>,
181        provider: Option<String>,
182    ) -> Result<Cx<'a>> {
183        let state_dir = self.runner.store.run_dir(&self.name)?;
184        let mut vars = crate::vars::Vars::default();
185        vars.set_run(RunVar::Name, self.name.as_str());
186        vars.set_run(RunVar::Dir, self.dir().display().to_string());
187        vars.set_run(RunVar::StateDir, state_dir.display().to_string());
188        vars.set_run(RunVar::KnownHosts, state_dir.join("known_hosts").display().to_string());
189        vars.set_run(RunVar::SshKey, crate::ssh::key_path(&self.runner.config.home).display().to_string());
190        if let Some(key) = crate::ssh::public_key(&self.runner.config.home) {
191            vars.set_run(RunVar::SshPublicKey, key);
192        }
193        if let Some(p) = &provider {
194            vars.set_run(RunVar::Provider, p.clone());
195        }
196        vars.set_params(params);
197        let stale = self.manifest().recovery.as_ref().map(|r| Stale::new(&r.stale_markers)).unwrap_or_default();
198        Ok(Cx {
199            lab: self,
200            vars,
201            secrets,
202            events,
203            scope: self.scope(provider),
204            stale,
205            processes: std::sync::Arc::clone(&self.runner.config.processes),
206            ssh_jump: None,
207            expires_at: None,
208            relayed: None,
209        })
210    }
211
212    /// `cx` for an operation that acts on the run (never a status read): on a provider whose
213    /// management commands need elevation (Hyper-V on Windows), its tools run through one
214    /// elevated session, kept in the run's state folder.
215    pub(crate) fn acting<'a>(&'a self, mut cx: Cx<'a>) -> Result<Cx<'a>> {
216        if cx.scope.host == isoloom_manifest::HostOs::Windows && self.driver.elevates(&cx.scope) {
217            let dir = self.runner.store.run_dir(&self.name)?.join("elevated");
218            let own = std::env::vars_os().filter_map(|(k, v)| Some((k.into_string().ok()?, v.into_string().ok()?)));
219            let inner = std::sync::Arc::clone(&self.runner.config.processes);
220            cx.processes = std::sync::Arc::new(crate::process::Elevated::new(inner, dir, own));
221        }
222        Ok(cx)
223    }
224
225    /// Starts the macOS relays `cx`'s run needs (Vagrant on an ESXi host), for the operation.
226    pub(crate) async fn relay(&self, cx: &mut Cx<'_>) -> Result<()> {
227        let config = &self.runner.config;
228        let vagrant = self.manifest().driver == isoloom_manifest::Driver::Vagrant;
229        let params = cx.vars.params();
230        let Some(host) = crate::relay::esxi_host(config.host == isoloom_manifest::HostOs::Macos, vagrant, cx.scope.provider.as_deref(), params) else {
231            return Ok(());
232        };
233        let port = params.get("ESXI_HOSTPORT").and_then(|p| p.parse().ok()).unwrap_or(22);
234        let relayed = crate::relay::prepare(host, port, &config.home, config.path.as_deref()).await;
235        cx.relayed = Some(relayed.map_err(Error::io(config.home.join("relay"), "set up"))?);
236        Ok(())
237    }
238
239    /// Binds what `record` keeps of the run into `cx`: its slot (`${run.slot}`), its SSH jump and
240    /// its expiry.
241    pub(crate) fn bind_record(cx: &mut Cx<'_>, record: &crate::store::RunRecord) {
242        if let Some(slot) = record.slot {
243            cx.vars.set_run(RunVar::Slot, slot.to_string());
244        }
245        cx.ssh_jump.clone_from(&record.ssh_jump);
246        cx.expires_at = record.expires_at;
247    }
248
249    /// This host, with `provider`.
250    pub(crate) fn scope(&self, provider: Option<String>) -> Scope {
251        Scope {
252            host: self.runner.config.host,
253            provider,
254        }
255    }
256
257    /// Reads what the engine gives back (published ports, outputs) and binds it, so a template
258    /// can name it (`${outputs.ip}`): the run's outputs.
259    pub(crate) async fn read_outputs(&self, cx: &mut Cx<'_>) -> Result<Vec<status::OutputValue>> {
260        let observed = self.observe(cx).await?;
261        Ok(self.bind_observed(cx, observed))
262    }
263
264    /// The run as its engine reports it: the driver's machines, then, when the lab's containers
265    /// run inside one of its VMs (`docker-vm`'s guest), those containers as read in the VM.
266    pub(crate) async fn observe(&self, cx: &mut Cx<'_>) -> Result<crate::driver::Observed> {
267        let mut observed = self.driver.observe(cx).await?;
268        if let Some(guest) = &self.manifest().guest {
269            crate::in_vm::observe(cx, guest, &mut observed).await;
270        }
271        Ok(observed)
272    }
273
274    /// Binds what `observed` gives back (published ports, outputs) into `cx`: the run's outputs.
275    pub(crate) fn bind_observed(&self, cx: &mut Cx<'_>, observed: crate::driver::Observed) -> Vec<status::OutputValue> {
276        cx.vars.set_published(observed.published);
277        cx.vars.set_hosts(observed.hosts);
278        let outputs = outputs::read(self.manifest(), &cx.vars, &observed.outputs, &observed.sensitive);
279        // A value the engine or the manifest keeps secret is masked in every later event.
280        cx.events.hide(outputs.iter().filter(|o| o.sensitive).filter_map(|o| o.value.clone()));
281        outputs::bind(&mut cx.vars, &outputs);
282        outputs
283    }
284
285    /// The lab's networks, their native names filled.
286    fn networks(&self, cx: &Cx<'_>) -> Vec<status::NetworkView> {
287        status::networks(self.manifest(), |t| cx.vars.render(t, "networks.native").ok())
288    }
289
290    /// Runs the embedder's hooks at `edge`; each failure is a warning.
291    pub(crate) async fn hooks(&self, cx: &Cx<'_>, edge: Edge, outputs: &[OutputValue]) {
292        if self.runner.hooks.is_empty() {
293            return;
294        }
295        let networks = self.networks(cx);
296        let view = LabView {
297            name: &self.name,
298            manifest: self.manifest(),
299            networks: &networks,
300            outputs,
301        };
302        for failure in edge.run(&self.runner.hooks, &view).await {
303            cx.events.warning(WarningCode::HookFailed, &failure);
304        }
305    }
306}