1
0
Fork 0
mirror of https://github.com/imjasonh/krust synced 2026-07-18 15:06:03 +00:00

feat: Add RegistryAuth::from_default() convenience methods

- Add from_default() and from_default_str() methods to RegistryAuth
- These methods automatically resolve auth from Docker config and credential helpers
- Update example to demonstrate both explicit and auto auth approaches
- Simplify krust's auth wrapper to use the new convenience method
- Fix test environment variable pollution

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jason Hall 2025-06-08 10:49:28 -04:00
parent a6ae83c3b2
commit 672ada3dd2
Failed to extract signature
9 changed files with 148 additions and 239 deletions

View file

@ -0,0 +1,88 @@
use oci_distribution::client::{ClientConfig, Client};
use oci_distribution::secrets::RegistryAuth;
use oci_distribution::Reference;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup logging
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("oci_distribution=debug".parse()?)
)
.init();
// Create a client
let config = ClientConfig::default();
let client = Client::new(config);
// Example 1: Using the new from_default() method
println!("Example 1: Using RegistryAuth::from_default()");
let alpine_ref = Reference::try_from("docker.io/library/alpine:latest")?;
let auth = RegistryAuth::from_default(&alpine_ref)?;
match client.pull_manifest(&alpine_ref, &auth).await {
Ok((manifest, digest)) => {
println!("Successfully pulled manifest for alpine:latest");
println!("Digest: {}", digest);
// Handle the manifest enum
match manifest {
oci_distribution::manifest::OciManifest::Image(img) => {
println!("Config digest: {}", img.config.digest);
}
oci_distribution::manifest::OciManifest::ImageIndex(_) => {
println!("Got an image index manifest");
}
}
}
Err(e) => {
println!("Failed to pull alpine:latest: {}", e);
}
}
// Example 2: Using the convenience *_auto methods (same result, less code)
println!("\nExample 2: Using pull_manifest_auto() convenience method");
let busybox_ref = Reference::try_from("docker.io/library/busybox:latest")?;
match client.pull_manifest_auto(&busybox_ref).await {
Ok((manifest, digest)) => {
println!("Successfully pulled manifest for busybox:latest");
println!("Digest: {}", digest);
}
Err(e) => {
println!("Failed to pull busybox:latest: {}", e);
}
}
// Example 3: Get platforms using auto auth
println!("\nExample 3: Getting platforms with automatic auth");
match client.get_image_platforms_auto(&busybox_ref).await {
Ok(platforms) => {
println!("Busybox platforms:");
for (arch, os) in platforms {
println!(" - {}/{}", os, arch);
}
}
Err(e) => {
println!("Failed to get platforms: {}", e);
}
}
// Example 4: Using from_default_str for non-Reference strings
println!("\nExample 4: Using RegistryAuth::from_default_str()");
let auth = RegistryAuth::from_default_str("gcr.io/example/image:tag")?;
println!("Resolved auth for gcr.io: {:?}", auth);
// If you have credentials configured for private registries,
// both approaches will automatically use them:
//
// let private_ref = Reference::try_from("ghcr.io/myuser/myimage:latest")?;
//
// // Option 1: Explicit auth resolution
// let auth = RegistryAuth::from_default(&private_ref)?;
// let result = client.pull(&private_ref, &auth, vec![]).await?;
//
// // Option 2: Using the convenience method
// let result = client.pull_auto(&private_ref).await?;
Ok(())
}

View file

