mirror of
https://github.com/imjasonh/krust
synced 2026-07-07 22:35:25 +00:00
commit
d095c26871
8 changed files with 410 additions and 27 deletions
|
|
@ -126,9 +126,9 @@ impl AuthConfig {
|
|||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct DockerConfig {
|
||||
#[serde(default)]
|
||||
pub auths: HashMap<String, DockerAuthEntry>,
|
||||
pub auths: Option<HashMap<String, DockerAuthEntry>>,
|
||||
#[serde(rename = "credHelpers", default)]
|
||||
pub cred_helpers: HashMap<String, String>,
|
||||
pub cred_helpers: Option<HashMap<String, String>>,
|
||||
#[serde(rename = "credsStore", skip_serializing_if = "Option::is_none")]
|
||||
pub creds_store: Option<String>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,243 @@
|
|||
//! Simple authentication wrapper for registry authentication
|
||||
|
||||
use crate::registry::RegistryAuth;
|
||||
use anyhow::Result;
|
||||
use crate::registry::{ImageReference, RegistryAuth};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tracing::debug;
|
||||
|
||||
use super::{DockerAuthEntry, DockerConfig};
|
||||
|
||||
/// Resolve authentication for a given resource using Docker config and credential helpers
|
||||
pub fn resolve_auth(resource: &str) -> Result<RegistryAuth> {
|
||||
// For now, return anonymous auth
|
||||
// TODO: Implement actual credential resolution from Docker config
|
||||
let _ = resource; // Silence unused warning
|
||||
debug!("Resolving auth for resource: {}", resource);
|
||||
|
||||
// Parse the resource to extract registry
|
||||
let registry = if let Ok(image_ref) = ImageReference::parse(resource) {
|
||||
image_ref.registry
|
||||
} else if resource.contains('/') {
|
||||
// If it looks like a repository (registry/repo), extract registry part
|
||||
resource.split('/').next().unwrap_or(resource).to_string()
|
||||
} else {
|
||||
// Just use the resource as-is (might be a registry hostname)
|
||||
resource.to_string()
|
||||
};
|
||||
|
||||
debug!("Extracted registry from resource: {}", registry);
|
||||
|
||||
// Try to read Docker config
|
||||
if let Ok(auth) = read_docker_config(®istry) {
|
||||
debug!("Found auth in Docker config for registry: {}", registry);
|
||||
return Ok(auth);
|
||||
}
|
||||
|
||||
// Try credential helpers
|
||||
if let Ok(auth) = try_credential_helpers(®istry) {
|
||||
debug!(
|
||||
"Found auth via credential helper for registry: {}",
|
||||
registry
|
||||
);
|
||||
return Ok(auth);
|
||||
}
|
||||
|
||||
debug!("No auth found, using anonymous for registry: {}", registry);
|
||||
Ok(RegistryAuth::Anonymous)
|
||||
}
|
||||
|
||||
fn read_docker_config(registry: &str) -> Result<RegistryAuth> {
|
||||
let config_paths = get_docker_config_paths();
|
||||
|
||||
for config_path in config_paths {
|
||||
if let Ok(config_content) = fs::read_to_string(&config_path) {
|
||||
debug!("Reading Docker config from: {:?}", config_path);
|
||||
|
||||
if let Ok(config) = serde_json::from_str::<DockerConfig>(&config_content) {
|
||||
if let Some(auths) = &config.auths {
|
||||
// Try exact registry match first
|
||||
if let Some(auth_entry) = auths.get(registry) {
|
||||
debug!("Found exact registry match for: {}", registry);
|
||||
return parse_auth_entry(auth_entry);
|
||||
}
|
||||
|
||||
// Try with https:// prefix (common in Docker config)
|
||||
let https_registry = format!("https://{}", registry);
|
||||
if let Some(auth_entry) = auths.get(&https_registry) {
|
||||
debug!("Found https registry match for: {}", https_registry);
|
||||
return parse_auth_entry(auth_entry);
|
||||
}
|
||||
|
||||
// Try registry-1.docker.io for docker.io
|
||||
if registry == "docker.io" || registry == "registry-1.docker.io" {
|
||||
for key in &[
|
||||
"docker.io",
|
||||
"registry-1.docker.io",
|
||||
"https://index.docker.io/v1/",
|
||||
] {
|
||||
if let Some(auth_entry) = auths.get(*key) {
|
||||
debug!("Found Docker Hub match with key: {}", key);
|
||||
return parse_auth_entry(auth_entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("No auth found in Docker config")
|
||||
}
|
||||
|
||||
fn parse_auth_entry(auth_entry: &DockerAuthEntry) -> Result<RegistryAuth> {
|
||||
// Check for bearer token first
|
||||
if let Some(token) = &auth_entry.registry_token {
|
||||
debug!("Using bearer token auth");
|
||||
return Ok(RegistryAuth::Bearer {
|
||||
token: token.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check for basic auth credentials
|
||||
if let (Some(username), Some(password)) = (&auth_entry.username, &auth_entry.password) {
|
||||
debug!("Using basic auth with username/password");
|
||||
return Ok(RegistryAuth::Basic {
|
||||
username: username.clone(),
|
||||
password: password.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check for base64 encoded auth
|
||||
if let Some(auth_b64) = &auth_entry.auth {
|
||||
debug!("Using base64 encoded auth");
|
||||
use base64::Engine;
|
||||
let decoded = base64::engine::general_purpose::STANDARD
|
||||
.decode(auth_b64)
|
||||
.context("Failed to decode base64 auth")?;
|
||||
let auth_str = String::from_utf8(decoded).context("Auth is not valid UTF-8")?;
|
||||
|
||||
if let Some((username, password)) = auth_str.split_once(':') {
|
||||
return Ok(RegistryAuth::Basic {
|
||||
username: username.to_string(),
|
||||
password: password.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("No valid auth found in auth entry")
|
||||
}
|
||||
|
||||
fn get_docker_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 HOME/.docker/config.json
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
paths.push(PathBuf::from(home).join(".docker").join("config.json"));
|
||||
}
|
||||
|
||||
// Check XDG_RUNTIME_DIR for rootless Docker
|
||||
if let Ok(xdg_runtime) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
paths.push(
|
||||
PathBuf::from(xdg_runtime)
|
||||
.join("containers")
|
||||
.join("auth.json"),
|
||||
);
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
fn try_credential_helpers(registry: &str) -> Result<RegistryAuth> {
|
||||
let config_paths = get_docker_config_paths();
|
||||
|
||||
for config_path in config_paths {
|
||||
if let Ok(config_content) = fs::read_to_string(&config_path) {
|
||||
if let Ok(config) = serde_json::from_str::<DockerConfig>(&config_content) {
|
||||
// Check specific credential helpers first
|
||||
if let Some(cred_helpers) = &config.cred_helpers {
|
||||
if let Some(helper) = cred_helpers.get(registry) {
|
||||
debug!(
|
||||
"Trying credential helper '{}' for registry: {}",
|
||||
helper, registry
|
||||
);
|
||||
if let Ok(auth) = call_credential_helper(helper, registry) {
|
||||
return Ok(auth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try default credential store
|
||||
if let Some(helper) = &config.creds_store {
|
||||
debug!(
|
||||
"Trying default credential helper '{}' for registry: {}",
|
||||
helper, registry
|
||||
);
|
||||
if let Ok(auth) = call_credential_helper(helper, registry) {
|
||||
return Ok(auth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("No credential helpers found")
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CredentialHelperResponse {
|
||||
#[serde(rename = "Username")]
|
||||
username: String,
|
||||
#[serde(rename = "Secret")]
|
||||
secret: String,
|
||||
}
|
||||
|
||||
fn call_credential_helper(helper: &str, registry: &str) -> Result<RegistryAuth> {
|
||||
use std::io::Write;
|
||||
use std::process::Stdio;
|
||||
|
||||
let helper_name = format!("docker-credential-{}", helper);
|
||||
|
||||
debug!("Calling credential helper: {}", helper_name);
|
||||
|
||||
let mut child = Command::new(&helper_name)
|
||||
.arg("get")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context(format!(
|
||||
"Failed to execute credential helper: {}",
|
||||
helper_name
|
||||
))?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin
|
||||
.write_all(registry.as_bytes())
|
||||
.context("Failed to write to credential helper stdin")?;
|
||||
}
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.context("Failed to wait for credential helper")?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!(
|
||||
"Credential helper {} failed: {}",
|
||||
helper_name,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let response: CredentialHelperResponse = serde_json::from_slice(&output.stdout)
|
||||
.context("Failed to parse credential helper response")?;
|
||||
|
||||
Ok(RegistryAuth::Basic {
|
||||
username: response.username,
|
||||
password: response.secret,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,20 +23,22 @@ fn test_docker_config_parsing() {
|
|||
|
||||
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 auths = config.auths.as_ref().unwrap();
|
||||
assert_eq!(auths.len(), 2);
|
||||
assert!(auths.contains_key("docker.io"));
|
||||
assert!(auths.contains_key("gcr.io"));
|
||||
|
||||
let docker_auth = &config.auths["docker.io"];
|
||||
let docker_auth = &auths["docker.io"];
|
||||
assert_eq!(docker_auth.auth, Some("dXNlcjpwYXNz".to_string()));
|
||||
|
||||
let gcr_auth = &config.auths["gcr.io"];
|
||||
let gcr_auth = &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");
|
||||
let cred_helpers = config.cred_helpers.as_ref().unwrap();
|
||||
assert_eq!(cred_helpers.len(), 1);
|
||||
assert_eq!(cred_helpers["ecr.amazonaws.com"], "ecr-login");
|
||||
|
||||
assert_eq!(config.creds_store, Some("osxkeychain".to_string()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,16 +31,25 @@ fn test_auth_integration_with_docker_config() -> Result<()> {
|
|||
// Set HOME to temp directory
|
||||
std::env::set_var("HOME", temp_dir.path());
|
||||
|
||||
// TODO: Implement actual credential resolution from Docker config
|
||||
// For now, all auth resolves to anonymous until we implement the actual credential logic
|
||||
|
||||
// Test GitHub Container Registry auth (currently returns anonymous)
|
||||
// Test GitHub Container Registry auth (should resolve from config)
|
||||
let ghcr_auth = resolve_auth("ghcr.io/user/image:tag")?;
|
||||
assert!(matches!(ghcr_auth, RegistryAuth::Anonymous));
|
||||
match ghcr_auth {
|
||||
RegistryAuth::Basic { username, password } => {
|
||||
assert_eq!(username, "test");
|
||||
assert_eq!(password, "test123");
|
||||
}
|
||||
_ => panic!("Expected Basic auth for ghcr.io, got: {:?}", ghcr_auth),
|
||||
}
|
||||
|
||||
// Test Docker Hub auth (currently returns anonymous)
|
||||
// Test Docker Hub auth (should resolve from config)
|
||||
let docker_auth = resolve_auth("docker.io/library/ubuntu:latest")?;
|
||||
assert!(matches!(docker_auth, RegistryAuth::Anonymous));
|
||||
match docker_auth {
|
||||
RegistryAuth::Basic { username, password } => {
|
||||
assert_eq!(username, "testuser");
|
||||
assert_eq!(password, "testpass");
|
||||
}
|
||||
_ => panic!("Expected Basic auth for docker.io, got: {:?}", docker_auth),
|
||||
}
|
||||
|
||||
// Test unknown registry returns anonymous
|
||||
let unknown_auth = resolve_auth("unknown.registry.io/image:tag")?;
|
||||
|
|
|
|||
|
|
@ -100,10 +100,15 @@ fn test_resolve_auth_from_config() -> Result<()> {
|
|||
env::remove_var("XDG_RUNTIME_DIR");
|
||||
env::set_var("HOME", tmp_dir.path());
|
||||
|
||||
// TODO: Implement actual credential resolution from Docker config
|
||||
// For now, should resolve to anonymous auth
|
||||
// Should resolve credentials from config
|
||||
let auth = resolve_auth("test.registry.io/myimage")?;
|
||||
assert!(matches!(auth, RegistryAuth::Anonymous));
|
||||
match auth {
|
||||
RegistryAuth::Basic { username, password } => {
|
||||
assert_eq!(username, "testuser");
|
||||
assert_eq!(password, "testpass");
|
||||
}
|
||||
_ => panic!("Expected Basic auth for test.registry.io, got: {:?}", auth),
|
||||
}
|
||||
|
||||
// Restore env vars
|
||||
if let Some(val) = old_docker_config {
|
||||
|
|
@ -158,10 +163,14 @@ fn test_resolve_auth_bearer_token() -> Result<()> {
|
|||
env::remove_var("XDG_RUNTIME_DIR");
|
||||
env::set_var("HOME", tmp_dir.path());
|
||||
|
||||
// TODO: Implement actual credential resolution from Docker config
|
||||
// For now, should resolve to anonymous auth
|
||||
// Should resolve bearer token from config
|
||||
let auth = resolve_auth("ghcr.io/user/image")?;
|
||||
assert!(matches!(auth, RegistryAuth::Anonymous));
|
||||
match auth {
|
||||
RegistryAuth::Bearer { token } => {
|
||||
assert_eq!(token, "test-bearer-token");
|
||||
}
|
||||
_ => panic!("Expected Bearer token auth for ghcr.io, got: {:?}", auth),
|
||||
}
|
||||
|
||||
// Restore env vars
|
||||
if let Some(val) = old_docker_config {
|
||||
|
|
|
|||
34
tests/testdata/auth_base64.txt
vendored
Normal file
34
tests/testdata/auth_base64.txt
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Test base64 encoded auth from Docker config
|
||||
|
||||
# Create a test project
|
||||
-- Cargo.toml --
|
||||
[package]
|
||||
name = "auth-b64-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
-- src/main.rs --
|
||||
fn main() {
|
||||
println!("Testing base64 auth");
|
||||
}
|
||||
|
||||
# Create Docker config with base64 auth (testuser:testpass = dGVzdHVzZXI6dGVzdHBhc3M=)
|
||||
mkdir .docker
|
||||
-- .docker/config.json --
|
||||
{
|
||||
"auths": {
|
||||
"ghcr.io": {
|
||||
"auth": "dGVzdHVzZXI6dGVzdHBhc3M="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Set HOME to use our config
|
||||
env HOME=$WORK
|
||||
env RUST_LOG=debug
|
||||
|
||||
# Try to build (will fail at registry, but auth should be resolved)
|
||||
! exec ./krust build --platform linux/amd64 --image ghcr.io/user/app:latest .
|
||||
stderr 'Resolving auth'
|
||||
stderr 'ghcr.io'
|
||||
62
tests/testdata/auth_credential_helper.txt
vendored
Normal file
62
tests/testdata/auth_credential_helper.txt
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Test credential helper execution
|
||||
|
||||
# Create a mock credential helper that logs when it's called
|
||||
-- docker-credential-mock --
|
||||
#!/bin/sh
|
||||
# Mock credential helper that logs execution and returns test credentials
|
||||
|
||||
# Log that we were called
|
||||
echo "MOCK_HELPER_CALLED" >&2
|
||||
|
||||
case "$1" in
|
||||
get)
|
||||
# Read registry from stdin
|
||||
read registry
|
||||
echo "MOCK_HELPER_REGISTRY=$registry" >&2
|
||||
|
||||
# Return credentials
|
||||
cat <<EOF
|
||||
{
|
||||
"Username": "helper-user",
|
||||
"Secret": "helper-pass"
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
-- Cargo.toml --
|
||||
[package]
|
||||
name = "cred-helper-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
-- src/main.rs --
|
||||
fn main() {
|
||||
println!("Testing credential helper");
|
||||
}
|
||||
|
||||
# Make the helper executable
|
||||
chmod +x docker-credential-mock
|
||||
|
||||
# Create Docker config that uses our mock helper
|
||||
mkdir .docker
|
||||
-- .docker/config.json --
|
||||
{
|
||||
"credHelpers": {
|
||||
"mock.registry.io": "mock"
|
||||
}
|
||||
}
|
||||
|
||||
# Add current dir to PATH and set HOME
|
||||
env HOME=$WORK
|
||||
env PATH=$WORK:$PATH
|
||||
env RUST_LOG=debug
|
||||
|
||||
# Try to push to mock.registry.io (will fail at network level, but should call helper)
|
||||
# Using --no-push to avoid actual network call
|
||||
! exec ./krust build --platform linux/amd64 --image mock.registry.io/test:latest .
|
||||
# Should see the helper being attempted
|
||||
stderr 'credential helper'
|
||||
36
tests/testdata/auth_docker_config_basic.txt
vendored
Normal file
36
tests/testdata/auth_docker_config_basic.txt
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Test basic auth from Docker config
|
||||
|
||||
# Create a test project
|
||||
-- Cargo.toml --
|
||||
[package]
|
||||
name = "auth-basic-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
-- src/main.rs --
|
||||
fn main() {
|
||||
println!("Testing basic auth");
|
||||
}
|
||||
|
||||
# Create Docker config with username/password
|
||||
mkdir .docker
|
||||
-- .docker/config.json --
|
||||
{
|
||||
"auths": {
|
||||
"test.example.com": {
|
||||
"username": "myuser",
|
||||
"password": "mypass"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Set HOME to use our config
|
||||
env HOME=$WORK
|
||||
env RUST_LOG=debug
|
||||
|
||||
# Try to build and push (will fail at registry level, but auth should be resolved)
|
||||
# We can verify auth was read from config in debug logs
|
||||
! exec ./krust build --platform linux/amd64 --image test.example.com/app:latest .
|
||||
stderr 'Resolving auth'
|
||||
stderr 'test.example.com'
|
||||
Loading…
Add table
Add a link
Reference in a new issue