1mod 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
35pub struct Lab<'r> {
37 pub(crate) runner: &'r Runner,
38 loaded: LoadedManifest,
39 name: RunName,
40 pub(crate) driver: AnyDriver,
41 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 #[must_use]
76 pub fn with_connection(mut self, secrets: Secrets) -> Lab<'r> {
77 self.connection = secrets;
78 self
79 }
80
81 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 #[must_use]
94 pub fn manifest(&self) -> &Manifest {
95 &self.loaded.manifest
96 }
97
98 #[must_use]
100 pub fn name(&self) -> &RunName {
101 &self.name
102 }
103
104 #[must_use]
106 pub fn path(&self) -> &Path {
107 &self.loaded.path
108 }
109
110 pub(crate) fn dir(&self) -> &Path {
112 &self.loaded.dir
113 }
114
115 pub(crate) fn loaded_sha256(&self) -> &str {
117 &self.loaded.sha256
118 }
119
120 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 pub async fn outputs(&self) -> Result<Vec<OutputValue>> {
172 Ok(self.status().await?.outputs)
173 }
174
175 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 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 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 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 pub(crate) fn scope(&self, provider: Option<String>) -> Scope {
251 Scope {
252 host: self.runner.config.host,
253 provider,
254 }
255 }
256
257 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 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 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 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 fn networks(&self, cx: &Cx<'_>) -> Vec<status::NetworkView> {
287 status::networks(self.manifest(), |t| cx.vars.render(t, "networks.native").ok())
288 }
289
290 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}