| 1 | //! Docker plumbing shared by the CI runner and the agent-session supervisor. |
| 2 | //! |
| 3 | //! Both create containers from the same runner image |
| 4 | //! ([`anvil_core::config::DEFAULT_RUNNER_IMAGE`]) against the same daemon, so |
| 5 | //! the connect/pull dance lives here rather than being written twice. |
| 6 | |
| 7 | use bollard::{ |
| 8 | Docker, |
| 9 | image::CreateImageOptions, |
| 10 | }; |
| 11 | use futures_util::StreamExt; |
| 12 | |
| 13 | /// Connect to the daemon over the local socket. |
| 14 | pub fn connect() -> Result<Docker, String> { |
| 15 | Docker::connect_with_socket_defaults() |
| 16 | .map_err(|e| format!("docker unavailable (is the socket mounted?): {e}")) |
| 17 | } |
| 18 | |
| 19 | /// Make sure `image` is present locally, pulling it if it is not. |
| 20 | /// |
| 21 | /// A failed pull is only fatal when the image is *also* absent locally. anvil's |
| 22 | /// own runner image is built by `deploy/runner/build.sh` straight into the |
| 23 | /// host's image store and exists in no registry, so an unconditional pull — |
| 24 | /// which is what this used to be — fails for the one image most jobs now use. |
| 25 | pub async fn ensure_image(docker: &Docker, image: &str) -> Result<(), String> { |
| 26 | // Split name:tag so we don't accidentally pull every tag. A ':' that has a |
| 27 | // '/' after it is a registry port, not a tag. |
| 28 | let (from_image, tag) = match image.rsplit_once(':') { |
| 29 | Some((name, tag)) if !tag.contains('/') => (name.to_string(), tag.to_string()), |
| 30 | _ => (image.to_string(), "latest".to_string()), |
| 31 | }; |
| 32 | |
| 33 | let mut pull = docker.create_image( |
| 34 | Some(CreateImageOptions { |
| 35 | from_image, |
| 36 | tag, |
| 37 | ..Default::default() |
| 38 | }), |
| 39 | None, |
| 40 | None, |
| 41 | ); |
| 42 | let mut pull_error = None; |
| 43 | while let Some(item) = pull.next().await { |
| 44 | if let Err(e) = item { |
| 45 | pull_error = Some(e.to_string()); |
| 46 | break; |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | let Some(pull_error) = pull_error else { |
| 51 | return Ok(()); |
| 52 | }; |
| 53 | |
| 54 | // The pull failed. That is fine if the image is already here — the local |
| 55 | // build case — and fatal otherwise. |
| 56 | match docker.inspect_image(image).await { |
| 57 | Ok(_) => { |
| 58 | tracing::debug!("pull of {image} failed ({pull_error}); using the local image"); |
| 59 | Ok(()) |
| 60 | } |
| 61 | Err(_) => Err(format!( |
| 62 | "image {image} is not available locally and could not be pulled: {pull_error}" |
| 63 | )), |
| 64 | } |
| 65 | } |