Skip to content

Commit 0df03ba

Browse files
committed
inside_docker: allow to run Rustwide inside a Docker container
1 parent 1adc0fd commit 0df03ba

File tree

11 files changed

+288
-25
lines changed

11 files changed

+288
-25
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- New method `BuildBuilder::run` to run a build.
1717
- New method `Command::log_command` to disable logging the command name and
1818
args before executing it.
19+
- New method `WorkspaceBuilder::running_inside_docker` to adapt Rustwide itself
20+
to run inside a Docker container.
1921

2022
### Changed
2123

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ walkdir = "2.2"
4040
toml = "0.4.6"
4141
fs2 = "0.4.3"
4242
remove_dir_all = "0.5.2"
43+
base64 = "0.10.1"
44+
getrandom = "0.1.12"
4345

4446
[dev-dependencies]
4547
env_logger = "0.6.1"

src/cmd/sandbox.rs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,33 +66,51 @@ struct MountConfig {
6666
}
6767

6868
impl MountConfig {
69-
fn to_volume_arg(&self) -> String {
69+
fn host_path(&self, workspace: &Workspace) -> Result<PathBuf, Error> {
70+
if let Some(container) = workspace.current_container() {
71+
// If we're inside a Docker container we'll need to remap the mount sources to point to
72+
// the directories in the host system instead of the containers. To do that we try to
73+
// see if the mount source is inside an existing mount point, and "rebase" the path.
74+
let inside_container_path = crate::utils::normalize_path(&self.host_path);
75+
for mount in container.mounts() {
76+
let dest = crate::utils::normalize_path(Path::new(mount.destination()));
77+
if let Ok(shared) = inside_container_path.strip_prefix(&dest) {
78+
return Ok(Path::new(mount.source()).join(shared));
79+
}
80+
}
81+
failure::bail!("the workspace is not mounted from outside the container");
82+
} else {
83+
Ok(crate::utils::normalize_path(&self.host_path))
84+
}
85+
}
86+
87+
fn to_volume_arg(&self, workspace: &Workspace) -> Result<String, Error> {
7088
let perm = match self.perm {
7189
MountKind::ReadWrite => "rw",
7290
MountKind::ReadOnly => "ro",
7391
MountKind::__NonExaustive => panic!("do not create __NonExaustive variants manually"),
7492
};
75-
format!(
93+
Ok(format!(
7694
"{}:{}:{},Z",
77-
absolute(&self.host_path).to_string_lossy(),
95+
self.host_path(workspace)?.to_string_lossy(),
7896
self.sandbox_path.to_string_lossy(),
7997
perm
80-
)
98+
))
8199
}
82100

83-
fn to_mount_arg(&self) -> String {
101+
fn to_mount_arg(&self, workspace: &Workspace) -> Result<String, Error> {
84102
let mut opts_with_leading_comma = vec![];
85103

86104
if self.perm == MountKind::ReadOnly {
87105
opts_with_leading_comma.push(",readonly");
88106
}
89107

90-
format!(
108+
Ok(format!(
91109
"type=bind,src={},dst={}{}",
92-
absolute(&self.host_path).to_string_lossy(),
110+
self.host_path(workspace)?.to_string_lossy(),
93111
self.sandbox_path.to_string_lossy(),
94112
opts_with_leading_comma.join(""),
95-
)
113+
))
96114
}
97115
}
98116

@@ -175,10 +193,10 @@ impl SandboxBuilder {
175193
// Linux we need the Z flag, which doesn't work with `--mount`, for SELinux relabeling.
176194
if cfg!(windows) {
177195
args.push("--mount".into());
178-
args.push(mount.to_mount_arg())
196+
args.push(mount.to_mount_arg(workspace)?)
179197
} else {
180198
args.push("-v".into());
181-
args.push(mount.to_volume_arg())
199+
args.push(mount.to_volume_arg(workspace)?)
182200
}
183201
}
184202

@@ -245,15 +263,6 @@ impl SandboxBuilder {
245263
}
246264
}
247265

248-
fn absolute(path: &Path) -> PathBuf {
249-
if path.is_absolute() {
250-
path.to_owned()
251-
} else {
252-
let cd = std::env::current_dir().expect("unable to get current dir");
253-
cd.join(path)
254-
}
255-
}
256-
257266
#[derive(Deserialize)]
258267
#[serde(rename_all = "PascalCase")]
259268
struct InspectContainer {

src/inside_docker.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use crate::cmd::Command;
2+
use crate::workspace::Workspace;
3+
use failure::Error;
4+
use getrandom::getrandom;
5+
use log::info;
6+
7+
static PROBE_FILENAME: &str = "rustwide-probe";
8+
9+
pub(crate) struct CurrentContainer {
10+
metadata: Metadata,
11+
}
12+
13+
impl CurrentContainer {
14+
pub(crate) fn detect(workspace: &Workspace) -> Result<Option<Self>, Error> {
15+
if let Some(id) = probe_container_id(workspace)? {
16+
info!("inspecting the current container");
17+
let inspect = Command::new(workspace, "docker")
18+
.args(&["inspect", &id])
19+
.log_output(false)
20+
.log_command(false)
21+
.run_capture()?;
22+
let content = inspect.stdout_lines().join("\n");
23+
let mut metadata: Vec<Metadata> = serde_json::from_str(&content)?;
24+
if metadata.len() != 1 {
25+
failure::bail!("invalid output returned by `docker inspect`");
26+
}
27+
Ok(Some(CurrentContainer {
28+
metadata: metadata.pop().unwrap(),
29+
}))
30+
} else {
31+
Ok(None)
32+
}
33+
}
34+
35+
pub(crate) fn mounts(&self) -> &[Mount] {
36+
&self.metadata.mounts
37+
}
38+
}
39+
40+
/// Apparently there is no cross platform way to easily get the current container ID from Docker
41+
/// itself. On Linux is possible to inspect the cgroups and parse the ID out of there, but of
42+
/// course cgroups are not available on Windows.
43+
///
44+
/// This function uses a simpler but slower method to get the ID: a file with a random string is
45+
/// created in the temp directory, the list of all the containers is fetched from Docker and then
46+
/// `cat` is executed inside each of them to check whether they have the same random string.
47+
pub(crate) fn probe_container_id(workspace: &Workspace) -> Result<Option<String>, Error> {
48+
info!("detecting the ID of the container where rustwide is running");
49+
50+
// Create the probe on the current file system
51+
let probe_path = std::env::temp_dir().join(PROBE_FILENAME);
52+
let probe_path_str = probe_path.to_str().unwrap();
53+
let mut probe_content = [0u8; 64];
54+
getrandom(&mut probe_content)?;
55+
let probe_content = base64::encode(&probe_content[..]);
56+
std::fs::write(&probe_path, probe_content.as_bytes())?;
57+
58+
// Check if the probe exists on any of the currently running containers.
59+
let out = Command::new(workspace, "docker")
60+
.args(&["ps", "--format", "{{.ID}}", "--no-trunc"])
61+
.log_output(false)
62+
.log_command(false)
63+
.run_capture()?;
64+
for id in out.stdout_lines() {
65+
info!("probing container id {}", id);
66+
67+
let res = Command::new(workspace, "docker")
68+
.args(&["exec", &id, "cat", probe_path_str])
69+
.log_output(false)
70+
.log_command(false)
71+
.run_capture();
72+
if let Ok(&[ref probed]) = res.as_ref().map(|out| out.stdout_lines()) {
73+
if *probed == probe_content {
74+
info!("probe successful, this is container ID {}", id);
75+
return Ok(Some(id.clone()));
76+
}
77+
}
78+
}
79+
80+
info!("probe unsuccessful, this is not running inside a container");
81+
Ok(None)
82+
}
83+
84+
#[derive(serde::Deserialize)]
85+
#[serde(rename_all = "PascalCase")]
86+
struct Metadata {
87+
mounts: Vec<Mount>,
88+
}
89+
90+
#[derive(serde::Deserialize)]
91+
#[serde(rename_all = "PascalCase")]
92+
pub(crate) struct Mount {
93+
source: String,
94+
destination: String,
95+
}
96+
97+
impl Mount {
98+
pub(crate) fn source(&self) -> &str {
99+
&self.source
100+
}
101+
102+
pub(crate) fn destination(&self) -> &str {
103+
&self.destination
104+
}
105+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extern crate toml;
2121
mod build;
2222
pub mod cmd;
2323
mod crates;
24+
mod inside_docker;
2425
pub mod logging;
2526
mod native;
2627
mod prepare;

src/workspace.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::build::BuildDirectory;
22
use crate::cmd::{Command, SandboxImage};
3+
use crate::inside_docker::CurrentContainer;
34
use crate::Toolchain;
45
use failure::{Error, ResultExt};
56
use log::info;
@@ -25,6 +26,7 @@ pub struct WorkspaceBuilder {
2526
command_timeout: Option<Duration>,
2627
command_no_output_timeout: Option<Duration>,
2728
fetch_registry_index_during_builds: bool,
29+
running_inside_docker: bool,
2830
fast_init: bool,
2931
}
3032

@@ -41,6 +43,7 @@ impl WorkspaceBuilder {
4143
command_timeout: DEFAULT_COMMAND_TIMEOUT,
4244
command_no_output_timeout: DEFAULT_COMMAND_NO_OUTPUT_TIMEOUT,
4345
fetch_registry_index_during_builds: false,
46+
running_inside_docker: false,
4447
fast_init: false,
4548
}
4649
}
@@ -103,6 +106,25 @@ impl WorkspaceBuilder {
103106
self
104107
}
105108

109+
/// Enable or disable support for running Rustwide itself inside Docker (disabled by default).
110+
///
111+
/// When support is enabled Rustwide will try to detect whether it's actually running inside a
112+
/// Docker container during initialization, and in that case it will adapt itself. This is
113+
/// needed because starting a sibling container from another one requires mount sources to be
114+
/// remapped to the real directory on the host.
115+
///
116+
/// Other than enabling support for it, to run Rustwide inside Docker your container needs to
117+
/// meet these requirements:
118+
///
119+
/// * The Docker socker (`/var/run/docker.sock`) needs to be mounted inside the container.
120+
/// * The workspace directory must be either mounted from the host system or in a child
121+
/// directory of a mount from the host system. Workspaces created inside the container are
122+
/// not supported.
123+
pub fn running_inside_docker(mut self, inside: bool) -> Self {
124+
self.running_inside_docker = inside;
125+
self
126+
}
127+
106128
/// Initialize the workspace. This will create all the necessary local files and fetch the rest from the network. It's
107129
/// not unexpected for this method to take minutes to run on slower network connections.
108130
pub fn init(self) -> Result<Workspace, Error> {
@@ -126,16 +148,23 @@ impl WorkspaceBuilder {
126148
.default_headers(headers)
127149
.build()?;
128150

129-
let ws = Workspace {
151+
let mut ws = Workspace {
130152
inner: Arc::new(WorkspaceInner {
131153
http,
132154
path: self.path,
133155
sandbox_image,
134156
command_timeout: self.command_timeout,
135157
command_no_output_timeout: self.command_no_output_timeout,
136158
fetch_registry_index_during_builds: self.fetch_registry_index_during_builds,
159+
current_container: None,
137160
}),
138161
};
162+
163+
if self.running_inside_docker {
164+
let container = CurrentContainer::detect(&ws)?;
165+
Arc::get_mut(&mut ws.inner).unwrap().current_container = container;
166+
}
167+
139168
ws.init(self.fast_init)?;
140169
Ok(ws)
141170
})
@@ -149,6 +178,7 @@ struct WorkspaceInner {
149178
command_timeout: Option<Duration>,
150179
command_no_output_timeout: Option<Duration>,
151180
fetch_registry_index_during_builds: bool,
181+
current_container: Option<CurrentContainer>,
152182
}
153183

154184
/// Directory on the filesystem containing rustwide's state and caches.
@@ -238,6 +268,10 @@ impl Workspace {
238268
self.inner.fetch_registry_index_during_builds
239269
}
240270

271+
pub(crate) fn current_container(&self) -> Option<&CurrentContainer> {
272+
self.inner.current_container.as_ref()
273+
}
274+
241275
fn init(&self, fast_init: bool) -> Result<(), Error> {
242276
info!("installing tools required by rustwide");
243277
crate::tools::install(self, fast_init)?;

0 commit comments

Comments
 (0)