anvilsign in

collin/anvil

1//! Docker plumbing shared by the job runner (`anvil-worker`) and the
2//! agent-session supervisor (`anvil-agent`).
3//!
4//! Both create containers from the same runner image
5//! (`anvil_core::config::DEFAULT_RUNNER_IMAGE`) against the same daemon, so the
6//! connect/pull dance lives here rather than being written twice. Its own crate
7//! rather than a module of either, so `anvil-agent` need not depend on the
8//! runner and the runner need not depend on `anvil-core`.
9
10use bollard::{
11 Docker,
12 image::CreateImageOptions,
13};
14use futures_util::StreamExt;
15
16/// Connect to the daemon.
17///
18/// Environment-aware (`DOCKER_HOST`, `DOCKER_CERT_PATH`, …) rather than the
19/// hardcoded `/var/run/docker.sock` this used to use. A runner on macOS is the
20/// reason: Docker Desktop creates that symlink only when "Allow the default
21/// Docker socket to be used" is ticked, and puts the real socket at
22/// `~/.docker/run/docker.sock`; Colima and OrbStack differ again. Falling back
23/// to the socket default when nothing is set keeps Linux behaviour identical.
24pub fn connect() -> Result<Docker, String> {
25 Docker::connect_with_defaults()
26 .map_err(|e| format!("docker unavailable (is DOCKER_HOST/the socket right?): {e}"))
27}
28
29/// Make sure `image` is present locally, pulling it if it is not.
30///
31/// A failed pull is only fatal when the image is *also* absent locally. anvil's
32/// own runner image is built by `deploy/runner/build.sh` straight into the
33/// host's image store and exists in no registry, so an unconditional pull —
34/// which is what this used to be — fails for the one image most jobs now use.
35pub async fn ensure_image(docker: &Docker, image: &str) -> Result<(), String> {
36 // Split name:tag so we don't accidentally pull every tag. A ':' that has a
37 // '/' after it is a registry port, not a tag.
38 let (from_image, tag) = match image.rsplit_once(':') {
39 Some((name, tag)) if !tag.contains('/') => (name.to_string(), tag.to_string()),
40 _ => (image.to_string(), "latest".to_string()),
41 };
42
43 let mut pull = docker.create_image(
44 Some(CreateImageOptions {
45 from_image,
46 tag,
47 ..Default::default()
48 }),
49 None,
50 None,
51 );
52 let mut pull_error = None;
53 while let Some(item) = pull.next().await {
54 if let Err(e) = item {
55 pull_error = Some(e.to_string());
56 break;
57 }
58 }
59
60 let Some(pull_error) = pull_error else {
61 return Ok(());
62 };
63
64 // The pull failed. That is fine if the image is already here — the local
65 // build case — and fatal otherwise.
66 match docker.inspect_image(image).await {
67 Ok(_) => {
68 tracing::debug!("pull of {image} failed ({pull_error}); using the local image");
69 Ok(())
70 }
71 Err(_) => Err(format!(
72 "image {image} is not available locally and could not be pulled: {pull_error}"
73 )),
74 }
75}