use isoloom_spec::model::{Constraints, Spec};
#[must_use]
pub fn rewrite_image(c: &Constraints, image: &str) -> String {
if c.registries.is_empty() {
return image.to_owned();
}
let (first, rest) = match image.split_once('/') {
Some((f, r)) if f.contains('.') || f.contains(':') || f == "localhost" => (f, r.to_owned()),
_ => ("docker.io", if image.contains('/') { image.to_owned() } else { format!("library/{image}") }),
};
let registry = match first {
"index.docker.io" | "registry-1.docker.io" => "docker.io",
r => r,
};
match c.registries.get(registry) {
Some(mirror) => format!("{}/{rest}", mirror.trim_end_matches('/')),
None => image.to_owned(),
}
}
pub(crate) fn tag_of(spec: &Spec, name: &str, image: Option<&str>) -> String {
image.map_or_else(|| format!("isoloom/{}-{name}", spec.name), str::to_owned)
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Images {
constraints: Option<Constraints>,
built: Vec<String>,
}
impl Images {
#[must_use]
pub fn of(spec: &Spec) -> Images {
let built = spec
.machines
.iter()
.filter_map(|(name, m)| {
let d = m.docker.as_ref()?;
(d.build.is_some() || d.generated.is_some()).then(|| tag_of(spec, name, d.image.as_deref()))
})
.collect();
let constraints = spec.constraints.clone().filter(|c| !c.registries.is_empty());
Images { constraints, built }
}
#[must_use]
pub fn pull(&self, image: &str) -> String {
match &self.constraints {
Some(c) if !self.built.iter().any(|b| b == image) => rewrite_image(c, image),
_ => image.to_owned(),
}
}
}
#[cfg(test)]
mod tests {
use indexmap::IndexMap;
use isoloom_spec::model::Constraints;
use proptest::prelude::*;
use super::rewrite_image;
fn mirrors(registries: &[(&str, &str)]) -> Constraints {
Constraints {
registries: registries.iter().map(|(r, m)| ((*r).to_owned(), (*m).to_owned())).collect::<IndexMap<_, _>>(),
..Constraints::default()
}
}
fn hub_image() -> impl Strategy<Value = String> {
("([a-z0-9]{1,8}/)?[a-z0-9]{1,8}", "(:[a-z0-9.]{1,6})?").prop_map(|(n, t)| format!("{n}{t}"))
}
proptest! {
#[test]
fn without_mirrors_an_image_is_pulled_as_written(image in "[ -~]{0,30}") {
prop_assert_eq!(rewrite_image(&Constraints::default(), &image), image);
}
#[test]
fn a_docker_hub_image_goes_through_its_mirror_under_library(image in hub_image()) {
let rewritten = rewrite_image(&mirrors(&[("docker.io", "registry.corp/hub/")]), &image);
let official = if image.contains('/') { image.clone() } else { format!("library/{image}") };
prop_assert_eq!(rewritten, format!("registry.corp/hub/{official}"));
}
#[test]
fn an_image_of_another_registry_goes_through_its_own_mirror_only(path in "[a-z0-9]{1,8}(/[a-z0-9]{1,8})?(:[0-9]{1,3})?") {
let image = format!("ghcr.io/{path}");
prop_assert_eq!(rewrite_image(&mirrors(&[("ghcr.io", "mirror.corp/ghcr")]), &image), format!("mirror.corp/ghcr/{path}"));
prop_assert_eq!(rewrite_image(&mirrors(&[("docker.io", "mirror.corp/hub")]), &image), image);
}
}
#[test]
fn a_built_image_is_tagged_locally_and_never_pulled_through_a_mirror() {
let spec = crate::testing::spec(
"version: 1\nname: lab\nmachines:\n web: { app: { image: nginx } }\n own: { app: { build: web } }\n gen: { os: debian@12, app: { steps: [{ packages: [curl] }] } }\nconstraints: { registries: { docker.io: m.corp/hub } }\n",
);
let images = super::Images::of(&spec);
assert_eq!(images.pull("nginx"), "m.corp/hub/library/nginx");
assert_eq!(images.pull("isoloom/lab-own"), "isoloom/lab-own");
assert_eq!(images.pull("isoloom/lab-gen"), "isoloom/lab-gen");
assert_eq!(images.pull("isoloom/lab-web"), "m.corp/hub/isoloom/lab-web", "not built: pulled like any other");
assert_eq!(super::tag_of(&spec, "own", None), "isoloom/lab-own");
assert_eq!(super::tag_of(&spec, "web", Some("nginx")), "nginx");
let none = crate::testing::spec("version: 1\nname: lab\nmachines:\n web: { app: { image: nginx } }\n");
assert_eq!(super::Images::of(&none).pull("nginx"), "nginx");
}
#[test]
fn docker_hubs_other_names_and_a_registry_with_a_port_are_registries() {
let c = mirrors(&[("docker.io", "m/hub"), ("localhost:5000", "m/local")]);
assert_eq!(rewrite_image(&c, "index.docker.io/library/redis"), "m/hub/library/redis");
assert_eq!(rewrite_image(&c, "registry-1.docker.io/x/y"), "m/hub/x/y");
assert_eq!(rewrite_image(&c, "localhost:5000/app"), "m/local/app");
assert_eq!(rewrite_image(&c, "localhost/app"), "localhost/app");
}
}