Skip to main content

isothrum_runner/run/
attach.rs

1//! Hot-adding the on-demand access machine (the learner's attacker box) into a running run, and
2//! hot-removing it. The box is a manifest machine marked `on_demand` that the initial `up` left
3//! down (a Compose `access` profile, a Vagrant `autostart: false`); `attach` brings it up on a
4//! chosen network with the learner's picked image, `detach` takes it back out.
5
6use isoloom_manifest::{Machine, Native};
7use serde::Serialize;
8use serde_json::json;
9
10use super::Running;
11use super::operation::Done;
12use crate::Result;
13use crate::error::Error;
14use crate::events::{Emitter, Events};
15use crate::options::RunOptions;
16use crate::secrets::sensitive_values;
17use crate::state::Op;
18use crate::status::{ShellCommand, any_target_running};
19
20/// What a hot-add produced: the access box brought up, and where to reach it.
21#[derive(Debug, Clone, Serialize)]
22pub struct AttachReport {
23    /// The access machine's manifest name.
24    pub machine: String,
25    /// Its native name (the Compose service, the Vagrant define).
26    pub native: String,
27    /// The lab network it is on (the chosen one, else its first).
28    pub network: Option<String>,
29    /// Its static address on that network, for the learner to reach it.
30    pub address: Option<String>,
31    /// The image or box it was brought up with, when the learner picked one.
32    pub image: Option<String>,
33}
34
35impl Running<'_> {
36    /// Hot-adds the on-demand access machine into the running run: brings its box up with the
37    /// learner-picked `image` (its baked default otherwise), on `network` when given. `machine`
38    /// names which on-demand machine, needed only when the lab has more than one.
39    ///
40    /// # Errors
41    ///
42    /// Fails with `machine_unknown` for a named machine the lab has no on-demand machine for,
43    /// `unsupported` when the lab has no on-demand machine (or more than one and none is named, or
44    /// the target is a cloud one), `not_running` when the run isn't up, and as the other
45    /// operations do for a step or the state.
46    pub async fn attach(&self, machine: Option<&str>, network: Option<&str>, image: Option<&str>, events: &dyn Events) -> Result<AttachReport> {
47        let opts = self.connected(&RunOptions::default());
48        let mask = sensitive_values(self.manifest(), &opts.secrets);
49        self.operate(Op::Attach, events, mask, async |e: &Emitter<'_>| {
50            let access = self.access_machine(machine)?.clone();
51            let native = native_name(&access).to_owned();
52            let (_, mut cx) = self.existing(&opts, e).await?;
53            if !any_target_running(&self.observe(&mut cx).await?.machines) {
54                return Err(Error::NotRunning { run: self.name().to_string() });
55            }
56            self.driver.attach(&cx, &native, image, network).await?;
57            let (network, address) = address_on(&access, network);
58            let report = AttachReport {
59                machine: access.name.as_str().to_owned(),
60                native,
61                network,
62                address,
63                image: image.map(str::to_owned),
64            };
65            let data = json!({ "machine": report.machine, "native": report.native, "network": report.network, "address": report.address });
66            Ok(Done {
67                value: report,
68                ok: true,
69                data,
70                state: None,
71            })
72        })
73        .await
74    }
75
76    /// Hot-removes the on-demand access machine from the running run, leaving the rest up.
77    ///
78    /// # Errors
79    ///
80    /// Fails as [`Running::attach`] does for the machine choice and the target, and as the other
81    /// operations do for a step or the state.
82    pub async fn detach(&self, machine: Option<&str>, events: &dyn Events) -> Result<()> {
83        let opts = self.connected(&RunOptions::default());
84        let mask = sensitive_values(self.manifest(), &opts.secrets);
85        self.operate(Op::Detach, events, mask, async |e: &Emitter<'_>| {
86            let access = self.access_machine(machine)?.clone();
87            let native = native_name(&access).to_owned();
88            let (_, cx) = self.existing(&opts, e).await?;
89            self.driver.detach(&cx, &native).await?;
90            Ok(Done {
91                value: (),
92                ok: true,
93                data: json!({ "machine": access.name.as_str(), "native": native }),
94                state: None,
95            })
96        })
97        .await
98    }
99
100    /// The command that opens an interactive shell into the on-demand access machine: the runner
101    /// builds it from the run's Compose project / Vagrant define, so the embedder opens the box by
102    /// service or define name and never reconstructs a container name. A pure query — it runs
103    /// nothing, emits nothing, and doesn't check the box is up (the embedder runs what it returns
104    /// in its own terminal once the box is attached).
105    ///
106    /// # Errors
107    ///
108    /// Fails as [`Running::attach`] does for the machine choice, and `unsupported` on a cloud
109    /// target (its attacker runs locally, not in the cloud).
110    pub fn shell_command(&self, machine: Option<&str>) -> Result<ShellCommand> {
111        let access = self.access_machine(machine)?;
112        let native = native_name(access).to_owned();
113        let (_, provider, params) = self.query_prelude()?;
114        let silent = |_: &crate::events::Event| {};
115        let emit = Emitter::new(self.name().as_str(), Op::Check, self.runner.config.clock.as_ref(), &silent);
116        let cx = self.cx(params, &self.connection, &emit, provider)?;
117        self.driver.shell_command(&cx, &native)
118    }
119
120    /// The manifest's on-demand access machine: the one `machine` names, or the only one.
121    fn access_machine(&self, machine: Option<&str>) -> Result<&Machine> {
122        let mut on_demand = self.manifest().machines.iter().filter(|m| m.on_demand);
123        if let Some(name) = machine {
124            return on_demand.find(|m| m.name.as_str() == name).ok_or_else(|| Error::MachineUnknown {
125                run: self.name().to_string(),
126                machine: name.to_owned(),
127            });
128        }
129        match (on_demand.next(), on_demand.next()) {
130            (Some(one), None) => Ok(one),
131            (None, _) => Err(Error::Unsupported {
132                what: "hot-adding an access machine: this lab has no on-demand access machine".into(),
133            }),
134            (Some(_), Some(_)) => {
135                let names = self
136                    .manifest()
137                    .machines
138                    .iter()
139                    .filter(|m| m.on_demand)
140                    .map(|m| m.name.as_str())
141                    .collect::<Vec<_>>()
142                    .join(", ");
143                Err(Error::Unsupported {
144                    what: format!("hot-adding an access machine: this lab has several ({names}); name which"),
145                })
146            }
147        }
148    }
149}
150
151/// The native name the driver addresses the box by.
152fn native_name(m: &Machine) -> &str {
153    match &m.native {
154        Some(Native::Compose { service }) => service.as_str(),
155        Some(Native::Vagrant { vagrant, .. }) => vagrant.as_str(),
156        _ => m.name.as_str(),
157    }
158}
159
160/// The box's static address on `network` (its first when none is named), from the manifest.
161fn address_on(m: &Machine, network: Option<&str>) -> (Option<String>, Option<String>) {
162    match network {
163        Some(n) => (
164            Some(n.to_owned()),
165            m.addresses.iter().find(|(k, _)| k.as_str() == n).map(|(_, a)| a.to_string()),
166        ),
167        None => m
168            .addresses
169            .iter()
170            .next()
171            .map_or((None, None), |(k, a)| (Some(k.as_str().to_owned()), Some(a.to_string()))),
172    }
173}