mirror of
https://github.com/imjasonh/krust
synced 2026-07-08 06:45:32 +00:00
feat: Add credential helper support to oci-distribution
- Move Docker config parsing and credential helper execution from krust - Add automatic auth resolution methods (*_auto) to the client - Support standard Docker config locations and environment variables - Add comprehensive tests and examples - Simplify krust to use the new credential helper functionality
This commit is contained in:
parent
4af701e617
commit
a6ae83c3b2
12 changed files with 887 additions and 15 deletions
|
|
@ -32,3 +32,7 @@ base64 = "0.22"
|
||||||
tempfile = "3.9"
|
tempfile = "3.9"
|
||||||
assert_cmd = "2.0"
|
assert_cmd = "2.0"
|
||||||
predicates = "3.0"
|
predicates = "3.0"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "auto_auth_demo"
|
||||||
|
path = "examples/auto_auth_demo.rs"
|
||||||
|
|
|
||||||
71
examples/auto_auth_demo.rs
Normal file
71
examples/auto_auth_demo.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
//! Example demonstrating automatic authentication with oci-distribution
|
||||||
|
//!
|
||||||
|
//! This example shows how to use the automatic auth methods that resolve
|
||||||
|
//! credentials from Docker config files and credential helpers.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use oci_distribution::{client::Client, Reference};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Initialize logging
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// Create a client
|
||||||
|
let client = Client::default();
|
||||||
|
|
||||||
|
// Example 1: Pull a public image (anonymous auth)
|
||||||
|
println!("Pulling public image alpine:latest...");
|
||||||
|
let reference: Reference = "docker.io/library/alpine:latest".parse()?;
|
||||||
|
|
||||||
|
match client.pull_manifest_auto(&reference).await {
|
||||||
|
Ok((manifest, digest)) => {
|
||||||
|
println!("✓ Successfully pulled manifest");
|
||||||
|
println!(" Digest: {}", digest);
|
||||||
|
match manifest {
|
||||||
|
oci_distribution::manifest::OciManifest::Image(img) => {
|
||||||
|
println!(" Type: Single platform image");
|
||||||
|
println!(" Config: {}", img.config.digest);
|
||||||
|
}
|
||||||
|
oci_distribution::manifest::OciManifest::ImageIndex(idx) => {
|
||||||
|
println!(" Type: Multi-platform image");
|
||||||
|
println!(" Platforms: {}", idx.manifests.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("✗ Failed to pull: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: Get platforms for an image
|
||||||
|
println!("\nDetecting platforms for rust:alpine...");
|
||||||
|
let rust_ref: Reference = "docker.io/library/rust:alpine".parse()?;
|
||||||
|
|
||||||
|
match client.get_image_platforms_auto(&rust_ref).await {
|
||||||
|
Ok(platforms) => {
|
||||||
|
println!("✓ Found {} platforms:", platforms.len());
|
||||||
|
for (os, arch) in platforms {
|
||||||
|
println!(" - {}/{}", os, arch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("✗ Failed to get platforms: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 3: Private registry (requires auth)
|
||||||
|
println!("\nNote: To test with a private registry, ensure you have credentials in:");
|
||||||
|
println!(" - ~/.docker/config.json");
|
||||||
|
println!(" - Or DOCKER_CONFIG environment variable");
|
||||||
|
println!(" - Or using a credential helper");
|
||||||
|
|
||||||
|
// Uncomment to test with a private registry:
|
||||||
|
// let private_ref: Reference = "ghcr.io/your-username/your-image:latest".parse()?;
|
||||||
|
// match client.pull_manifest_auto(&private_ref).await {
|
||||||
|
// Ok((_, digest)) => println!("✓ Successfully authenticated and pulled private image: {}", digest),
|
||||||
|
// Err(e) => println!("✗ Failed to pull private image: {}", e),
|
||||||
|
// }
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,10 @@ use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
mod keychain;
|
mod keychain;
|
||||||
|
mod simple;
|
||||||
|
|
||||||
pub use keychain::{DefaultKeychain, Keychain};
|
pub use keychain::{DefaultKeychain, Keychain};
|
||||||
|
pub use simple::resolve_auth;
|
||||||
|
|
||||||
/// Authentication configuration containing credentials
|
/// Authentication configuration containing credentials
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
|
|
||||||
11
src/auth/simple.rs
Normal file
11
src/auth/simple.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
//! 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<RegistryAuth> {
|
||||||
|
resolve_docker_auth(resource)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to resolve auth: {}", e))
|
||||||
|
}
|
||||||
20
src/main.rs
20
src/main.rs
|
|
@ -1,7 +1,7 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use krust::{
|
use krust::{
|
||||||
auth::{DefaultKeychain, Keychain},
|
auth::resolve_auth,
|
||||||
builder::{get_rust_target_triple, RustBuilder},
|
builder::{get_rust_target_triple, RustBuilder},
|
||||||
cli::{Cli, Commands},
|
cli::{Cli, Commands},
|
||||||
config::Config,
|
config::Config,
|
||||||
|
|
@ -59,8 +59,7 @@ async fn main() -> Result<()> {
|
||||||
format!("{}/{}:latest", repo, project_name)
|
format!("{}/{}:latest", repo, project_name)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize registry client and keychain
|
// Initialize registry client
|
||||||
let keychain = DefaultKeychain::new();
|
|
||||||
let mut registry_client = RegistryClient::new()?;
|
let mut registry_client = RegistryClient::new()?;
|
||||||
|
|
||||||
// Determine platforms to build for
|
// Determine platforms to build for
|
||||||
|
|
@ -74,10 +73,7 @@ async fn main() -> Result<()> {
|
||||||
base_image
|
base_image
|
||||||
);
|
);
|
||||||
// Get auth for the base image registry
|
// Get auth for the base image registry
|
||||||
let base_auth = keychain
|
let base_auth = resolve_auth(&base_image)?;
|
||||||
.resolve(&base_image)?
|
|
||||||
.authorization()?
|
|
||||||
.to_registry_auth();
|
|
||||||
|
|
||||||
match registry_client
|
match registry_client
|
||||||
.get_image_platforms(&base_image, &base_auth)
|
.get_image_platforms(&base_image, &base_auth)
|
||||||
|
|
@ -143,10 +139,7 @@ async fn main() -> Result<()> {
|
||||||
let platform_ref = format!("{}:{}", base_ref, platform_tag);
|
let platform_ref = format!("{}:{}", base_ref, platform_tag);
|
||||||
|
|
||||||
// Get auth for the target registry
|
// Get auth for the target registry
|
||||||
let push_auth = keychain
|
let push_auth = resolve_auth(&platform_ref)?;
|
||||||
.resolve(&platform_ref)?
|
|
||||||
.authorization()?
|
|
||||||
.to_registry_auth();
|
|
||||||
|
|
||||||
let (digest_ref, manifest_size) = registry_client
|
let (digest_ref, manifest_size) = registry_client
|
||||||
.push_image(&platform_ref, config_data, layers, &push_auth)
|
.push_image(&platform_ref, config_data, layers, &push_auth)
|
||||||
|
|
@ -188,10 +181,7 @@ async fn main() -> Result<()> {
|
||||||
info!("Creating and pushing manifest list...");
|
info!("Creating and pushing manifest list...");
|
||||||
|
|
||||||
// Get auth for the final image push
|
// Get auth for the final image push
|
||||||
let final_auth = keychain
|
let final_auth = resolve_auth(&image_ref)?;
|
||||||
.resolve(&image_ref)?
|
|
||||||
.authorization()?
|
|
||||||
.to_registry_auth();
|
|
||||||
|
|
||||||
let manifest_list_ref = registry_client
|
let manifest_list_ref = registry_client
|
||||||
.push_manifest_list(&image_ref, manifest_descriptors, &final_auth)
|
.push_manifest_list(&image_ref, manifest_descriptors, &final_auth)
|
||||||
|
|
|
||||||
111
tests/credential_helper_integration_test.rs
Normal file
111
tests/credential_helper_integration_test.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
//! Integration tests for credential helper functionality
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use krust::auth::resolve_auth;
|
||||||
|
use oci_distribution::secrets::RegistryAuth;
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_auth_anonymous() -> Result<()> {
|
||||||
|
// Create empty temp directory for Docker config
|
||||||
|
let tmp_dir = TempDir::new()?;
|
||||||
|
|
||||||
|
// Save current env vars
|
||||||
|
let old_docker_config = env::var("DOCKER_CONFIG").ok();
|
||||||
|
let old_registry_auth = env::var("REGISTRY_AUTH_FILE").ok();
|
||||||
|
|
||||||
|
// Set to empty directory
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
env::remove_var("REGISTRY_AUTH_FILE");
|
||||||
|
|
||||||
|
// Should resolve to anonymous
|
||||||
|
let auth = resolve_auth("docker.io/library/alpine")?;
|
||||||
|
assert!(matches!(auth, RegistryAuth::Anonymous));
|
||||||
|
|
||||||
|
// 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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_auth_from_config() -> Result<()> {
|
||||||
|
let tmp_dir = TempDir::new()?;
|
||||||
|
let config_path = tmp_dir.path().join("config.json");
|
||||||
|
|
||||||
|
// Create a test config
|
||||||
|
let config = r#"{
|
||||||
|
"auths": {
|
||||||
|
"test.registry.io": {
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "testpass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
fs::write(&config_path, config)?;
|
||||||
|
|
||||||
|
// Save current env var
|
||||||
|
let old_val = env::var("DOCKER_CONFIG").ok();
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
|
||||||
|
// Should resolve to basic auth
|
||||||
|
let auth = resolve_auth("test.registry.io/myimage")?;
|
||||||
|
assert!(matches!(auth, RegistryAuth::Basic(user, pass)
|
||||||
|
if user == "testuser" && pass == "testpass"));
|
||||||
|
|
||||||
|
// Restore env var
|
||||||
|
if let Some(val) = old_val {
|
||||||
|
env::set_var("DOCKER_CONFIG", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("DOCKER_CONFIG");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_auth_bearer_token() -> Result<()> {
|
||||||
|
let tmp_dir = TempDir::new()?;
|
||||||
|
let config_path = tmp_dir.path().join("config.json");
|
||||||
|
|
||||||
|
// Create a test config with bearer token
|
||||||
|
let config = r#"{
|
||||||
|
"auths": {
|
||||||
|
"ghcr.io": {
|
||||||
|
"registrytoken": "test-bearer-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
fs::write(&config_path, config)?;
|
||||||
|
|
||||||
|
// Save current env var
|
||||||
|
let old_val = env::var("DOCKER_CONFIG").ok();
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
env::set_var("DOCKER_CONFIG", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("DOCKER_CONFIG");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
2
vendor/oci-distribution/Cargo.toml
vendored
2
vendor/oci-distribution/Cargo.toml
vendored
|
|
@ -32,8 +32,10 @@ trust-dns = ["reqwest/trust-dns"]
|
||||||
test-registry = []
|
test-registry = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.22"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
chrono = { version = "0.4.23", features = ["serde"] }
|
chrono = { version = "0.4.23", features = ["serde"] }
|
||||||
|
dirs = "6.0"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
http = "1.1"
|
http = "1.1"
|
||||||
http-auth = { version = "0.1", default_features = false }
|
http-auth = { version = "0.1", default_features = false }
|
||||||
|
|
|
||||||
39
vendor/oci-distribution/src/client.rs
vendored
39
vendor/oci-distribution/src/client.rs
vendored
|
|
@ -918,6 +918,45 @@ impl Client {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pull a manifest with automatic auth resolution using Docker config and credential helpers
|
||||||
|
pub async fn pull_manifest_auto(
|
||||||
|
&self,
|
||||||
|
image: &Reference,
|
||||||
|
) -> Result<(OciManifest, String)> {
|
||||||
|
let auth = crate::credential_helper::resolve_docker_auth(&image.to_string())?;
|
||||||
|
self.pull_manifest(image, &auth).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pull an image with automatic auth resolution using Docker config and credential helpers
|
||||||
|
pub async fn pull_auto(
|
||||||
|
&self,
|
||||||
|
image: &Reference,
|
||||||
|
) -> Result<ImageData> {
|
||||||
|
let auth = crate::credential_helper::resolve_docker_auth(&image.to_string())?;
|
||||||
|
self.pull(image, &auth, vec![]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push an image with automatic auth resolution using Docker config and credential helpers
|
||||||
|
pub async fn push_auto(
|
||||||
|
&self,
|
||||||
|
image_ref: &Reference,
|
||||||
|
layers: &[ImageLayer],
|
||||||
|
config: Config,
|
||||||
|
manifest: Option<OciImageManifest>,
|
||||||
|
) -> Result<PushResponse> {
|
||||||
|
let auth = crate::credential_helper::resolve_docker_auth(&image_ref.to_string())?;
|
||||||
|
self.push(image_ref, layers, config, &auth, manifest).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get image platforms with automatic auth resolution
|
||||||
|
pub async fn get_image_platforms_auto(
|
||||||
|
&self,
|
||||||
|
image: &Reference,
|
||||||
|
) -> Result<Vec<(String, String)>> {
|
||||||
|
let auth = crate::credential_helper::resolve_docker_auth(&image.to_string())?;
|
||||||
|
self.get_image_platforms(image, &auth).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn _pull_manifest_and_config(
|
async fn _pull_manifest_and_config(
|
||||||
&self,
|
&self,
|
||||||
image: &Reference,
|
image: &Reference,
|
||||||
|
|
|
||||||
165
vendor/oci-distribution/src/credential_helper.rs
vendored
Normal file
165
vendor/oci-distribution/src/credential_helper.rs
vendored
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
//! Docker credential helper support
|
||||||
|
|
||||||
|
use crate::docker_config::{DockerAuthEntry, DockerConfig, extract_registry, load_docker_config, normalize_registry};
|
||||||
|
use crate::errors::{OciDistributionError, Result};
|
||||||
|
use crate::secrets::RegistryAuth;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
/// Response from a Docker credential helper
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct HelperResponse {
|
||||||
|
#[serde(rename = "Username")]
|
||||||
|
username: Option<String>,
|
||||||
|
#[serde(rename = "Secret")]
|
||||||
|
secret: Option<String>,
|
||||||
|
#[serde(rename = "ServerURL")]
|
||||||
|
_server_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a credential helper to get credentials
|
||||||
|
pub fn execute_credential_helper(helper: &str, registry: &str) -> Result<(String, String)> {
|
||||||
|
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()
|
||||||
|
.map_err(|e| OciDistributionError::GenericError(Some(
|
||||||
|
format!("Failed to spawn credential helper {}: {}", helper_name, e)
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
// Write registry URL to stdin
|
||||||
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
|
stdin.write_all(registry.as_bytes()).map_err(|e| {
|
||||||
|
OciDistributionError::IoError(e)
|
||||||
|
})?;
|
||||||
|
stdin.write_all(b"\n").map_err(|e| {
|
||||||
|
OciDistributionError::IoError(e)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = child.wait_with_output().map_err(|e| {
|
||||||
|
OciDistributionError::IoError(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(OciDistributionError::GenericError(Some(
|
||||||
|
format!("Credential helper {} failed: {}", helper_name, stderr)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse output as JSON
|
||||||
|
let response: HelperResponse = serde_json::from_slice(&output.stdout)
|
||||||
|
.map_err(|e| OciDistributionError::GenericError(Some(
|
||||||
|
format!("Failed to parse credential helper response: {}", e)
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
match (response.username, response.secret) {
|
||||||
|
(Some(username), Some(password)) => Ok((username, password)),
|
||||||
|
_ => Err(OciDistributionError::GenericError(Some(
|
||||||
|
"Credential helper did not return username and password".to_string()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find auth entry for a registry in Docker config
|
||||||
|
fn find_auth_entry(config: &DockerConfig, registry: &str) -> Option<DockerAuthEntry> {
|
||||||
|
let variants = 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(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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve authentication for a given resource using Docker config and credential helpers
|
||||||
|
pub fn resolve_docker_auth(resource: &str) -> Result<RegistryAuth> {
|
||||||
|
let config = load_docker_config()?;
|
||||||
|
let registry = extract_registry(resource);
|
||||||
|
|
||||||
|
debug!("Resolving auth for resource: {} (registry: {})", resource, registry);
|
||||||
|
|
||||||
|
// Try to find auth entry in config
|
||||||
|
if let Some(auth_entry) = find_auth_entry(&config, registry) {
|
||||||
|
debug!("Found auth entry for {}", registry);
|
||||||
|
|
||||||
|
// Check if it's anonymous
|
||||||
|
if auth_entry.is_anonymous() {
|
||||||
|
return Ok(RegistryAuth::Anonymous);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for bearer tokens first
|
||||||
|
if let Some(token) = auth_entry.registry_token {
|
||||||
|
return Ok(RegistryAuth::Bearer(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(token) = auth_entry.identity_token {
|
||||||
|
return Ok(RegistryAuth::Bearer(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check for basic auth
|
||||||
|
if let (Some(username), Some(password)) = (&auth_entry.username, &auth_entry.password) {
|
||||||
|
return Ok(RegistryAuth::Basic(username.clone(), password.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to decode base64 auth string
|
||||||
|
if let Some(auth) = &auth_entry.auth {
|
||||||
|
use base64::Engine;
|
||||||
|
match base64::engine::general_purpose::STANDARD.decode(auth) {
|
||||||
|
Ok(decoded) => {
|
||||||
|
if let Ok(decoded_str) = String::from_utf8(decoded) {
|
||||||
|
if let Some((user, pass)) = decoded_str.split_once(':') {
|
||||||
|
return Ok(RegistryAuth::Basic(user.to_string(), pass.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to decode auth for {}: {}", registry, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try credential helper
|
||||||
|
if let Some(helper) = get_credential_helper(&config, registry) {
|
||||||
|
debug!("Trying credential helper: {} for {}", helper, registry);
|
||||||
|
match execute_credential_helper(&helper, registry) {
|
||||||
|
Ok((username, password)) => {
|
||||||
|
return Ok(RegistryAuth::Basic(username, password));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Credential helper failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to anonymous
|
||||||
|
debug!("No credentials found for {}, using anonymous", registry);
|
||||||
|
Ok(RegistryAuth::Anonymous)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "credential_helper_tests.rs"]
|
||||||
|
mod tests;
|
||||||
295
vendor/oci-distribution/src/credential_helper_tests.rs
vendored
Normal file
295
vendor/oci-distribution/src/credential_helper_tests.rs
vendored
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
//! Tests for credential helper functionality
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::credential_helper::*;
|
||||||
|
use crate::secrets::RegistryAuth;
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_anonymous_when_no_config() {
|
||||||
|
// Create a temp directory for DOCKER_CONFIG that doesn't have any config
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
|
||||||
|
// Save current env vars
|
||||||
|
let old_docker_config = env::var("DOCKER_CONFIG").ok();
|
||||||
|
let old_registry_auth = env::var("REGISTRY_AUTH_FILE").ok();
|
||||||
|
|
||||||
|
// Set to our empty temp directory
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
env::remove_var("REGISTRY_AUTH_FILE");
|
||||||
|
|
||||||
|
let auth = resolve_docker_auth("docker.io/library/alpine").unwrap();
|
||||||
|
assert_eq!(auth, RegistryAuth::Anonymous);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_basic_auth_from_config() {
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = tmp_dir.path().join("config.json");
|
||||||
|
|
||||||
|
let config = r#"{
|
||||||
|
"auths": {
|
||||||
|
"docker.io": {
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "testpass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
fs::write(&config_path, config).unwrap();
|
||||||
|
|
||||||
|
// Save current env var
|
||||||
|
let old_val = env::var("DOCKER_CONFIG").ok();
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
|
||||||
|
let auth = resolve_docker_auth("docker.io/library/alpine").unwrap();
|
||||||
|
assert_eq!(auth, RegistryAuth::Basic("testuser".to_string(), "testpass".to_string()));
|
||||||
|
|
||||||
|
// Restore env var
|
||||||
|
if let Some(val) = old_val {
|
||||||
|
env::set_var("DOCKER_CONFIG", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("DOCKER_CONFIG");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_bearer_token_from_config() {
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = tmp_dir.path().join("config.json");
|
||||||
|
|
||||||
|
let config = r#"{
|
||||||
|
"auths": {
|
||||||
|
"ghcr.io": {
|
||||||
|
"registrytoken": "ghp_abc123token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
fs::write(&config_path, config).unwrap();
|
||||||
|
|
||||||
|
// Save current env var
|
||||||
|
let old_val = env::var("DOCKER_CONFIG").ok();
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
|
||||||
|
let auth = resolve_docker_auth("ghcr.io/user/image").unwrap();
|
||||||
|
assert_eq!(auth, RegistryAuth::Bearer("ghp_abc123token".to_string()));
|
||||||
|
|
||||||
|
// Restore env var
|
||||||
|
if let Some(val) = old_val {
|
||||||
|
env::set_var("DOCKER_CONFIG", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("DOCKER_CONFIG");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_base64_auth_from_config() {
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = tmp_dir.path().join("config.json");
|
||||||
|
|
||||||
|
// Base64 encoded "testuser:testpass"
|
||||||
|
let config = r#"{
|
||||||
|
"auths": {
|
||||||
|
"docker.io": {
|
||||||
|
"auth": "dGVzdHVzZXI6dGVzdHBhc3M="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
fs::write(&config_path, config).unwrap();
|
||||||
|
|
||||||
|
// Save current env var
|
||||||
|
let old_val = env::var("DOCKER_CONFIG").ok();
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
|
||||||
|
let auth = resolve_docker_auth("docker.io/library/alpine").unwrap();
|
||||||
|
assert_eq!(auth, RegistryAuth::Basic("testuser".to_string(), "testpass".to_string()));
|
||||||
|
|
||||||
|
// Restore env var
|
||||||
|
if let Some(val) = old_val {
|
||||||
|
env::set_var("DOCKER_CONFIG", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("DOCKER_CONFIG");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_registry_normalization() {
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
let config_path = tmp_dir.path().join("config.json");
|
||||||
|
|
||||||
|
// Config has index.docker.io but we query with docker.io
|
||||||
|
let config = r#"{
|
||||||
|
"auths": {
|
||||||
|
"index.docker.io": {
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "testpass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
fs::write(&config_path, config).unwrap();
|
||||||
|
|
||||||
|
// Save current env var
|
||||||
|
let old_val = env::var("DOCKER_CONFIG").ok();
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
|
||||||
|
// Should find auth even though we use docker.io
|
||||||
|
let auth = resolve_docker_auth("docker.io/library/alpine").unwrap();
|
||||||
|
assert_eq!(auth, RegistryAuth::Basic("testuser".to_string(), "testpass".to_string()));
|
||||||
|
|
||||||
|
// Restore env var
|
||||||
|
if let Some(val) = old_val {
|
||||||
|
env::set_var("DOCKER_CONFIG", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("DOCKER_CONFIG");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_credential_helper_mock() {
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
|
||||||
|
// Create a mock credential helper script
|
||||||
|
let helper_path = tmp_dir.path().join("docker-credential-mock");
|
||||||
|
let mut file = fs::File::create(&helper_path).unwrap();
|
||||||
|
writeln!(file, "#!/bin/sh").unwrap();
|
||||||
|
writeln!(file, "echo '{{\"Username\":\"helper-user\",\"Secret\":\"helper-pass\"}}'").unwrap();
|
||||||
|
|
||||||
|
// Make it executable
|
||||||
|
let mut perms = fs::metadata(&helper_path).unwrap().permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(&helper_path, perms).unwrap();
|
||||||
|
|
||||||
|
// Create config that uses the helper
|
||||||
|
let config_path = tmp_dir.path().join("config.json");
|
||||||
|
let config = r#"{
|
||||||
|
"credHelpers": {
|
||||||
|
"mock.registry.io": "mock"
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
fs::write(&config_path, config).unwrap();
|
||||||
|
|
||||||
|
// Save current env vars
|
||||||
|
let old_config = env::var("DOCKER_CONFIG").ok();
|
||||||
|
let old_path = env::var("PATH").ok();
|
||||||
|
|
||||||
|
// Set our temp dir in PATH and DOCKER_CONFIG
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
let new_path = format!("{}:{}", tmp_dir.path().display(), env::var("PATH").unwrap_or_default());
|
||||||
|
env::set_var("PATH", new_path);
|
||||||
|
|
||||||
|
// Test the credential helper
|
||||||
|
let auth = resolve_docker_auth("mock.registry.io/test/image").unwrap();
|
||||||
|
assert_eq!(auth, RegistryAuth::Basic("helper-user".to_string(), "helper-pass".to_string()));
|
||||||
|
|
||||||
|
// Restore env vars
|
||||||
|
if let Some(val) = old_config {
|
||||||
|
env::set_var("DOCKER_CONFIG", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("DOCKER_CONFIG");
|
||||||
|
}
|
||||||
|
if let Some(val) = old_path {
|
||||||
|
env::set_var("PATH", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("PATH");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_credential_store_mock() {
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
|
||||||
|
// Create a mock credential helper script
|
||||||
|
let helper_path = tmp_dir.path().join("docker-credential-defaultstore");
|
||||||
|
let mut file = fs::File::create(&helper_path).unwrap();
|
||||||
|
writeln!(file, "#!/bin/sh").unwrap();
|
||||||
|
writeln!(file, "echo '{{\"Username\":\"store-user\",\"Secret\":\"store-pass\"}}'").unwrap();
|
||||||
|
|
||||||
|
// Make it executable
|
||||||
|
let mut perms = fs::metadata(&helper_path).unwrap().permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
fs::set_permissions(&helper_path, perms).unwrap();
|
||||||
|
|
||||||
|
// Create config that uses default creds store
|
||||||
|
let config_path = tmp_dir.path().join("config.json");
|
||||||
|
let config = r#"{
|
||||||
|
"credsStore": "defaultstore"
|
||||||
|
}"#;
|
||||||
|
fs::write(&config_path, config).unwrap();
|
||||||
|
|
||||||
|
// Save current env vars
|
||||||
|
let old_config = env::var("DOCKER_CONFIG").ok();
|
||||||
|
let old_path = env::var("PATH").ok();
|
||||||
|
|
||||||
|
// Set our temp dir in PATH and DOCKER_CONFIG
|
||||||
|
env::set_var("DOCKER_CONFIG", tmp_dir.path());
|
||||||
|
let new_path = format!("{}:{}", tmp_dir.path().display(), env::var("PATH").unwrap_or_default());
|
||||||
|
env::set_var("PATH", new_path);
|
||||||
|
|
||||||
|
// Test the credential helper
|
||||||
|
let auth = resolve_docker_auth("any.registry.io/test/image").unwrap();
|
||||||
|
assert_eq!(auth, RegistryAuth::Basic("store-user".to_string(), "store-pass".to_string()));
|
||||||
|
|
||||||
|
// Restore env vars
|
||||||
|
if let Some(val) = old_config {
|
||||||
|
env::set_var("DOCKER_CONFIG", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("DOCKER_CONFIG");
|
||||||
|
}
|
||||||
|
if let Some(val) = old_path {
|
||||||
|
env::set_var("PATH", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("PATH");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_registry_auth_file_env() {
|
||||||
|
let tmp_dir = TempDir::new().unwrap();
|
||||||
|
let auth_file = tmp_dir.path().join("auth.json");
|
||||||
|
|
||||||
|
let config = r#"{
|
||||||
|
"auths": {
|
||||||
|
"special.registry.io": {
|
||||||
|
"username": "authfile-user",
|
||||||
|
"password": "authfile-pass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
fs::write(&auth_file, config).unwrap();
|
||||||
|
|
||||||
|
// Save current env var
|
||||||
|
let old_val = env::var("REGISTRY_AUTH_FILE").ok();
|
||||||
|
env::set_var("REGISTRY_AUTH_FILE", auth_file.to_str().unwrap());
|
||||||
|
|
||||||
|
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 {
|
||||||
|
env::set_var("REGISTRY_AUTH_FILE", val);
|
||||||
|
} else {
|
||||||
|
env::remove_var("REGISTRY_AUTH_FILE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
vendor/oci-distribution/src/docker_config.rs
vendored
Normal file
180
vendor/oci-distribution/src/docker_config.rs
vendored
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
//! Docker config file parsing and credential helper support
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
/// Docker config file structure
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct DockerConfig {
|
||||||
|
/// Registry authentication entries
|
||||||
|
#[serde(default)]
|
||||||
|
pub auths: HashMap<String, DockerAuthEntry>,
|
||||||
|
/// Registry-specific credential helpers
|
||||||
|
#[serde(rename = "credHelpers", default)]
|
||||||
|
pub cred_helpers: HashMap<String, String>,
|
||||||
|
/// Default credential store to use
|
||||||
|
#[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 {
|
||||||
|
/// Base64-encoded username:password
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub auth: Option<String>,
|
||||||
|
/// Username for authentication
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub username: Option<String>,
|
||||||
|
/// Password for authentication
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub password: Option<String>,
|
||||||
|
/// Identity token for registry authentication
|
||||||
|
#[serde(rename = "identitytoken", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub identity_token: Option<String>,
|
||||||
|
/// Registry token for bearer authentication
|
||||||
|
#[serde(rename = "registrytoken", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub registry_token: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DockerAuthEntry {
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get paths to check for Docker config
|
||||||
|
pub 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
|
||||||
|
pub fn load_docker_config() -> crate::errors::Result<DockerConfig> {
|
||||||
|
// Try each config path
|
||||||
|
for path in 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());
|
||||||
|
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
|
||||||
|
pub 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
|
||||||
|
pub 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
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_registry() {
|
||||||
|
assert_eq!(extract_registry("docker.io/library/ubuntu:latest"), "docker.io");
|
||||||
|
assert_eq!(extract_registry("gcr.io/project/image:tag"), "gcr.io");
|
||||||
|
assert_eq!(extract_registry("localhost:5000/image"), "localhost:5000");
|
||||||
|
assert_eq!(extract_registry("ubuntu:latest"), "index.docker.io");
|
||||||
|
assert_eq!(extract_registry("user/image:tag"), "index.docker.io");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_normalize_registry() {
|
||||||
|
let variants = normalize_registry("docker.io");
|
||||||
|
assert!(variants.contains(&"docker.io".to_string()));
|
||||||
|
assert!(variants.contains(&"index.docker.io".to_string()));
|
||||||
|
|
||||||
|
let variants = normalize_registry("gcr.io");
|
||||||
|
assert!(variants.contains(&"gcr.io".to_string()));
|
||||||
|
assert!(variants.contains(&"https://gcr.io".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
2
vendor/oci-distribution/src/lib.rs
vendored
2
vendor/oci-distribution/src/lib.rs
vendored
|
|
@ -6,6 +6,8 @@ use sha2::Digest;
|
||||||
pub mod annotations;
|
pub mod annotations;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod credential_helper;
|
||||||
|
pub mod docker_config;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue