1
0
Fork 0
mirror of https://github.com/imjasonh/krust synced 2026-07-08 14:55:35 +00:00

Merge pull request #11 from imjasonh/feat/registry-auth

feat: Implement registry authentication support
This commit is contained in:
Jason Hall 2025-06-08 09:26:23 -04:00 committed by GitHub
commit e2c4b6b14c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 898 additions and 12 deletions

View file

@ -26,6 +26,7 @@ dirs = "6.0"
toml = "0.8"
which = "8.0"
tempfile = "3.9"
base64 = "0.22"
[dev-dependencies]
tempfile = "3.9"

364
src/auth/keychain.rs Normal file
View file

@ -0,0 +1,364 @@
//! Keychain implementation for credential management
use super::{Anonymous, AuthConfig, Authenticator, DockerAuthEntry, DockerConfig};
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tracing::{debug, warn};
/// Trait for types that can resolve authentication for a given resource
pub trait Keychain: Send + Sync {
/// Resolve authentication for a given resource (registry URL or image reference)
fn resolve(&self, resource: &str) -> Result<Box<dyn Authenticator>>;
}
/// Default keychain implementation that checks Docker config files
pub struct DefaultKeychain {
/// Cached config to avoid re-reading files
config_cache: Arc<Mutex<Option<DockerConfig>>>,
}
impl DefaultKeychain {
/// Create a new DefaultKeychain
pub fn new() -> Self {
Self {
config_cache: Arc::new(Mutex::new(None)),
}
}
/// Get paths to check for Docker config
fn config_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
// Check DOCKER_CONFIG environment variable
if let Ok(docker_config) = std::env::var("DOCKER_CONFIG") {
paths.push(PathBuf::from(docker_config).join("config.json"));
}
// Check REGISTRY_AUTH_FILE environment variable
if let Ok(auth_file) = std::env::var("REGISTRY_AUTH_FILE") {
paths.push(PathBuf::from(auth_file));
}
// Check XDG_RUNTIME_DIR for containers auth
if let Ok(xdg_runtime) = std::env::var("XDG_RUNTIME_DIR") {
paths.push(PathBuf::from(xdg_runtime).join("containers/auth.json"));
}
// Check default Docker config location
if let Some(home) = dirs::home_dir() {
paths.push(home.join(".docker/config.json"));
}
paths
}
/// Load Docker config from disk
fn load_config(&self) -> Result<DockerConfig> {
// Check cache first
{
let cache = self.config_cache.lock().unwrap();
if let Some(config) = cache.as_ref() {
return Ok(config.clone());
}
}
// Try each config path
for path in Self::config_paths() {
if path.exists() {
debug!("Checking Docker config at: {}", path.display());
match std::fs::read_to_string(&path) {
Ok(content) => {
match serde_json::from_str::<DockerConfig>(&content) {
Ok(config) => {
debug!("Loaded Docker config from: {}", path.display());
// Cache the config
let mut cache = self.config_cache.lock().unwrap();
*cache = Some(config.clone());
return Ok(config);
}
Err(e) => {
warn!("Failed to parse Docker config at {}: {}", path.display(), e);
}
}
}
Err(e) => {
warn!("Failed to read Docker config at {}: {}", path.display(), e);
}
}
}
}
// Return empty config if no valid config found
Ok(DockerConfig {
auths: HashMap::new(),
cred_helpers: HashMap::new(),
creds_store: None,
})
}
/// Extract registry from image reference
fn extract_registry(image_ref: &str) -> &str {
// Handle different image reference formats:
// - docker.io/library/ubuntu:latest -> docker.io
// - gcr.io/project/image:tag -> gcr.io
// - localhost:5000/image -> localhost:5000
// - ubuntu:latest -> docker.io (implicit)
if let Some(slash_pos) = image_ref.find('/') {
let registry_part = &image_ref[..slash_pos];
// Check if this looks like a registry (contains . or :)
if registry_part.contains('.') || registry_part.contains(':') {
return registry_part;
}
}
// Default to Docker Hub
"index.docker.io"
}
/// Normalize registry URL for matching
fn normalize_registry(registry: &str) -> Vec<String> {
let mut variants = vec![registry.to_string()];
// Add common variants
if registry == "docker.io" || registry == "index.docker.io" {
variants.push("docker.io".to_string());
variants.push("index.docker.io".to_string());
variants.push("https://index.docker.io/v1/".to_string());
variants.push("https://index.docker.io/v2/".to_string());
} else if !registry.starts_with("http://") && !registry.starts_with("https://") {
// Add protocol variants
variants.push(format!("https://{}", registry));
variants.push(format!("http://{}", registry));
// Add /v1/ and /v2/ variants
variants.push(format!("https://{}/v1/", registry));
variants.push(format!("https://{}/v2/", registry));
}
variants
}
/// Find auth entry for a registry
fn find_auth_entry(&self, config: &DockerConfig, registry: &str) -> Option<DockerAuthEntry> {
let variants = Self::normalize_registry(registry);
for variant in variants {
if let Some(entry) = config.auths.get(&variant) {
return Some(entry.clone());
}
}
None
}
/// Get credential helper for a registry
fn get_credential_helper(&self, config: &DockerConfig, registry: &str) -> Option<String> {
// Check specific credential helper for registry
if let Some(helper) = config.cred_helpers.get(registry) {
return Some(helper.clone());
}
// Check default credential store
config.creds_store.clone()
}
/// Execute credential helper to get credentials
fn execute_credential_helper(&self, helper: &str, registry: &str) -> Result<AuthConfig> {
use std::io::Write;
use std::process::{Command, Stdio};
let helper_name = format!("docker-credential-{}", helper);
debug!(
"Executing credential helper: {} for {}",
helper_name, registry
);
let mut child = Command::new(&helper_name)
.arg("get")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context(format!(
"Failed to spawn credential helper: {}",
helper_name
))?;
// Write registry URL to stdin
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(registry.as_bytes())?;
stdin.write_all(b"\n")?;
}
let output = child.wait_with_output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Credential helper {} failed: {}", helper_name, stderr);
}
// Parse output as JSON
#[derive(serde::Deserialize)]
struct HelperResponse {
#[serde(rename = "Username")]
username: Option<String>,
#[serde(rename = "Secret")]
secret: Option<String>,
#[serde(rename = "ServerURL")]
_server_url: Option<String>,
}
let response: HelperResponse = serde_json::from_slice(&output.stdout)
.context("Failed to parse credential helper response")?;
Ok(AuthConfig {
username: response.username,
password: response.secret,
..Default::default()
})
}
}
impl Default for DefaultKeychain {
fn default() -> Self {
Self::new()
}
}
impl Keychain for DefaultKeychain {
fn resolve(&self, resource: &str) -> Result<Box<dyn Authenticator>> {
let config = self.load_config()?;
let registry = Self::extract_registry(resource);
debug!(
"Resolving auth for resource: {} (registry: {})",
resource, registry
);
// Try to find auth entry in config
if let Some(auth_entry) = self.find_auth_entry(&config, registry) {
debug!("Found auth entry for {}", registry);
let auth_config = auth_entry.to_auth_config();
// Return appropriate authenticator based on auth type
if auth_config.is_anonymous() {
return Ok(Box::new(Anonymous));
}
return Ok(Box::new(ConfigAuthenticator {
config: auth_config,
}));
}
// Try credential helper
if let Some(helper) = self.get_credential_helper(&config, registry) {
debug!("Trying credential helper: {} for {}", helper, registry);
match self.execute_credential_helper(&helper, registry) {
Ok(auth_config) => {
return Ok(Box::new(ConfigAuthenticator {
config: auth_config,
}));
}
Err(e) => {
warn!("Credential helper failed: {}", e);
}
}
}
// Default to anonymous
debug!("No credentials found for {}, using anonymous", registry);
Ok(Box::new(Anonymous))
}
}
/// Authenticator that returns a fixed AuthConfig
struct ConfigAuthenticator {
config: AuthConfig,
}
impl Authenticator for ConfigAuthenticator {
fn authorization(&self) -> Result<AuthConfig> {
Ok(self.config.clone())
}
}
/// Multi-keychain that tries multiple keychains in order
pub struct MultiKeychain {
keychains: Vec<Box<dyn Keychain>>,
}
impl MultiKeychain {
/// Create a new MultiKeychain
#[allow(dead_code)]
pub fn new(keychains: Vec<Box<dyn Keychain>>) -> Self {
Self { keychains }
}
}
impl Keychain for MultiKeychain {
fn resolve(&self, resource: &str) -> Result<Box<dyn Authenticator>> {
for keychain in &self.keychains {
match keychain.resolve(resource) {
Ok(auth) => {
// Check if it's not anonymous
if let Ok(config) = auth.authorization() {
if !config.is_anonymous() {
return Ok(auth);
}
}
}
Err(e) => {
debug!("Keychain failed: {}", e);
}
}
}
// Default to anonymous
Ok(Box::new(Anonymous))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_registry() {
assert_eq!(
DefaultKeychain::extract_registry("docker.io/library/ubuntu:latest"),
"docker.io"
);
assert_eq!(
DefaultKeychain::extract_registry("gcr.io/project/image:tag"),
"gcr.io"
);
assert_eq!(
DefaultKeychain::extract_registry("localhost:5000/image"),
"localhost:5000"
);
assert_eq!(
DefaultKeychain::extract_registry("ubuntu:latest"),
"index.docker.io"
);
assert_eq!(
DefaultKeychain::extract_registry("user/image:tag"),
"index.docker.io"
);
}
#[test]
fn test_normalize_registry() {
let variants = DefaultKeychain::normalize_registry("docker.io");
assert!(variants.contains(&"docker.io".to_string()));
assert!(variants.contains(&"index.docker.io".to_string()));
let variants = DefaultKeychain::normalize_registry("gcr.io");
assert!(variants.contains(&"gcr.io".to_string()));
assert!(variants.contains(&"https://gcr.io".to_string()));
}
}

237
src/auth/mod.rs Normal file
View file

@ -0,0 +1,237 @@
//! Authentication module for container registries
//!
//! This module provides authentication functionality similar to go-containerregistry's authn package,
//! supporting Docker config files, credential helpers, and various authentication methods.
use anyhow::Result;
use base64::Engine;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
mod keychain;
pub use keychain::{DefaultKeychain, Keychain};
/// Authentication configuration containing credentials
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registry_token: Option<String>,
}
impl AuthConfig {
/// Create a new AuthConfig with username and password
pub fn new(username: String, password: String) -> Self {
Self {
username: Some(username),
password: Some(password),
..Default::default()
}
}
/// Create an anonymous AuthConfig
pub fn anonymous() -> Self {
Self::default()
}
/// Check if this is anonymous authentication
pub fn is_anonymous(&self) -> bool {
self.username.is_none()
&& self.password.is_none()
&& self.auth.is_none()
&& self.identity_token.is_none()
&& self.registry_token.is_none()
}
/// Convert to authorization header value
pub fn to_authorization_header(&self) -> Result<Option<String>> {
if let Some(token) = &self.registry_token {
return Ok(Some(format!("Bearer {}", token)));
}
if let Some(token) = &self.identity_token {
return Ok(Some(format!("Bearer {}", token)));
}
if let Some(auth) = &self.auth {
return Ok(Some(format!("Basic {}", auth)));
}
if let (Some(username), Some(password)) = (&self.username, &self.password) {
let encoded = base64::engine::general_purpose::STANDARD
.encode(format!("{}:{}", username, password));
return Ok(Some(format!("Basic {}", encoded)));
}
Ok(None)
}
/// Convert to oci-distribution RegistryAuth
pub fn to_registry_auth(&self) -> oci_distribution::secrets::RegistryAuth {
use oci_distribution::secrets::RegistryAuth;
if self.is_anonymous() {
return RegistryAuth::Anonymous;
}
if let (Some(username), Some(password)) = (&self.username, &self.password) {
return RegistryAuth::Basic(username.clone(), password.clone());
}
if let Some(auth) = &self.auth {
// Try to decode the base64 auth string
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(auth) {
if let Ok(decoded_str) = String::from_utf8(decoded) {
if let Some((user, pass)) = decoded_str.split_once(':') {
return RegistryAuth::Basic(user.to_string(), pass.to_string());
}
}
}
}
RegistryAuth::Anonymous
}
}
/// Trait for types that can provide authentication
pub trait Authenticator: Send + Sync {
/// Get the authentication configuration
fn authorization(&self) -> Result<AuthConfig>;
}
/// Anonymous authenticator
pub struct Anonymous;
impl Authenticator for Anonymous {
fn authorization(&self) -> Result<AuthConfig> {
Ok(AuthConfig::anonymous())
}
}
/// Basic authenticator with username and password
pub struct Basic {
username: String,
password: String,
}
impl Basic {
pub fn new(username: String, password: String) -> Self {
Self { username, password }
}
}
impl Authenticator for Basic {
fn authorization(&self) -> Result<AuthConfig> {
Ok(AuthConfig::new(
self.username.clone(),
self.password.clone(),
))
}
}
/// Bearer token authenticator
pub struct Bearer {
token: String,
}
impl Bearer {
pub fn new(token: String) -> Self {
Self { token }
}
}
impl Authenticator for Bearer {
fn authorization(&self) -> Result<AuthConfig> {
Ok(AuthConfig {
registry_token: Some(self.token.clone()),
..Default::default()
})
}
}
/// Docker config file structure
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DockerConfig {
#[serde(default)]
pub auths: HashMap<String, DockerAuthEntry>,
#[serde(rename = "credHelpers", default)]
pub cred_helpers: HashMap<String, String>,
#[serde(rename = "credsStore", skip_serializing_if = "Option::is_none")]
pub creds_store: Option<String>,
}
/// Entry in the Docker config auths section
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DockerAuthEntry {
#[serde(skip_serializing_if = "Option::is_none")]
pub auth: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
#[serde(rename = "identitytoken", skip_serializing_if = "Option::is_none")]
pub identity_token: Option<String>,
#[serde(rename = "registrytoken", skip_serializing_if = "Option::is_none")]
pub registry_token: Option<String>,
}
impl DockerAuthEntry {
/// Convert to AuthConfig
pub fn to_auth_config(&self) -> AuthConfig {
AuthConfig {
username: self.username.clone(),
password: self.password.clone(),
auth: self.auth.clone(),
identity_token: self.identity_token.clone(),
registry_token: self.registry_token.clone(),
}
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn test_auth_config_anonymous() {
let auth = AuthConfig::anonymous();
assert!(auth.is_anonymous());
assert_eq!(auth.to_authorization_header().unwrap(), None);
}
#[test]
fn test_auth_config_basic() {
let auth = AuthConfig::new("user".to_string(), "pass".to_string());
assert!(!auth.is_anonymous());
let header = auth.to_authorization_header().unwrap().unwrap();
assert!(header.starts_with("Basic "));
// Verify base64 encoding
let expected = base64::engine::general_purpose::STANDARD.encode("user:pass");
assert_eq!(header, format!("Basic {}", expected));
}
#[test]
fn test_auth_config_bearer() {
let auth = AuthConfig {
registry_token: Some("token123".to_string()),
..Default::default()
};
let header = auth.to_authorization_header().unwrap().unwrap();
assert_eq!(header, "Bearer token123");
}
}

169
src/auth/tests.rs Normal file
View file

@ -0,0 +1,169 @@
//! Tests for the auth module
use super::*;
use crate::auth::keychain::MultiKeychain;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
/// Helper to create a test Docker config file
fn create_test_config(dir: &Path, content: &str) -> PathBuf {
let config_path = dir.join("config.json");
fs::write(&config_path, content).unwrap();
config_path
}
#[test]
fn test_docker_config_parsing() {
let config_json = r#"{
"auths": {
"docker.io": {
"auth": "dXNlcjpwYXNz"
},
"gcr.io": {
"username": "oauth2accesstoken",
"password": "ya29.token",
"registrytoken": "bearer-token"
}
},
"credHelpers": {
"ecr.amazonaws.com": "ecr-login"
},
"credsStore": "osxkeychain"
}"#;
let config: DockerConfig = serde_json::from_str(config_json).unwrap();
assert_eq!(config.auths.len(), 2);
assert!(config.auths.contains_key("docker.io"));
assert!(config.auths.contains_key("gcr.io"));
let docker_auth = &config.auths["docker.io"];
assert_eq!(docker_auth.auth, Some("dXNlcjpwYXNz".to_string()));
let gcr_auth = &config.auths["gcr.io"];
assert_eq!(gcr_auth.username, Some("oauth2accesstoken".to_string()));
assert_eq!(gcr_auth.password, Some("ya29.token".to_string()));
assert_eq!(gcr_auth.registry_token, Some("bearer-token".to_string()));
assert_eq!(config.cred_helpers.len(), 1);
assert_eq!(config.cred_helpers["ecr.amazonaws.com"], "ecr-login");
assert_eq!(config.creds_store, Some("osxkeychain".to_string()));
}
#[test]
fn test_docker_auth_entry_to_auth_config() {
let entry = DockerAuthEntry {
auth: Some("dXNlcjpwYXNz".to_string()),
username: None,
password: None,
identity_token: None,
registry_token: None,
};
let config = entry.to_auth_config();
assert_eq!(config.auth, Some("dXNlcjpwYXNz".to_string()));
assert!(config.username.is_none());
assert!(config.password.is_none());
}
#[test]
fn test_default_keychain_config_paths() {
let temp_dir = TempDir::new().unwrap();
let config_content = r#"{
"auths": {
"test.registry.io": {
"auth": "dGVzdDp0ZXN0"
}
}
}"#;
// Test DOCKER_CONFIG env var
let docker_config_dir = temp_dir.path().join("docker");
fs::create_dir_all(&docker_config_dir).unwrap();
create_test_config(&docker_config_dir, config_content);
std::env::set_var("DOCKER_CONFIG", docker_config_dir.to_str().unwrap());
let keychain = DefaultKeychain::new();
let auth = keychain.resolve("test.registry.io/image:tag").unwrap();
let auth_config = auth.authorization().unwrap();
assert!(!auth_config.is_anonymous());
assert_eq!(auth_config.auth, Some("dGVzdDp0ZXN0".to_string()));
std::env::remove_var("DOCKER_CONFIG");
}
// Registry extraction tests are skipped as the method is private
// The functionality is tested through the public resolve() method
#[test]
fn test_keychain_resolve_different_registries() {
// Test that keychain can resolve different registry formats
let keychain = DefaultKeychain::new();
// Should return anonymous for unknown registries
let auth = keychain.resolve("unknown.registry.io/image:tag").unwrap();
let config = auth.authorization().unwrap();
assert!(config.is_anonymous());
}
#[test]
fn test_anonymous_authenticator() {
let anon = Anonymous;
let auth = anon.authorization().unwrap();
assert!(auth.is_anonymous());
assert!(auth.to_authorization_header().unwrap().is_none());
}
#[test]
fn test_basic_authenticator() {
let basic = Basic::new("user".to_string(), "pass".to_string());
let auth = basic.authorization().unwrap();
assert!(!auth.is_anonymous());
let header = auth.to_authorization_header().unwrap().unwrap();
assert!(header.starts_with("Basic "));
}
#[test]
fn test_bearer_authenticator() {
let bearer = Bearer::new("token123".to_string());
let auth = bearer.authorization().unwrap();
assert!(!auth.is_anonymous());
let header = auth.to_authorization_header().unwrap().unwrap();
assert_eq!(header, "Bearer token123");
}
#[test]
fn test_multi_keychain() {
// Create a keychain that always returns anonymous
struct AlwaysAnonymous;
impl Keychain for AlwaysAnonymous {
fn resolve(&self, _: &str) -> Result<Box<dyn Authenticator>> {
Ok(Box::new(Anonymous))
}
}
// Create a keychain that returns basic auth
struct AlwaysBasic;
impl Keychain for AlwaysBasic {
fn resolve(&self, _: &str) -> Result<Box<dyn Authenticator>> {
Ok(Box::new(Basic::new("user".to_string(), "pass".to_string())))
}
}
// Test that MultiKeychain returns the first non-anonymous auth
let multi = MultiKeychain::new(vec![
Box::new(AlwaysAnonymous) as Box<dyn Keychain>,
Box::new(AlwaysBasic) as Box<dyn Keychain>,
]);
let auth = multi.resolve("any.registry.io").unwrap();
let config = auth.authorization().unwrap();
assert!(!config.is_anonymous());
assert_eq!(config.username, Some("user".to_string()));
}

