Created
March 17, 2025 17:04
-
-
Save M3t0r/70cc4b2e36c2c95dc0b53815b95a7578 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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<dyn std::error::Error>; | |
| 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<String, AuthFileAuth>, | |
| } | |
| #[derive(serde::Deserialize, Clone)] | |
| struct AuthFileAuth { | |
| auth: String, | |
| } | |
| impl AuthFile { | |
| pub async fn load_podman() -> Result<Self, Error> { | |
| 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<Self, Error> { | |
| 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<RegistryAuth, Error> { | |
| 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<Reference> } | |
| impl Images { | |
| pub async fn load() -> Result<Self, Error> { | |
| 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<Self::Item>; | |
| 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); | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment