Skip to content

Instantly share code, notes, and snippets.

@M3t0r
Created March 17, 2025 17:04
Show Gist options
  • Select an option

  • Save M3t0r/70cc4b2e36c2c95dc0b53815b95a7578 to your computer and use it in GitHub Desktop.

Select an option

Save M3t0r/70cc4b2e36c2c95dc0b53815b95a7578 to your computer and use it in GitHub Desktop.
#!/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, &reg, &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