View file

@ -1,3 +1,4 @@
pub mod auth;
pub mod builder;
pub mod cli;
pub mod config;

View file

@ -1,6 +1,7 @@
use anyhow::{Context, Result};
use clap::Parser;
use krust::{
auth::{DefaultKeychain, Keychain},
builder::{get_rust_target_triple, RustBuilder},
cli::{Cli, Commands},
config::Config,
@ -58,9 +59,9 @@ async fn main() -> Result<()> {
format!("{}/{}:latest", repo, project_name)
};
// Initialize registry client early for platform detection
let auth = oci_distribution::secrets::RegistryAuth::Anonymous;
let mut registry_client = RegistryClient::new(auth)?;
// Initialize registry client and keychain
let keychain = DefaultKeychain::new();
let mut registry_client = RegistryClient::new()?;
// Determine platforms to build for
let platforms = if let Some(platforms) = platform {
@ -72,7 +73,16 @@ async fn main() -> Result<()> {
"Detecting available platforms from base image: {}",
base_image
);
match registry_client.get_image_platforms(&base_image).await {
// Get auth for the base image registry
let base_auth = keychain
.resolve(&base_image)?
.authorization()?
.to_registry_auth();
match registry_client
.get_image_platforms(&base_image, &base_auth)
.await
{
Ok(detected_platforms) => {
if detected_platforms.is_empty() {
info!("No platforms detected, using defaults");
@ -132,8 +142,14 @@ async fn main() -> Result<()> {
let platform_tag = format!("platform-{}", platform_str.replace('/', "-"));
let platform_ref = format!("{}:{}", base_ref, platform_tag);
// Get auth for the target registry
let push_auth = keychain
.resolve(&platform_ref)?
.authorization()?
.to_registry_auth();
let (digest_ref, manifest_size) = registry_client
.push_image(&platform_ref, config_data, layers)
.push_image(&platform_ref, config_data, layers, &push_auth)
.await?;
// Parse platform string
@ -171,8 +187,14 @@ async fn main() -> Result<()> {
if !no_push {
info!("Creating and pushing manifest list...");
// Get auth for the final image push
let final_auth = keychain
.resolve(&image_ref)?
.authorization()?
.to_registry_auth();
let manifest_list_ref = registry_client
.push_manifest_list(&image_ref, manifest_descriptors)
.push_manifest_list(&image_ref, manifest_descriptors, &final_auth)
.await?;
// Output the manifest list reference

View file

@ -10,14 +10,12 @@ mod tests;
pub struct RegistryClient {
client: Client,
#[allow(dead_code)]
auth: RegistryAuth,
}
impl RegistryClient {
pub fn new(auth: RegistryAuth) -> Result<Self> {
pub fn new() -> Result<Self> {
let client = Client::new(oci_distribution::client::ClientConfig::default());
Ok(Self { client, auth })
Ok(Self { client })
}
pub async fn push_image(
@ -25,6 +23,7 @@ impl RegistryClient {
image_ref: &str,
config_data: Vec<u8>,
layers: Vec<(Vec<u8>, String)>,
auth: &RegistryAuth,
) -> Result<(String, usize)> {
let reference: Reference = image_ref
.parse()
@ -32,6 +31,12 @@ impl RegistryClient {
info!("Pushing image to {}", reference);
// Authenticate with the registry
self.client
.auth(&reference, auth, oci_distribution::RegistryOperation::Push)
.await
.context("Failed to authenticate with registry")?;
// Push config blob
let config_digest = format!("sha256:{}", sha256::digest(&config_data));
debug!("Pushing config blob: {}", config_digest);
@ -110,10 +115,17 @@ impl RegistryClient {
&mut self,
image_ref: &str,
manifest_descriptors: Vec<crate::manifest::ManifestDescriptor>,
auth: &RegistryAuth,
) -> Result<String> {
let reference = Reference::from_str(image_ref)
.context(format!("Failed to parse image reference: {}", image_ref))?;
// Authenticate with the registry
self.client
.auth(&reference, auth, oci_distribution::RegistryOperation::Push)
.await
.context("Failed to authenticate with registry")?;
// Create the image index
let index = crate::manifest::ImageIndex::new(manifest_descriptors);
@ -180,7 +192,11 @@ impl RegistryClient {
}
/// Fetch the manifest for an image and extract available platforms
pub async fn get_image_platforms(&mut self, image_ref: &str) -> Result<Vec<String>> {
pub async fn get_image_platforms(
&mut self,
image_ref: &str,
auth: &RegistryAuth,
) -> Result<Vec<String>> {
let reference: Reference = image_ref
.parse()
.context("Failed to parse image reference")?;
@ -190,7 +206,7 @@ impl RegistryClient {
// Pull the manifest
let (manifest, _) = self
.client
.pull_manifest(&reference, &self.auth)
.pull_manifest(&reference, auth)
.await
.context("Failed to pull manifest")?;

View file

@ -0,0 +1,76 @@
//! Integration tests for authentication
use anyhow::Result;
use krust::auth::{AuthConfig, DefaultKeychain, Keychain};
use std::fs;
use tempfile::TempDir;
#[test]
fn test_auth_integration_with_docker_config() -> Result<()> {
// Create a temporary directory for Docker config
let temp_dir = TempDir::new()?;
let config_dir = temp_dir.path().join(".docker");
fs::create_dir_all(&config_dir)?;
// Create a test Docker config
let config_content = r#"{
"auths": {
"ghcr.io": {
"auth": "dGVzdDp0ZXN0MTIz"
},
"docker.io": {
"username": "testuser",
"password": "testpass"
}
}
}"#;
fs::write(config_dir.join("config.json"), config_content)?;
// Set HOME to temp directory
std::env::set_var("HOME", temp_dir.path());
let keychain = DefaultKeychain::new();
// Test GitHub Container Registry auth
let ghcr_auth = keychain.resolve("ghcr.io/user/image:tag")?;
let ghcr_config = ghcr_auth.authorization()?;
assert!(!ghcr_config.is_anonymous());
assert_eq!(ghcr_config.auth, Some("dGVzdDp0ZXN0MTIz".to_string()));
// Test Docker Hub auth
let docker_auth = keychain.resolve("docker.io/library/ubuntu:latest")?;
let docker_config = docker_auth.authorization()?;
assert!(!docker_config.is_anonymous());
assert_eq!(docker_config.username, Some("testuser".to_string()));
assert_eq!(docker_config.password, Some("testpass".to_string()));
// Test unknown registry returns anonymous
let unknown_auth = keychain.resolve("unknown.registry.io/image:tag")?;
let unknown_config = unknown_auth.authorization()?;
assert!(unknown_config.is_anonymous());
// Clean up
std::env::remove_var("HOME");
Ok(())
}
#[test]
fn test_auth_config_creation() {
// Test anonymous
let anon = AuthConfig::anonymous();
assert!(anon.is_anonymous());
// Test basic auth
let basic = AuthConfig::new("user".to_string(), "pass".to_string());
assert!(!basic.is_anonymous());
assert_eq!(basic.username, Some("user".to_string()));
assert_eq!(basic.password, Some("pass".to_string()));
// Test auth header generation
let header = basic.to_authorization_header().unwrap();
assert!(header.is_some());
let header_val = header.unwrap();
assert!(header_val.starts_with("Basic "));
}