1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//! The Isothrum runner: executes the run manifest Isoloom writes next to a target's generated
//! files (`.isoloom/<target>/run.json`), and reads nothing else of them.
//!
//! The manifest holds the native commands in order, the documented driver actions, readiness,
//! outputs, check runners, and the slots inputs and secrets travel through. The runner adds what
//! only it can know: the run's name and private state, one operation per run at a time, the
//! values of params and secrets, and the event stream an embedder (the Cyber CTF launcher, the
//! Isothrum desktop app, the `isothrum` command line) shows. It sits between Isoloom (whose
//! `isoloom-manifest` crate is the contract) and those embedders, and depends on no UI.
//!
//! # Example
//!
//! Opening a manifest reads and checks it, and nothing runs yet:
//!
//! ```
//! use isothrum_runner::{RunName, Runner, RunnerConfig};
//!
//! let home = tempfile::tempdir()?;
//! let lab = tempfile::tempdir()?;
//! let manifest = lab.path().join("run.json");
//! std::fs::write(&manifest, include_str!("../tests/fixtures/flag-portal.run.json"))?;
//!
//! let runner = Runner::new(RunnerConfig::new(home.path()))?;
//! let lab = runner.open(&manifest, Some(RunName::new("demo")?))?;
//! assert_eq!(lab.manifest().lab.name.as_str(), "flag-portal");
//! assert_eq!(lab.name().as_str(), "demo");
//! assert!(runner.runs()?.is_empty(), "no operation has run");
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! Running it takes the tools it names (here Docker), and an event sink:
//!
//! ```no_run
//! # async fn demo(lab: &isothrum_runner::Lab<'_>) -> isothrum_runner::Result<()> {
//! use isothrum_runner::{JsonLines, Park, RunOptions};
//!
//! let events = JsonLines::new(std::io::stderr());
//! let opts = RunOptions::new().with_param("ISOLOOM_PUBLISH_FIXED", "1");
//! let run = lab.up(&opts, &events).await?;
//! let report = lab.check(&opts, &events).await?;
//! lab.park(Park::Shutdown, &events).await?;
//! lab.resume(&opts, &events).await?;
//! lab.down(&opts, &events).await?;
//! # let _ = (run, report);
//! # Ok(())
//! # }
//! ```
//!
//! # Ports
//!
//! Every outside effect goes through a port the embedder can replace: [`ProcessRunner`] (the
//! tools), [`HostPorts`] (the host's ports), [`Clock`] (the time), [`Events`] (where events go)
//! and [`Hooks`] (the embedder's sidecars). They are open traits on purpose, since embedders
//! implement them; the `fake` feature has test doubles for the first three.
//!
//! # Deliveries
//!
//! Params and slot values reach the tools only through the manifest's deliveries, never on a
//! command line: `env` (a step's variables), `tf_var` (`TF_VAR_<var>`, a `map_entry` making one
//! JSON object), `state_file` (an owner-only file in the run's state folder, a Terraform variable
//! file or `NAME=value` lines, written at `up`, kept for the destroy, removed by a successful
//! `down`), and `guest` over SSH, Isoloom's delivery into a machine for a cloud with no secret
//! store (DigitalOcean, Linode): once the step it names (else every `up` step) succeeded, one
//! SSH session per machine, to the outputs `address_<machine>` (`-` as `_`) and
//! `ssh_user_<machine>` (else `ssh_user`), runs a fixed script as root that keeps each value
//! owner-only under `/var/lib/isoloom/secrets/<slot name>` and then touches
//! `/var/lib/isoloom/secrets/.delivered`, which the machine's set-up waits for; the values travel
//! base64-encoded on its standard input, retried while SSH can't connect (up to 15 minutes).
//!
//! # Concurrency
//!
//! The runner is **async**, on Tokio, chosen once for the whole crate: every tool runs as an async
//! process with a time limit (a step's `timeout_s`, else an hour; 25 s for a probe; two minutes
//! for an action's own Docker calls; ten for a hook), and dropping an operation's future ends the
//! process tree it started. Operations on one run are serialized; operations on different runs
//! run side by side. One exception to "no blocking call in async code": the run's state files (a
//! record, a lock, the pinned ports, the event log) are a few KB on the local disk, so they are
//! read and written inline rather than through `spawn_blocking`.
//!
//! # Errors and tracing
//!
//! Every fallible call returns [`Result`], whose [`Error`] has a stable [`ErrorCode`]; an
//! operation also reports its failure as its last event. The runner logs through `tracing` (spans
//! `run`, `step` and `driver`, events for each process), never by printing.
pub use Adoption;
pub use ;
pub use FixedClock;
pub use ;
pub use ;
pub use ;
pub use BoxFuture;
pub use ;
pub use isoloom_manifest;
pub use Lab;
pub use ;
pub use RunName;
pub use FakePorts;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;