#!/usr/bin/env -S cargo +nightly -Zscript --quiet --- [package] edition = "2024" [dependencies] oci-client = { version = "0.14.0" } tokio = { version = "1", features = ["rt", "net", "macros", "fs", "process"] } serde = { version = "1.0" } serde_json = { version = "1.0" } base64 = { version = "0.22" } --- #![feature(let_chains)] type Error = Box; mod container_auth { use std::collections::HashMap; use std::path::{Path, PathBuf}; use oci_client::{ Reference, secrets::RegistryAuth, }; use base64::prelude::{Engine as _, BASE64_STANDARD as base64}; use crate::Error; // lean and mean c machine unsafe extern "C" { fn geteuid() -> u32; } #[derive(serde::Deserialize, Clone)] pub struct AuthFile { auths: HashMap, } #[derive(serde::Deserialize, Clone)] struct AuthFileAuth { auth: String, } impl AuthFile { pub async fn load_podman() -> Result { let path: PathBuf = [ "/run", "user", unsafe { geteuid() }.to_string().as_ref(), "containers", "auth.json", ] .iter() .collect(); let auth = Self::load_file(&path).await; auth } pub async fn load_file(path: &Path) -> Result { let auths = tokio::fs::read(path).await?; let auths: Self = serde_json::from_slice(&auths)?; Ok(auths) } pub fn for_image(&self, image: &Reference) -> Result { match self.auths.get(image.registry()) { Some(auth) => { let (user, password) = auth.credentials()?; Ok(RegistryAuth::Basic(user, password)) }, None => Ok(RegistryAuth::Anonymous), } } } impl AuthFileAuth { fn credentials(&self) -> Result<(String, String), Error> { let creds = base64.decode(&self.auth)?; let creds = String::from_utf8(creds)?; let (user, password) = creds.split_once(":").ok_or("No ':' to split username and password")?; Ok((user.to_owned(), password.to_owned())) } } } use crate::container_auth::AuthFile; mod makefile_images { use std::io::Write; use std::collections::HashMap; use oci_client::Reference; use crate::Error; pub struct Images { images: Vec } impl Images { pub async fn load() -> Result { let make = tokio::process::Command::new("make") .arg("--dry-run") .arg("--print-data-base") .output() .await?; if !make.status.success() { std::io::stderr().write_all(&make.stderr).unwrap(); return Err(format!("make failed to print database: {}", make.status.code().unwrap_or(257i32)).into()) } let make = make.stdout; let make = String::from_utf8(make)?; let make = make.lines() .filter(|l| !l.starts_with("#")) // comments (very common) .filter(|l| l.contains(" := ")) // assignments .filter(|l| !l.contains(": ")); // rule specific assignments let make_vars: HashMap<&str, &str> = make.map(|l| l.split_once(" := ").unwrap()).collect(); let registry = make_vars.get("REGISTRY").ok_or("REGISTRY not defined in Makefile")?.trim_end_matches("/"); let images = make_vars.get("IMAGES").ok_or("IMAGES not defined in Makefile")?; let mut references = vec![]; for image in images.split(" ") { let Some(version) = make_vars.get(format!("{image}_version").as_str()) else { eprintln!("No version defined. Skipping: {image}"); continue; }; references.push(Reference::with_tag(registry.to_string(), image.to_string(), version.trim().to_string())); } Ok(Self{images: references}) } } impl IntoIterator for Images { type Item = Reference; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.images.into_iter() } } } use crate::makefile_images::Images; use oci_client::{ Reference, client::{Client, ClientConfig}, }; async fn query_needs_build(image: &Reference, client: &Client, auths: &AuthFile) -> bool { let Some(want) = image.tag().map(|s| s.replace(&[':', '+'], "_")) else { eprintln!("{image}: no version specified. Skipping"); return false; }; let auth = auths.for_image(image).expect("error loading credentials"); let tags = match client.list_tags(image, &auth, None, None).await { Ok(tags) => tags.tags, Err(oci_client::errors::OciDistributionError::ImageManifestNotFoundError(_)) => vec![], Err(e) => { eprintln!("{image}: error fetching tags. Skipping: {e}"); return false; }, }; if tags.contains(&want) { eprintln!( "✅ {}/\x1b[1m{}\x1b[0m: has {want}", image.registry(), image.repository(), ); false } else { eprintln!( "❌ {}/\x1b[1m{}\x1b[0m: want {want}, have {}", image.registry(), image.repository(), tags.join(", ") ); true } } #[tokio::main(flavor = "current_thread")] async fn main() { let reg = Client::new(ClientConfig::default()); let auths = AuthFile::load_podman() .await .expect("Can't load podman's login information"); let images = Images::load() .await .expect("Can't load image versions from Makefile"); let mut tasks = tokio::task::JoinSet::new(); for image in images.into_iter() { let reg = reg.clone(); let auths = auths.clone(); let repo = image.repository().to_owned(); tasks.spawn(async move { query_needs_build(&image, ®, &auths) .await .then_some(repo) }); } let need_build: Vec<_> = tasks.join_all().await .into_iter() .filter_map(|i| i) .collect(); if need_build.len() != 0 { eprintln!("\n{} images need rebuilding:", need_build.len()); let command = format!("make {{build,push}}-{{{}}}", need_build.join(",")); if std::env::args().find(|s| s == "--dry-run").is_some() { println!("{command}"); } else { let make = tokio::process::Command::new("sh") .arg("-c") .arg(command) .spawn() .expect("can't exec make") .wait() .await .expect("failed to exec make"); if !make.success() { std::process::exit(3); } } } }