@ -1,47 +0,0 @@
use clap::{Parser, Subcommand};
/// Pull a WebAssembly module from a OCI container registry
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
pub(crate) struct Cli {
/// Enable verbose mode
#[clap(short, long)]
pub verbose: bool,
/// Perform anonymous operation, by default the tool tries to reuse the docker credentials read
/// from the default docker file
#[clap(short, long)]
pub anonymous: bool,
/// Pull image from registry using HTTP instead of HTTPS
#[clap(short, long)]
pub insecure: bool,
#[clap(subcommand)]
pub command: Commands,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Commands {
#[clap(arg_required_else_help = true)]
Pull {
/// Write contents to file
#[clap(short, long)]
output: String,
/// Name of the image to pull
image: String,
},
#[clap(arg_required_else_help = true)]
Push {
/// OCI Annotations to be added to the manifest
#[clap(short, long, required(false))]
annotations: Vec<String>,
/// Wasm file to push
module: String,
/// Name of the image to pull
image: String,
},
}

View file

@ -1,111 +0,0 @@
use oci_distribution::{annotations, secrets::RegistryAuth, Client, Reference};
use docker_credential::{CredentialRetrievalError, DockerCredential};
use std::collections::HashMap;
use tracing::{debug, warn};
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
mod cli;
use clap::Parser;
use cli::Cli;
mod pull;
use pull::pull_wasm;
mod push;
use push::push_wasm;
fn build_auth(reference: &Reference, cli: &Cli) -> RegistryAuth {
let server = reference
.resolve_registry()
.strip_suffix('/')
.unwrap_or_else(|| reference.resolve_registry());
if cli.anonymous {
return RegistryAuth::Anonymous;
}
match docker_credential::get_credential(server) {
Err(CredentialRetrievalError::ConfigNotFound) => RegistryAuth::Anonymous,
Err(CredentialRetrievalError::NoCredentialConfigured) => RegistryAuth::Anonymous,
Err(e) => panic!("Error handling docker configuration file: {}", e),
Ok(DockerCredential::UsernamePassword(username, password)) => {
debug!("Found docker credentials");
RegistryAuth::Basic(username, password)
}
Ok(DockerCredential::IdentityToken(_)) => {
warn!("Cannot use contents of docker config, identity token not supported. Using anonymous auth");
RegistryAuth::Anonymous
}
}
}
fn build_client_config(cli: &Cli) -> oci_distribution::client::ClientConfig {
let protocol = if cli.insecure {
oci_distribution::client::ClientProtocol::Http
} else {
oci_distribution::client::ClientProtocol::Https
};
oci_distribution::client::ClientConfig {
protocol,
..Default::default()
}
}
#[tokio::main]
pub async fn main() {
let cli = Cli::parse();
// setup logging
let level_filter = if cli.verbose { "debug" } else { "info" };
let filter_layer = EnvFilter::new(level_filter);
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt::layer().with_writer(std::io::stderr))
.init();
let client_config = build_client_config(&cli);
let mut client = Client::new(client_config);
match &cli.command {
crate::cli::Commands::Pull { output, image } => {
let reference: Reference = image.parse().expect("Not a valid image reference");
let auth = build_auth(&reference, &cli);
pull_wasm(&mut client, &auth, &reference, output).await;
}
crate::cli::Commands::Push {
module,
image,
annotations,
} => {
let reference: Reference = image.parse().expect("Not a valid image reference");
let auth = build_auth(&reference, &cli);
let annotations = if annotations.is_empty() {
let mut values: HashMap<String, String> = HashMap::new();
values.insert(
annotations::ORG_OPENCONTAINERS_IMAGE_TITLE.to_string(),
module.clone(),
);
Some(values)
} else {
let mut values: HashMap<String, String> = HashMap::new();
for annotation in annotations {
let tmp: Vec<_> = annotation.splitn(2, '=').collect();
if tmp.len() == 2 {
values.insert(String::from(tmp[0]), String::from(tmp[1]));
}
}
values
.entry(annotations::ORG_OPENCONTAINERS_IMAGE_TITLE.to_string())
.or_insert_with(|| module.clone());
Some(values)
};
push_wasm(&mut client, &auth, &reference, module, annotations).await;
}
}
}

View file

@ -1,26 +0,0 @@
use oci_distribution::{manifest, secrets::RegistryAuth, Client, Reference};
use tracing::info;
pub(crate) async fn pull_wasm(
client: &mut Client,
auth: &RegistryAuth,
reference: &Reference,
output: &str,
) {
info!(?reference, ?output, "pulling wasm module");
let image_content = client
.pull(reference, auth, vec![manifest::WASM_LAYER_MEDIA_TYPE])
.await
.expect("Cannot pull Wasm module")
.layers
.into_iter()
.next()
.map(|layer| layer.data)
.expect("No data found");
async_std::fs::write(output, image_content)
.await
.expect("Cannot write to file");
println!("Wasm module successfully written to {}", output);
}

View file

@ -1,44 +0,0 @@
use oci_distribution::{
client::{Config, ImageLayer},
manifest,
secrets::RegistryAuth,
Client, Reference,
};
use std::collections::HashMap;
use tracing::info;
pub(crate) async fn push_wasm(
client: &mut Client,
auth: &RegistryAuth,
reference: &Reference,
module: &str,
annotations: Option<HashMap<String, String>>,
) {
info!(?reference, ?module, "pushing wasm module");
let data = async_std::fs::read(module)
.await
.expect("Cannot read Wasm module from disk");
let layers = vec![ImageLayer::new(
data,
manifest::WASM_LAYER_MEDIA_TYPE.to_string(),
None,
)];
let config = Config {
data: b"{}".to_vec(),
media_type: manifest::WASM_CONFIG_MEDIA_TYPE.to_string(),
annotations: None,
};
let image_manifest = manifest::OciImageManifest::build(&layers, &config, annotations);
let response = client
.push(reference, &layers, config, auth, Some(image_manifest))
.await
.map(|push_response| push_response.manifest_url)
.expect("Cannot push Wasm module");
println!("Wasm module successfully pushed {:?}", response);
}