From 672ada3dd2965c72cb1970bb8580ccf7e798f790 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Sun, 8 Jun 2025 10:49:28 -0400 Subject: [PATCH] feat: Add RegistryAuth::from_default() convenience methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/auth/simple.rs | 3 +- tests/credential_helper_integration_test.rs | 17 ++- .../examples/auto_auth_demo.rs | 88 ++++++++++++++ vendor/oci-distribution/examples/wasm/cli.rs | 47 -------- vendor/oci-distribution/examples/wasm/main.rs | 111 ------------------ vendor/oci-distribution/examples/wasm/pull.rs | 26 ---- vendor/oci-distribution/examples/wasm/push.rs | 44 ------- .../src/credential_helper_tests.rs | 26 +++- vendor/oci-distribution/src/secrets.rs | 25 ++++ 9 files changed, 148 insertions(+), 239 deletions(-) create mode 100644 vendor/oci-distribution/examples/auto_auth_demo.rs delete mode 100644 vendor/oci-distribution/examples/wasm/cli.rs delete mode 100644 vendor/oci-distribution/examples/wasm/main.rs delete mode 100644 vendor/oci-distribution/examples/wasm/pull.rs delete mode 100644 vendor/oci-distribution/examples/wasm/push.rs diff --git a/src/auth/simple.rs b/src/auth/simple.rs index c10c230..84acfb9 100644 --- a/src/auth/simple.rs +++ b/src/auth/simple.rs @@ -1,11 +1,10 @@ //! Simple authentication wrapper using oci-distribution's credential helper use anyhow::Result; -use oci_distribution::credential_helper::resolve_docker_auth; use oci_distribution::secrets::RegistryAuth; /// Resolve authentication for a given resource using Docker config and credential helpers pub fn resolve_auth(resource: &str) -> Result { - resolve_docker_auth(resource) + RegistryAuth::from_default_str(resource) .map_err(|e| anyhow::anyhow!("Failed to resolve auth: {}", e)) } diff --git a/tests/credential_helper_integration_test.rs b/tests/credential_helper_integration_test.rs index 04d8103..cfec739 100644 --- a/tests/credential_helper_integration_test.rs +++ b/tests/credential_helper_integration_test.rs @@ -91,21 +91,30 @@ fn test_resolve_auth_bearer_token() -> Result<()> { fs::write(&config_path, config)?; - // Save current env var - let old_val = env::var("DOCKER_CONFIG").ok(); + // Save current env vars + let old_docker_config = env::var("DOCKER_CONFIG").ok(); + let old_registry_auth = env::var("REGISTRY_AUTH_FILE").ok(); + + // Set our test config and clear other env vars env::set_var("DOCKER_CONFIG", tmp_dir.path()); + env::remove_var("REGISTRY_AUTH_FILE"); // Should resolve to bearer auth let auth = resolve_auth("ghcr.io/user/image")?; assert!(matches!(auth, RegistryAuth::Bearer(token) if token == "test-bearer-token")); - // Restore env var - if let Some(val) = old_val { + // Restore env vars + if let Some(val) = old_docker_config { env::set_var("DOCKER_CONFIG", val); } else { env::remove_var("DOCKER_CONFIG"); } + if let Some(val) = old_registry_auth { + env::set_var("REGISTRY_AUTH_FILE", val); + } else { + env::remove_var("REGISTRY_AUTH_FILE"); + } Ok(()) } diff --git a/vendor/oci-distribution/examples/auto_auth_demo.rs b/vendor/oci-distribution/examples/auto_auth_demo.rs new file mode 100644 index 0000000..91f54b9 --- /dev/null +++ b/vendor/oci-distribution/examples/auto_auth_demo.rs @@ -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> { + // 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(()) +} diff --git a/vendor/oci-distribution/examples/wasm/cli.rs b/vendor/oci-distribution/examples/wasm/cli.rs deleted file mode 100644 index f0d4946..0000000 --- a/vendor/oci-distribution/examples/wasm/cli.rs +++ /dev/null @@ -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, - - /// Wasm file to push - module: String, - - /// Name of the image to pull - image: String, - }, -} diff --git a/vendor/oci-distribution/examples/wasm/main.rs b/vendor/oci-distribution/examples/wasm/main.rs deleted file mode 100644 index 5cad960..0000000 --- a/vendor/oci-distribution/examples/wasm/main.rs +++ /dev/null @@ -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 = HashMap::new(); - values.insert( - annotations::ORG_OPENCONTAINERS_IMAGE_TITLE.to_string(), - module.clone(), - ); - Some(values) - } else { - let mut values: HashMap = 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; - } - } -} diff --git a/vendor/oci-distribution/examples/wasm/pull.rs b/vendor/oci-distribution/examples/wasm/pull.rs deleted file mode 100644 index 78b6c76..0000000 --- a/vendor/oci-distribution/examples/wasm/pull.rs +++ /dev/null @@ -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); -} diff --git a/vendor/oci-distribution/examples/wasm/push.rs b/vendor/oci-distribution/examples/wasm/push.rs deleted file mode 100644 index ae38e81..0000000 --- a/vendor/oci-distribution/examples/wasm/push.rs +++ /dev/null @@ -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>, -) { - 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); -} diff --git a/vendor/oci-distribution/src/credential_helper_tests.rs b/vendor/oci-distribution/src/credential_helper_tests.rs index 74d141f..85b509a 100644 --- a/vendor/oci-distribution/src/credential_helper_tests.rs +++ b/vendor/oci-distribution/src/credential_helper_tests.rs @@ -191,9 +191,11 @@ mod tests { // Save current env vars let old_config = env::var("DOCKER_CONFIG").ok(); let old_path = env::var("PATH").ok(); + let old_registry_auth = env::var("REGISTRY_AUTH_FILE").ok(); - // Set our temp dir in PATH and DOCKER_CONFIG + // Set our temp dir in PATH and DOCKER_CONFIG, clear REGISTRY_AUTH_FILE env::set_var("DOCKER_CONFIG", tmp_dir.path()); + env::remove_var("REGISTRY_AUTH_FILE"); let new_path = format!("{}:{}", tmp_dir.path().display(), env::var("PATH").unwrap_or_default()); env::set_var("PATH", new_path); @@ -212,6 +214,11 @@ mod tests { } else { env::remove_var("PATH"); } + if let Some(val) = old_registry_auth { + env::set_var("REGISTRY_AUTH_FILE", val); + } else { + env::remove_var("REGISTRY_AUTH_FILE"); + } } #[test] @@ -278,18 +285,27 @@ mod tests { fs::write(&auth_file, config).unwrap(); - // Save current env var - let old_val = env::var("REGISTRY_AUTH_FILE").ok(); + // Save current env vars + let old_registry_auth = env::var("REGISTRY_AUTH_FILE").ok(); + let old_docker_config = env::var("DOCKER_CONFIG").ok(); + + // Set REGISTRY_AUTH_FILE and clear DOCKER_CONFIG to avoid conflicts env::set_var("REGISTRY_AUTH_FILE", auth_file.to_str().unwrap()); + env::remove_var("DOCKER_CONFIG"); let auth = resolve_docker_auth("special.registry.io/image").unwrap(); assert_eq!(auth, RegistryAuth::Basic("authfile-user".to_string(), "authfile-pass".to_string())); - // Restore env var - if let Some(val) = old_val { + // Restore env vars + if let Some(val) = old_registry_auth { env::set_var("REGISTRY_AUTH_FILE", val); } else { env::remove_var("REGISTRY_AUTH_FILE"); } + if let Some(val) = old_docker_config { + env::set_var("DOCKER_CONFIG", val); + } else { + env::remove_var("DOCKER_CONFIG"); + } } } diff --git a/vendor/oci-distribution/src/secrets.rs b/vendor/oci-distribution/src/secrets.rs index ab9fdb5..18f1b4a 100644 --- a/vendor/oci-distribution/src/secrets.rs +++ b/vendor/oci-distribution/src/secrets.rs @@ -1,5 +1,8 @@ //! Types for working with registry access secrets +use crate::errors::Result; +use crate::Reference; + /// A method for authenticating to a registry #[derive(Eq, PartialEq, Debug, Clone)] pub enum RegistryAuth { @@ -11,6 +14,28 @@ pub enum RegistryAuth { Bearer(String), } +impl RegistryAuth { + /// Create a RegistryAuth by resolving from Docker config files and credential helpers + /// + /// This will check: + /// 1. Docker config files (DOCKER_CONFIG, REGISTRY_AUTH_FILE, ~/.docker/config.json) + /// 2. Credential helpers specified in the config + /// 3. Default credential store + /// + /// If no credentials are found, returns Anonymous auth. + pub fn from_default(image: &Reference) -> Result { + let image_str = image.whole(); + crate::credential_helper::resolve_docker_auth(&image_str) + } + + /// Create a RegistryAuth by resolving from Docker config files and credential helpers + /// + /// This is similar to `from_default` but takes a string reference instead of a Reference. + pub fn from_default_str(image_ref: &str) -> Result { + crate::credential_helper::resolve_docker_auth(image_ref) + } +} + pub(crate) trait Authenticable { fn apply_authentication(self, auth: &RegistryAuth) -> Self; }