mirror of
https://github.com/imjasonh/krust
synced 2026-07-22 16:01:49 +00:00
Merge branch 'main' into dependabot/github_actions/dtolnay/rust-toolchain-f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561
This commit is contained in:
commit
31e5c5a5a3
9 changed files with 495 additions and 342 deletions
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
os: [ubuntu-latest, ubuntu-24.04-arm]
|
os: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
rust: [stable, beta]
|
rust: [stable, beta]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
- uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # master
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.rust }}
|
toolchain: ${{ matrix.rust }}
|
||||||
|
|
@ -30,17 +30,17 @@ jobs:
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu gcc-x86-64-linux-gnu
|
sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu gcc-x86-64-linux-gnu
|
||||||
- name: Cache cargo registry
|
- name: Cache cargo registry
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/git
|
path: ~/.cargo/git
|
||||||
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||||
- name: Cache cargo build
|
- name: Cache cargo build
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v4
|
||||||
with:
|
with:
|
||||||
path: target
|
path: target
|
||||||
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ sha256 = "1.5"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
flate2 = "1.0"
|
flate2 = "1.0"
|
||||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
reqwest = { version = "0.13", features = ["json", "stream"] }
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
yaml-rust2 = "0.10"
|
yaml-rust2 = "0.11"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
dirs = "6.0"
|
dirs = "6.0"
|
||||||
|
|
|
||||||
|
|
@ -205,25 +205,25 @@ mod unit_tests {
|
||||||
|
|
||||||
// Test anonymous
|
// Test anonymous
|
||||||
let auth = AuthConfig::anonymous();
|
let auth = AuthConfig::anonymous();
|
||||||
matches!(auth.to_registry_auth(), RegistryAuth::Anonymous);
|
assert!(matches!(auth.to_registry_auth(), RegistryAuth::Anonymous));
|
||||||
|
|
||||||
// Test basic auth
|
// Test basic auth
|
||||||
let auth = AuthConfig::new("user".to_string(), "pass".to_string());
|
let auth = AuthConfig::new("user".to_string(), "pass".to_string());
|
||||||
matches!(
|
assert!(matches!(
|
||||||
auth.to_registry_auth(),
|
auth.to_registry_auth(),
|
||||||
RegistryAuth::Basic { username, password }
|
RegistryAuth::Basic { ref username, ref password }
|
||||||
if username == "user" && password == "pass"
|
if username == "user" && password == "pass"
|
||||||
);
|
));
|
||||||
|
|
||||||
// Test bearer token
|
// Test bearer token
|
||||||
let auth = AuthConfig {
|
let auth = AuthConfig {
|
||||||
registry_token: Some("token123".to_string()),
|
registry_token: Some("token123".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
matches!(
|
assert!(matches!(
|
||||||
auth.to_registry_auth(),
|
auth.to_registry_auth(),
|
||||||
RegistryAuth::Bearer { token }
|
RegistryAuth::Bearer { ref token }
|
||||||
if token == "token123"
|
if token == "token123"
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,4 +108,75 @@ version = "0.1.0"
|
||||||
|
|
||||||
assert_eq!(builder.cargo_args, vec!["--features", "foo"]);
|
assert_eq!(builder.cargo_args, vec!["--features", "foo"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_binary_name_with_bin_arg() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let builder = RustBuilder::new(dir.path(), "x86_64-unknown-linux-musl")
|
||||||
|
.with_cargo_args(vec!["--bin".to_string(), "my-binary".to_string()]);
|
||||||
|
let name = builder.get_binary_name().unwrap();
|
||||||
|
assert_eq!(name, "my-binary");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_binary_name_with_example_arg() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let builder = RustBuilder::new(dir.path(), "x86_64-unknown-linux-musl")
|
||||||
|
.with_cargo_args(vec!["--example".to_string(), "my-example".to_string()]);
|
||||||
|
let name = builder.get_binary_name().unwrap();
|
||||||
|
assert_eq!(name, "my-example");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_binary_name_bin_arg_at_end_without_value() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
// --bin at end with no following value should fall through to Cargo.toml
|
||||||
|
let cargo_toml = dir.path().join("Cargo.toml");
|
||||||
|
fs::write(
|
||||||
|
&cargo_toml,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "fallback-name"
|
||||||
|
version = "0.1.0"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let builder = RustBuilder::new(dir.path(), "x86_64-unknown-linux-musl")
|
||||||
|
.with_cargo_args(vec!["--bin".to_string()]);
|
||||||
|
let name = builder.get_binary_name().unwrap();
|
||||||
|
assert_eq!(name, "fallback-name");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_binary_subdir_with_example() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let builder = RustBuilder::new(dir.path(), "x86_64-unknown-linux-musl")
|
||||||
|
.with_cargo_args(vec!["--example".to_string(), "my-example".to_string()]);
|
||||||
|
assert_eq!(builder.get_binary_subdir(), Some("examples"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_binary_subdir_without_example() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let builder = RustBuilder::new(dir.path(), "x86_64-unknown-linux-musl");
|
||||||
|
assert_eq!(builder.get_binary_subdir(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_binary_subdir_with_bin() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let builder = RustBuilder::new(dir.path(), "x86_64-unknown-linux-musl")
|
||||||
|
.with_cargo_args(vec!["--bin".to_string(), "my-bin".to_string()]);
|
||||||
|
assert_eq!(builder.get_binary_subdir(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_binary_subdir_example_at_end_without_value() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
// --example at end with no value should not match (needs i+1 < len)
|
||||||
|
let builder = RustBuilder::new(dir.path(), "x86_64-unknown-linux-musl")
|
||||||
|
.with_cargo_args(vec!["--example".to_string()]);
|
||||||
|
assert_eq!(builder.get_binary_subdir(), None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,6 @@ pub enum Commands {
|
||||||
cargo_args: Vec<String>,
|
cargo_args: Vec<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Push a built image to a container registry
|
|
||||||
Push {
|
|
||||||
/// Image reference to push
|
|
||||||
image: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Resolve krust:// references in YAML files
|
/// Resolve krust:// references in YAML files
|
||||||
Resolve {
|
Resolve {
|
||||||
/// Path to YAML file or directory containing YAML files
|
/// Path to YAML file or directory containing YAML files
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ pub struct Config {
|
||||||
|
|
||||||
/// Registry authentication configuration
|
/// Registry authentication configuration
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub registries: HashMap<String, RegistryAuth>,
|
pub registries: HashMap<String, RegistryCredential>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
|
@ -38,7 +38,7 @@ pub struct BuildConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RegistryAuth {
|
pub struct RegistryCredential {
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub auth: Option<String>,
|
pub auth: Option<String>,
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,20 @@ pub struct Descriptor {
|
||||||
pub digest: String,
|
pub digest: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse a platform string like "linux/amd64" or "linux/arm/v7" into (os, arch, variant).
|
||||||
|
pub fn parse_platform_string(platform: &str) -> Result<(String, String, Option<String>)> {
|
||||||
|
let parts: Vec<&str> = platform.split('/').collect();
|
||||||
|
match parts.len() {
|
||||||
|
2 => Ok((parts[0].to_string(), parts[1].to_string(), None)),
|
||||||
|
3 => Ok((
|
||||||
|
parts[0].to_string(),
|
||||||
|
parts[1].to_string(),
|
||||||
|
Some(parts[2].to_string()),
|
||||||
|
)),
|
||||||
|
_ => anyhow::bail!("Invalid platform format: {}", platform),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ImageBuilder {
|
pub struct ImageBuilder {
|
||||||
binary_path: PathBuf,
|
binary_path: PathBuf,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
@ -104,7 +118,7 @@ impl ImageBuilder {
|
||||||
) -> Result<(Vec<u8>, Vec<u8>, Manifest)> {
|
) -> Result<(Vec<u8>, Vec<u8>, Manifest)> {
|
||||||
info!("Building container image");
|
info!("Building container image");
|
||||||
|
|
||||||
let (_os, _arch) = self.parse_platform()?;
|
let (_os, _arch, _variant) = self.parse_platform()?;
|
||||||
|
|
||||||
// Fetch base image data
|
// Fetch base image data
|
||||||
info!(
|
info!(
|
||||||
|
|
@ -133,7 +147,7 @@ impl ImageBuilder {
|
||||||
|
|
||||||
// Add the application layer
|
// Add the application layer
|
||||||
all_layers.push(Descriptor {
|
all_layers.push(Descriptor {
|
||||||
media_type: "application/vnd.docker.image.rootfs.diff.tar.gzip".to_string(),
|
media_type: "application/vnd.oci.image.layer.v1.tar+gzip".to_string(),
|
||||||
size: app_layer_size,
|
size: app_layer_size,
|
||||||
digest: app_layer_digest,
|
digest: app_layer_digest,
|
||||||
});
|
});
|
||||||
|
|
@ -147,9 +161,9 @@ impl ImageBuilder {
|
||||||
// Create manifest
|
// Create manifest
|
||||||
let manifest = Manifest {
|
let manifest = Manifest {
|
||||||
schema_version: 2,
|
schema_version: 2,
|
||||||
media_type: "application/vnd.docker.distribution.manifest.v2+json".to_string(),
|
media_type: "application/vnd.oci.image.manifest.v1+json".to_string(),
|
||||||
config: Descriptor {
|
config: Descriptor {
|
||||||
media_type: "application/vnd.docker.container.image.v1+json".to_string(),
|
media_type: "application/vnd.oci.image.config.v1+json".to_string(),
|
||||||
size: config_size,
|
size: config_size,
|
||||||
digest: config_digest,
|
digest: config_digest,
|
||||||
},
|
},
|
||||||
|
|
@ -159,12 +173,8 @@ impl ImageBuilder {
|
||||||
Ok((config_data, app_layer_data, manifest))
|
Ok((config_data, app_layer_data, manifest))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_platform(&self) -> Result<(String, String)> {
|
fn parse_platform(&self) -> Result<(String, String, Option<String>)> {
|
||||||
let parts: Vec<&str> = self.platform.split('/').collect();
|
parse_platform_string(&self.platform)
|
||||||
if parts.len() != 2 {
|
|
||||||
anyhow::bail!("Invalid platform format: {}", self.platform);
|
|
||||||
}
|
|
||||||
Ok((parts[0].to_string(), parts[1].to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_layer(&self) -> Result<(Vec<u8>, String)> {
|
fn create_layer(&self) -> Result<(Vec<u8>, String)> {
|
||||||
|
|
@ -264,13 +274,12 @@ mod tests {
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
fn create_test_binary() -> PathBuf {
|
fn create_test_binary() -> (PathBuf, tempfile::TempPath) {
|
||||||
let mut temp_file = NamedTempFile::new().unwrap();
|
let mut temp_file = NamedTempFile::new().unwrap();
|
||||||
temp_file.write_all(b"fake binary content").unwrap();
|
temp_file.write_all(b"fake binary content").unwrap();
|
||||||
let path = temp_file.path().to_path_buf();
|
let temp_path = temp_file.into_temp_path();
|
||||||
// Keep the file alive by converting to a regular file
|
let path = temp_path.to_path_buf();
|
||||||
std::fs::copy(&path, &path).unwrap();
|
(path, temp_path)
|
||||||
path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_base_image_config() -> ImageConfig {
|
fn create_base_image_config() -> ImageConfig {
|
||||||
|
|
@ -318,14 +327,29 @@ mod tests {
|
||||||
"linux/amd64".to_string(),
|
"linux/amd64".to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (os, arch) = builder.parse_platform().unwrap();
|
let (os, arch, variant) = builder.parse_platform().unwrap();
|
||||||
assert_eq!(os, "linux");
|
assert_eq!(os, "linux");
|
||||||
assert_eq!(arch, "amd64");
|
assert_eq!(arch, "amd64");
|
||||||
|
assert_eq!(variant, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_platform_with_variant() {
|
||||||
|
let builder = ImageBuilder::new(
|
||||||
|
PathBuf::from("/tmp/test"),
|
||||||
|
"test-base".to_string(),
|
||||||
|
"linux/arm/v7".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (os, arch, variant) = builder.parse_platform().unwrap();
|
||||||
|
assert_eq!(os, "linux");
|
||||||
|
assert_eq!(arch, "arm");
|
||||||
|
assert_eq!(variant, Some("v7".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_layered_config_preserves_base_environment() {
|
fn test_create_layered_config_preserves_base_environment() {
|
||||||
let binary_path = create_test_binary();
|
let (binary_path, _guard) = create_test_binary();
|
||||||
let builder = ImageBuilder::new(
|
let builder = ImageBuilder::new(
|
||||||
binary_path,
|
binary_path,
|
||||||
"test-base".to_string(),
|
"test-base".to_string(),
|
||||||
|
|
@ -359,7 +383,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_layered_config_combines_diff_ids() {
|
fn test_create_layered_config_combines_diff_ids() {
|
||||||
let binary_path = create_test_binary();
|
let (binary_path, _guard) = create_test_binary();
|
||||||
let builder = ImageBuilder::new(
|
let builder = ImageBuilder::new(
|
||||||
binary_path,
|
binary_path,
|
||||||
"test-base".to_string(),
|
"test-base".to_string(),
|
||||||
|
|
@ -382,7 +406,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_layered_config_combines_history() {
|
fn test_create_layered_config_combines_history() {
|
||||||
let binary_path = create_test_binary();
|
let (binary_path, _guard) = create_test_binary();
|
||||||
let builder = ImageBuilder::new(
|
let builder = ImageBuilder::new(
|
||||||
binary_path,
|
binary_path,
|
||||||
"test-base".to_string(),
|
"test-base".to_string(),
|
||||||
|
|
@ -524,7 +548,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_layered_config_adds_path_if_missing() {
|
fn test_create_layered_config_adds_path_if_missing() {
|
||||||
let binary_path = create_test_binary();
|
let (binary_path, _guard) = create_test_binary();
|
||||||
let builder = ImageBuilder::new(
|
let builder = ImageBuilder::new(
|
||||||
binary_path,
|
binary_path,
|
||||||
"test-base".to_string(),
|
"test-base".to_string(),
|
||||||
|
|
@ -546,7 +570,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_layered_config_sets_cmd() {
|
fn test_create_layered_config_sets_cmd() {
|
||||||
let binary_path = create_test_binary();
|
let (binary_path, _guard) = create_test_binary();
|
||||||
let builder = ImageBuilder::new(
|
let builder = ImageBuilder::new(
|
||||||
binary_path.clone(),
|
binary_path.clone(),
|
||||||
"test-base".to_string(),
|
"test-base".to_string(),
|
||||||
|
|
|
||||||
335
src/main.rs
335
src/main.rs
|
|
@ -5,14 +5,14 @@ use krust::{
|
||||||
builder::{get_rust_target_triple, RustBuilder},
|
builder::{get_rust_target_triple, RustBuilder},
|
||||||
cli::{Cli, Commands},
|
cli::{Cli, Commands},
|
||||||
config::Config,
|
config::Config,
|
||||||
image::ImageBuilder,
|
image::{parse_platform_string, ImageBuilder},
|
||||||
manifest::{ManifestDescriptor, Platform},
|
manifest::{ManifestDescriptor, Platform},
|
||||||
registry::RegistryClient,
|
registry::RegistryClient,
|
||||||
resolve::{find_krust_references, read_yaml_files, replace_krust_references},
|
resolve::{find_krust_references, read_yaml_files, replace_krust_references},
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tracing::{error, info};
|
use tracing::info;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -102,98 +102,17 @@ async fn main() -> Result<()> {
|
||||||
let no_push_flag = no_push;
|
let no_push_flag = no_push;
|
||||||
|
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
info!("Building for platform: {}", platform_str);
|
let descriptor = build_and_push_platform(
|
||||||
|
&project_path,
|
||||||
|
&base_image,
|
||||||
|
&target_repo,
|
||||||
|
&platform_str,
|
||||||
|
cargo_args,
|
||||||
|
!no_push_flag,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Build the Rust binary for this platform
|
Ok::<_, anyhow::Error>(descriptor)
|
||||||
let target = get_rust_target_triple(&platform_str)?;
|
|
||||||
let builder =
|
|
||||||
RustBuilder::new(&project_path, &target).with_cargo_args(cargo_args);
|
|
||||||
|
|
||||||
let build_result = builder.build()?;
|
|
||||||
|
|
||||||
// Build container image for this platform
|
|
||||||
let image_builder = ImageBuilder::new(
|
|
||||||
build_result.binary_path,
|
|
||||||
base_image.clone(),
|
|
||||||
platform_str.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a registry client for this task
|
|
||||||
let mut registry_client = RegistryClient::new()?;
|
|
||||||
|
|
||||||
// Always use layered approach - registry layer will handle cross-registry blob copying
|
|
||||||
let base_auth = resolve_auth(&base_image)?;
|
|
||||||
let (config_data, layer_data, manifest) = image_builder
|
|
||||||
.build(&mut registry_client, &base_auth)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Push platform-specific image if not --no-push
|
|
||||||
let manifest_descriptor = if !no_push_flag {
|
|
||||||
info!("Pushing image for platform: {}", platform_str);
|
|
||||||
|
|
||||||
// Get auth for the target registry
|
|
||||||
let push_auth = resolve_auth(&target_repo)?;
|
|
||||||
|
|
||||||
// Get the media type of the application layer (last layer in manifest)
|
|
||||||
let app_layer_media_type = manifest
|
|
||||||
.layers
|
|
||||||
.last()
|
|
||||||
.map(|l| l.media_type.clone())
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
"application/vnd.docker.image.rootfs.diff.tar.gzip".to_string()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Push layered image by digest only (no tag)
|
|
||||||
// This will be referenced by digest in the final manifest list
|
|
||||||
let (digest_ref, manifest_size) = registry_client
|
|
||||||
.push_layered_image(
|
|
||||||
&target_repo,
|
|
||||||
config_data,
|
|
||||||
layer_data,
|
|
||||||
app_layer_media_type,
|
|
||||||
&manifest,
|
|
||||||
&push_auth,
|
|
||||||
&base_image,
|
|
||||||
&base_auth,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Parse platform string
|
|
||||||
let parts: Vec<&str> = platform_str.split('/').collect();
|
|
||||||
let (os, arch) = if parts.len() >= 2 {
|
|
||||||
(parts[0].to_string(), parts[1].to_string())
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Invalid platform format: {}",
|
|
||||||
platform_str
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract just the digest from the full reference
|
|
||||||
let digest = digest_ref.split('@').next_back().unwrap_or("").to_string();
|
|
||||||
|
|
||||||
info!("Pushed platform image to: {}", digest_ref);
|
|
||||||
|
|
||||||
// Return manifest descriptor
|
|
||||||
info!(
|
|
||||||
"Adding manifest to list - platform: {}/{}, digest: {}, size: {}",
|
|
||||||
os, arch, digest, manifest_size
|
|
||||||
);
|
|
||||||
Some(ManifestDescriptor {
|
|
||||||
media_type: "application/vnd.oci.image.manifest.v1+json".to_string(),
|
|
||||||
size: manifest_size as i64,
|
|
||||||
digest,
|
|
||||||
platform: Platform {
|
|
||||||
architecture: arch,
|
|
||||||
os,
|
|
||||||
variant: None,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>(manifest_descriptor)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tasks.push(task);
|
tasks.push(task);
|
||||||
|
|
@ -210,32 +129,16 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
// Always push manifest list if not --no-push (even for single platform)
|
// Always push manifest list if not --no-push (even for single platform)
|
||||||
if !no_push {
|
if !no_push {
|
||||||
info!("Creating and pushing manifest list...");
|
let image_ref = push_tagged_manifest_list(
|
||||||
|
&mut registry_client,
|
||||||
// Determine the target for the manifest list
|
&target_repo,
|
||||||
let has_tag = tag.is_some();
|
manifest_descriptors,
|
||||||
let manifest_target = if let Some(tag_name) = &tag {
|
&tag,
|
||||||
// If --tag is specified, push to that tag
|
)
|
||||||
format!("{}:{}", target_repo, tag_name)
|
.await?;
|
||||||
} else {
|
|
||||||
// If no tag specified, push digest-only (no tag)
|
|
||||||
target_repo.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get auth for the final image push
|
|
||||||
let final_auth = resolve_auth(&manifest_target)?;
|
|
||||||
|
|
||||||
let manifest_list_ref = registry_client
|
|
||||||
.push_manifest_list(
|
|
||||||
&manifest_target,
|
|
||||||
manifest_descriptors,
|
|
||||||
&final_auth,
|
|
||||||
has_tag,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Output the manifest list reference (always by digest)
|
// Output the manifest list reference (always by digest)
|
||||||
println!("{}", manifest_list_ref);
|
println!("{}", image_ref);
|
||||||
} else {
|
} else {
|
||||||
info!(
|
info!(
|
||||||
"Successfully built image for {} platform(s)",
|
"Successfully built image for {} platform(s)",
|
||||||
|
|
@ -244,11 +147,6 @@ async fn main() -> Result<()> {
|
||||||
info!("Skipping push (--no-push specified)");
|
info!("Skipping push (--no-push specified)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Push { image } => {
|
|
||||||
let _ = image;
|
|
||||||
error!("Push command not yet implemented");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
Commands::Resolve {
|
Commands::Resolve {
|
||||||
filenames,
|
filenames,
|
||||||
platform,
|
platform,
|
||||||
|
|
@ -306,6 +204,104 @@ async fn main() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a binary and push an image for a single platform.
|
||||||
|
/// Returns a ManifestDescriptor if push is true, None otherwise.
|
||||||
|
async fn build_and_push_platform(
|
||||||
|
project_path: &Path,
|
||||||
|
base_image: &str,
|
||||||
|
target_repo: &str,
|
||||||
|
platform_str: &str,
|
||||||
|
cargo_args: Vec<String>,
|
||||||
|
push: bool,
|
||||||
|
) -> Result<Option<ManifestDescriptor>> {
|
||||||
|
info!("Building for platform: {}", platform_str);
|
||||||
|
|
||||||
|
// Build the Rust binary for this platform
|
||||||
|
let target = get_rust_target_triple(platform_str)?;
|
||||||
|
let builder = RustBuilder::new(project_path, &target).with_cargo_args(cargo_args);
|
||||||
|
let build_result = builder.build()?;
|
||||||
|
|
||||||
|
// Build container image for this platform
|
||||||
|
let image_builder = ImageBuilder::new(
|
||||||
|
build_result.binary_path,
|
||||||
|
base_image.to_string(),
|
||||||
|
platform_str.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a registry client for this task
|
||||||
|
let mut registry_client = RegistryClient::new()?;
|
||||||
|
|
||||||
|
let base_auth = resolve_auth(base_image)?;
|
||||||
|
let (config_data, layer_data, manifest) = image_builder
|
||||||
|
.build(&mut registry_client, &base_auth)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !push {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Pushing image for platform: {}", platform_str);
|
||||||
|
|
||||||
|
let push_auth = resolve_auth(target_repo)?;
|
||||||
|
let app_layer_media_type = manifest
|
||||||
|
.layers
|
||||||
|
.last()
|
||||||
|
.map(|l| l.media_type.clone())
|
||||||
|
.unwrap_or_else(|| "application/vnd.oci.image.layer.v1.tar+gzip".to_string());
|
||||||
|
|
||||||
|
let (digest_ref, manifest_size) = registry_client
|
||||||
|
.push_layered_image(
|
||||||
|
target_repo,
|
||||||
|
config_data,
|
||||||
|
layer_data,
|
||||||
|
app_layer_media_type,
|
||||||
|
&manifest,
|
||||||
|
&push_auth,
|
||||||
|
base_image,
|
||||||
|
&base_auth,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (os, arch, variant) = parse_platform_string(platform_str)?;
|
||||||
|
let digest = digest_ref.split('@').next_back().unwrap_or("").to_string();
|
||||||
|
|
||||||
|
info!("Pushed platform image: {} ({})", digest_ref, platform_str);
|
||||||
|
|
||||||
|
Ok(Some(ManifestDescriptor {
|
||||||
|
media_type: "application/vnd.oci.image.manifest.v1+json".to_string(),
|
||||||
|
size: manifest_size as i64,
|
||||||
|
digest,
|
||||||
|
platform: Platform {
|
||||||
|
architecture: arch,
|
||||||
|
os,
|
||||||
|
variant,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a manifest list, optionally tagged.
|
||||||
|
async fn push_tagged_manifest_list(
|
||||||
|
registry_client: &mut RegistryClient,
|
||||||
|
target_repo: &str,
|
||||||
|
manifest_descriptors: Vec<ManifestDescriptor>,
|
||||||
|
tag: &Option<String>,
|
||||||
|
) -> Result<String> {
|
||||||
|
info!("Creating and pushing manifest list...");
|
||||||
|
|
||||||
|
let has_tag = tag.is_some();
|
||||||
|
let manifest_target = if let Some(tag_name) = tag {
|
||||||
|
format!("{}:{}", target_repo, tag_name)
|
||||||
|
} else {
|
||||||
|
target_repo.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let final_auth = resolve_auth(&manifest_target)?;
|
||||||
|
|
||||||
|
registry_client
|
||||||
|
.push_manifest_list(&manifest_target, manifest_descriptors, &final_auth, has_tag)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve krust:// references in YAML files
|
/// Resolve krust:// references in YAML files
|
||||||
async fn resolve_yaml_files(
|
async fn resolve_yaml_files(
|
||||||
filenames: Vec<PathBuf>,
|
filenames: Vec<PathBuf>,
|
||||||
|
|
@ -341,107 +337,50 @@ async fn resolve_yaml_files(
|
||||||
for krust_path in all_references {
|
for krust_path in all_references {
|
||||||
info!("Building image for: krust://{}", krust_path);
|
info!("Building image for: krust://{}", krust_path);
|
||||||
|
|
||||||
// Resolve the path (could be relative like ./example/hello-krust)
|
|
||||||
let project_path = PathBuf::from(&krust_path);
|
let project_path = PathBuf::from(&krust_path);
|
||||||
|
|
||||||
if !project_path.exists() {
|
if !project_path.exists() {
|
||||||
anyhow::bail!("Path does not exist: {}", krust_path);
|
anyhow::bail!("Path does not exist: {}", krust_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get project name
|
|
||||||
let project_name = get_project_name(&project_path)?;
|
let project_name = get_project_name(&project_path)?;
|
||||||
let target_repo = format!("{}/{}", repo, project_name);
|
let target_repo = format!("{}/{}", repo, project_name);
|
||||||
|
|
||||||
// Load project config
|
|
||||||
let project_config = Config::load_project_config(&project_path)?;
|
let project_config = Config::load_project_config(&project_path)?;
|
||||||
let base_image = project_config
|
let base_image = project_config
|
||||||
.base_image
|
.base_image
|
||||||
.unwrap_or(config.base_image.clone());
|
.unwrap_or(config.base_image.clone());
|
||||||
|
|
||||||
// Determine platforms
|
|
||||||
let platforms = if let Some(ref platforms) = platform {
|
let platforms = if let Some(ref platforms) = platform {
|
||||||
platforms.clone()
|
platforms.clone()
|
||||||
} else {
|
} else {
|
||||||
// Default to linux/amd64 for resolve
|
|
||||||
vec!["linux/amd64".to_string()]
|
vec!["linux/amd64".to_string()]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build for each platform
|
// Build for each platform
|
||||||
let mut manifest_descriptors = Vec::new();
|
let mut manifest_descriptors = Vec::new();
|
||||||
|
|
||||||
for platform_str in &platforms {
|
for platform_str in &platforms {
|
||||||
info!("Building {} for platform: {}", krust_path, platform_str);
|
if let Some(descriptor) = build_and_push_platform(
|
||||||
|
&project_path,
|
||||||
let target = get_rust_target_triple(platform_str)?;
|
&base_image,
|
||||||
let builder = RustBuilder::new(&project_path, &target);
|
&target_repo,
|
||||||
let build_result = builder.build()?;
|
platform_str,
|
||||||
|
Vec::new(),
|
||||||
let image_builder = ImageBuilder::new(
|
true,
|
||||||
build_result.binary_path,
|
)
|
||||||
base_image.clone(),
|
.await?
|
||||||
platform_str.clone(),
|
{
|
||||||
);
|
manifest_descriptors.push(descriptor);
|
||||||
|
}
|
||||||
let base_auth = resolve_auth(&base_image)?;
|
|
||||||
let (config_data, layer_data, manifest) = image_builder
|
|
||||||
.build(&mut registry_client, &base_auth)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Push the image
|
|
||||||
let push_auth = resolve_auth(&target_repo)?;
|
|
||||||
let app_layer_media_type = manifest
|
|
||||||
.layers
|
|
||||||
.last()
|
|
||||||
.map(|l| l.media_type.clone())
|
|
||||||
.unwrap_or_else(|| "application/vnd.docker.image.rootfs.diff.tar.gzip".to_string());
|
|
||||||
|
|
||||||
let (digest_ref, manifest_size) = registry_client
|
|
||||||
.push_layered_image(
|
|
||||||
&target_repo,
|
|
||||||
config_data,
|
|
||||||
layer_data,
|
|
||||||
app_layer_media_type,
|
|
||||||
&manifest,
|
|
||||||
&push_auth,
|
|
||||||
&base_image,
|
|
||||||
&base_auth,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Parse platform
|
|
||||||
let parts: Vec<&str> = platform_str.split('/').collect();
|
|
||||||
let (os, arch) = if parts.len() >= 2 {
|
|
||||||
(parts[0].to_string(), parts[1].to_string())
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!("Invalid platform format: {}", platform_str));
|
|
||||||
};
|
|
||||||
|
|
||||||
let digest = digest_ref.split('@').next_back().unwrap_or("").to_string();
|
|
||||||
|
|
||||||
manifest_descriptors.push(ManifestDescriptor {
|
|
||||||
media_type: "application/vnd.oci.image.manifest.v1+json".to_string(),
|
|
||||||
size: manifest_size as i64,
|
|
||||||
digest,
|
|
||||||
platform: Platform {
|
|
||||||
architecture: arch,
|
|
||||||
os,
|
|
||||||
variant: None,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push manifest list
|
// Push manifest list
|
||||||
let has_tag = tag.is_some();
|
let image_ref = push_tagged_manifest_list(
|
||||||
let manifest_target = if let Some(tag_name) = &tag {
|
&mut registry_client,
|
||||||
format!("{}:{}", target_repo, tag_name)
|
&target_repo,
|
||||||
} else {
|
manifest_descriptors,
|
||||||
target_repo.clone()
|
&tag,
|
||||||
};
|
)
|
||||||
|
.await?;
|
||||||
let final_auth = resolve_auth(&manifest_target)?;
|
|
||||||
let image_ref = registry_client
|
|
||||||
.push_manifest_list(&manifest_target, manifest_descriptors, &final_auth, has_tag)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("Resolved krust://{} -> {}", krust_path, image_ref);
|
info!("Resolved krust://{} -> {}", krust_path, image_ref);
|
||||||
replacements.insert(krust_path, image_ref);
|
replacements.insert(krust_path, image_ref);
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,7 @@ pub struct OciImageManifest {
|
||||||
pub annotations: Option<HashMap<String, String>>,
|
pub annotations: Option<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
use crate::manifest::Platform;
|
||||||
pub struct Platform {
|
|
||||||
pub architecture: String,
|
|
||||||
pub os: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub variant: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ImageIndexEntry {
|
pub struct ImageIndexEntry {
|
||||||
|
|
@ -185,20 +179,16 @@ impl ImageReference {
|
||||||
|
|
||||||
pub struct RegistryClient {
|
pub struct RegistryClient {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
#[allow(dead_code)]
|
|
||||||
auth_cache: HashMap<String, String>, // registry -> token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegistryClient {
|
impl RegistryClient {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
// Create two clients - one with redirects, one without
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(30))
|
||||||
|
.timeout(std::time::Duration::from_secs(300))
|
||||||
.build()?;
|
.build()?;
|
||||||
Ok(Self {
|
Ok(Self { client })
|
||||||
client,
|
|
||||||
auth_cache: HashMap::new(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a blob exists in the registry using HEAD request
|
/// Check if a blob exists in the registry using HEAD request
|
||||||
|
|
@ -385,10 +375,13 @@ impl RegistryClient {
|
||||||
challenge.scope.clone()
|
challenge.scope.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let token_url = format!(
|
let mut token_url =
|
||||||
"{}?service={}&scope={}",
|
reqwest::Url::parse(&challenge.realm).context("Invalid token realm URL")?;
|
||||||
challenge.realm, challenge.service, scope
|
token_url
|
||||||
);
|
.query_pairs_mut()
|
||||||
|
.append_pair("service", &challenge.service)
|
||||||
|
.append_pair("scope", &scope);
|
||||||
|
let token_url = token_url.to_string();
|
||||||
|
|
||||||
let response = self.client.get(&token_url).send().await?;
|
let response = self.client.get(&token_url).send().await?;
|
||||||
|
|
||||||
|
|
@ -418,10 +411,13 @@ impl RegistryClient {
|
||||||
challenge.scope.clone()
|
challenge.scope.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let token_url = format!(
|
let mut token_url =
|
||||||
"{}?service={}&scope={}",
|
reqwest::Url::parse(&challenge.realm).context("Invalid token realm URL")?;
|
||||||
challenge.realm, challenge.service, scope
|
token_url
|
||||||
);
|
.query_pairs_mut()
|
||||||
|
.append_pair("service", &challenge.service)
|
||||||
|
.append_pair("scope", &scope);
|
||||||
|
let token_url = token_url.to_string();
|
||||||
let auth_header = format!("{}:{}", username, password);
|
let auth_header = format!("{}:{}", username, password);
|
||||||
let encoded_auth = base64::engine::general_purpose::STANDARD.encode(auth_header.as_bytes());
|
let encoded_auth = base64::engine::general_purpose::STANDARD.encode(auth_header.as_bytes());
|
||||||
|
|
||||||
|
|
@ -445,11 +441,23 @@ impl RegistryClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pull a manifest from the registry
|
// Pull a manifest from the registry, optionally filtering by platform
|
||||||
|
// when the manifest is an image index.
|
||||||
pub async fn pull_manifest(
|
pub async fn pull_manifest(
|
||||||
&mut self,
|
&mut self,
|
||||||
image_ref: &str,
|
image_ref: &str,
|
||||||
auth: &RegistryAuth,
|
auth: &RegistryAuth,
|
||||||
|
) -> Result<(OciImageManifest, String)> {
|
||||||
|
self.pull_manifest_for_platform(image_ref, auth, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull a manifest from the registry, selecting the given platform from
|
||||||
|
// an image index if present.
|
||||||
|
async fn pull_manifest_for_platform(
|
||||||
|
&mut self,
|
||||||
|
image_ref: &str,
|
||||||
|
auth: &RegistryAuth,
|
||||||
|
platform: Option<&str>,
|
||||||
) -> Result<(OciImageManifest, String)> {
|
) -> Result<(OciImageManifest, String)> {
|
||||||
debug!("Parsing image reference: {}", image_ref);
|
debug!("Parsing image reference: {}", image_ref);
|
||||||
let reference = ImageReference::parse(image_ref)?;
|
let reference = ImageReference::parse(image_ref)?;
|
||||||
|
|
@ -476,7 +484,7 @@ impl RegistryClient {
|
||||||
|
|
||||||
let mut req = self.client
|
let mut req = self.client
|
||||||
.get(&url)
|
.get(&url)
|
||||||
.header("Accept", "application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json");
|
.header("Accept", "application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json,application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json");
|
||||||
|
|
||||||
if let Some(token) = token {
|
if let Some(token) = token {
|
||||||
req = req.header("Authorization", format!("Bearer {}", token));
|
req = req.header("Authorization", format!("Bearer {}", token));
|
||||||
|
|
@ -488,69 +496,135 @@ impl RegistryClient {
|
||||||
anyhow::bail!("Failed to pull manifest: {}", response.status());
|
anyhow::bail!("Failed to pull manifest: {}", response.status());
|
||||||
}
|
}
|
||||||
|
|
||||||
let digest = response
|
let header_digest = response
|
||||||
.headers()
|
.headers()
|
||||||
.get("docker-content-digest")
|
.get("docker-content-digest")
|
||||||
.and_then(|h| h.to_str().ok())
|
.and_then(|h| h.to_str().ok())
|
||||||
.unwrap_or("")
|
.map(|s| s.to_string());
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let body = response.bytes().await?;
|
let body = response.bytes().await?;
|
||||||
|
|
||||||
|
// Use header digest if available, otherwise compute from body
|
||||||
|
let index_digest =
|
||||||
|
header_digest.unwrap_or_else(|| format!("sha256:{}", sha256::digest(body.as_ref())));
|
||||||
debug!("Manifest response body: {}", String::from_utf8_lossy(&body));
|
debug!("Manifest response body: {}", String::from_utf8_lossy(&body));
|
||||||
|
|
||||||
// Try to parse as either image manifest or image index
|
// Try to parse as either image manifest or image index
|
||||||
let manifest: OciImageManifest = if let Ok(image_manifest) =
|
let (manifest, digest) =
|
||||||
serde_json::from_slice::<OciImageManifest>(&body)
|
if let Ok(image_manifest) = serde_json::from_slice::<OciImageManifest>(&body) {
|
||||||
{
|
// Check if this is actually an image index by looking at the media type
|
||||||
image_manifest
|
if image_manifest.media_type.contains("index")
|
||||||
} else if let Ok(image_index) = serde_json::from_slice::<OciImageIndex>(&body) {
|
|| image_manifest.media_type.contains("manifest.list")
|
||||||
// If it's an image index, we need to find the specific platform manifest
|
{
|
||||||
// For now, just take the first one (this should be enhanced to match platform)
|
// Re-parse as index
|
||||||
if let Some(first_manifest) = image_index.manifests.first() {
|
let image_index: OciImageIndex = serde_json::from_slice(&body)?;
|
||||||
// Pull the platform-specific manifest directly
|
self.select_platform_manifest(&reference, &image_index, auth, platform)
|
||||||
let platform_digest = &first_manifest.digest;
|
.await?
|
||||||
let url = format!(
|
} else {
|
||||||
"https://{}/v2/{}/manifests/{}",
|
(image_manifest, index_digest)
|
||||||
reference.registry, reference.repository, platform_digest
|
|
||||||
);
|
|
||||||
|
|
||||||
debug!("Pulling platform-specific manifest from URL: {}", url);
|
|
||||||
|
|
||||||
let mut req = self.client
|
|
||||||
.get(&url)
|
|
||||||
.header("Accept", "application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json");
|
|
||||||
|
|
||||||
// Re-authenticate for the platform-specific request
|
|
||||||
let platform_token = self
|
|
||||||
.authenticate(&reference.registry, &reference.repository, auth)
|
|
||||||
.await?;
|
|
||||||
if let Some(token) = platform_token {
|
|
||||||
req = req.header("Authorization", format!("Bearer {}", token));
|
|
||||||
}
|
}
|
||||||
|
} else if let Ok(image_index) = serde_json::from_slice::<OciImageIndex>(&body) {
|
||||||
let response = req.send().await?;
|
self.select_platform_manifest(&reference, &image_index, auth, platform)
|
||||||
|
.await?
|
||||||
if !response.status().is_success() {
|
|
||||||
anyhow::bail!("Failed to pull platform manifest: {}", response.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
let platform_body = response.bytes().await?;
|
|
||||||
debug!(
|
|
||||||
"Platform manifest response body: {}",
|
|
||||||
String::from_utf8_lossy(&platform_body)
|
|
||||||
);
|
|
||||||
|
|
||||||
serde_json::from_slice::<OciImageManifest>(&platform_body)?
|
|
||||||
} else {
|
} else {
|
||||||
anyhow::bail!("Image index has no manifests");
|
anyhow::bail!("Response is neither a valid image manifest nor image index");
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
anyhow::bail!("Response is neither a valid image manifest nor image index");
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok((manifest, digest))
|
Ok((manifest, digest))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Select and pull a platform-specific manifest from an image index.
|
||||||
|
/// Returns the manifest and its digest (from the platform-specific response).
|
||||||
|
async fn select_platform_manifest(
|
||||||
|
&mut self,
|
||||||
|
reference: &ImageReference,
|
||||||
|
image_index: &OciImageIndex,
|
||||||
|
auth: &RegistryAuth,
|
||||||
|
platform: Option<&str>,
|
||||||
|
) -> Result<(OciImageManifest, String)> {
|
||||||
|
let selected = if let Some(platform_str) = platform {
|
||||||
|
// Parse the requested platform using the shared parser
|
||||||
|
let (req_os, req_arch, req_variant) =
|
||||||
|
crate::image::parse_platform_string(platform_str)?;
|
||||||
|
|
||||||
|
// Find a matching manifest entry
|
||||||
|
image_index
|
||||||
|
.manifests
|
||||||
|
.iter()
|
||||||
|
.find(|entry| {
|
||||||
|
if let Some(p) = &entry.platform {
|
||||||
|
let os_match = p.os == req_os;
|
||||||
|
let arch_match = p.architecture == req_arch;
|
||||||
|
let variant_match = match &req_variant {
|
||||||
|
Some(v) => p.variant.as_deref() == Some(v.as_str()),
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
os_match && arch_match && variant_match
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"No manifest found for platform {} in image index",
|
||||||
|
platform_str
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
// No platform specified, take the first entry
|
||||||
|
image_index
|
||||||
|
.manifests
|
||||||
|
.first()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Image index has no manifests"))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let platform_digest = &selected.digest;
|
||||||
|
let url = format!(
|
||||||
|
"https://{}/v2/{}/manifests/{}",
|
||||||
|
reference.registry, reference.repository, platform_digest
|
||||||
|
);
|
||||||
|
|
||||||
|
debug!("Pulling platform-specific manifest from URL: {}", url);
|
||||||
|
|
||||||
|
let mut req = self.client
|
||||||
|
.get(&url)
|
||||||
|
.header("Accept", "application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json");
|
||||||
|
|
||||||
|
let platform_token = self
|
||||||
|
.authenticate(&reference.registry, &reference.repository, auth)
|
||||||
|
.await?;
|
||||||
|
if let Some(token) = platform_token {
|
||||||
|
req = req.header("Authorization", format!("Bearer {}", token));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = req.send().await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
anyhow::bail!("Failed to pull platform manifest: {}", response.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
let header_digest = response
|
||||||
|
.headers()
|
||||||
|
.get("docker-content-digest")
|
||||||
|
.and_then(|h| h.to_str().ok())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let platform_body = response.bytes().await?;
|
||||||
|
|
||||||
|
// Use header digest if available, otherwise compute from body
|
||||||
|
let platform_digest = header_digest
|
||||||
|
.unwrap_or_else(|| format!("sha256:{}", sha256::digest(platform_body.as_ref())));
|
||||||
|
debug!(
|
||||||
|
"Platform manifest response body: {}",
|
||||||
|
String::from_utf8_lossy(&platform_body)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
serde_json::from_slice::<OciImageManifest>(&platform_body)?,
|
||||||
|
platform_digest,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// Pull a blob from the registry
|
// Pull a blob from the registry
|
||||||
pub async fn pull_blob(
|
pub async fn pull_blob(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -805,13 +879,13 @@ impl RegistryClient {
|
||||||
anyhow::bail!("Failed to upload blob: {} - {}", monolithic_status, body)
|
anyhow::bail!("Failed to upload blob: {} - {}", monolithic_status, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push a manifest to the registry
|
// Push a manifest to the registry, returns the digest string
|
||||||
pub async fn push_manifest(
|
pub async fn push_manifest(
|
||||||
&mut self,
|
&mut self,
|
||||||
image_ref: &str,
|
image_ref: &str,
|
||||||
manifest: &OciImageManifest,
|
manifest: &OciImageManifest,
|
||||||
auth: &RegistryAuth,
|
auth: &RegistryAuth,
|
||||||
) -> Result<(String, String)> {
|
) -> Result<String> {
|
||||||
let reference = ImageReference::parse(image_ref)?;
|
let reference = ImageReference::parse(image_ref)?;
|
||||||
let manifest_json = serde_json::to_vec_pretty(manifest)?;
|
let manifest_json = serde_json::to_vec_pretty(manifest)?;
|
||||||
let manifest_digest = format!("sha256:{}", sha256::digest(&manifest_json));
|
let manifest_digest = format!("sha256:{}", sha256::digest(&manifest_json));
|
||||||
|
|
@ -827,11 +901,7 @@ impl RegistryClient {
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
debug!("Manifest {} already exists, skipping push", manifest_digest);
|
debug!("Manifest {} already exists, skipping push", manifest_digest);
|
||||||
let digest_ref = format!(
|
return Ok(manifest_digest);
|
||||||
"{}/{}@{}",
|
|
||||||
reference.registry, reference.repository, manifest_digest
|
|
||||||
);
|
|
||||||
return Ok((digest_ref, manifest_digest));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Pushing manifest with digest: {}", manifest_digest);
|
info!("Pushing manifest with digest: {}", manifest_digest);
|
||||||
|
|
@ -871,16 +941,10 @@ impl RegistryClient {
|
||||||
let digest = headers
|
let digest = headers
|
||||||
.get("docker-content-digest")
|
.get("docker-content-digest")
|
||||||
.and_then(|h| h.to_str().ok())
|
.and_then(|h| h.to_str().ok())
|
||||||
.unwrap_or("")
|
.unwrap_or(&manifest_digest)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let location = headers
|
Ok(digest)
|
||||||
.get("location")
|
|
||||||
.and_then(|h| h.to_str().ok())
|
|
||||||
.unwrap_or(&url)
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
Ok((location, digest))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy methods for compatibility with existing code
|
// Legacy methods for compatibility with existing code
|
||||||
|
|
@ -929,7 +993,7 @@ impl RegistryClient {
|
||||||
annotations: None,
|
annotations: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (_, digest) = self.push_manifest(repository, &manifest, auth).await?;
|
let digest = self.push_manifest(repository, &manifest, auth).await?;
|
||||||
let reference = ImageReference::parse(repository)?;
|
let reference = ImageReference::parse(repository)?;
|
||||||
let digest_ref = format!("{}/{}@{}", reference.registry, reference.repository, digest);
|
let digest_ref = format!("{}/{}@{}", reference.registry, reference.repository, digest);
|
||||||
let manifest_size = serde_json::to_vec(&manifest)?.len();
|
let manifest_size = serde_json::to_vec(&manifest)?.len();
|
||||||
|
|
@ -940,10 +1004,12 @@ impl RegistryClient {
|
||||||
pub async fn fetch_image_data(
|
pub async fn fetch_image_data(
|
||||||
&mut self,
|
&mut self,
|
||||||
image_ref: &str,
|
image_ref: &str,
|
||||||
_platform: &str,
|
platform: &str,
|
||||||
auth: &RegistryAuth,
|
auth: &RegistryAuth,
|
||||||
) -> Result<(OciImageManifest, crate::image::ImageConfig)> {
|
) -> Result<(OciImageManifest, crate::image::ImageConfig)> {
|
||||||
let (manifest, _digest) = self.pull_manifest(image_ref, auth).await?;
|
let (manifest, _digest) = self
|
||||||
|
.pull_manifest_for_platform(image_ref, auth, Some(platform))
|
||||||
|
.await?;
|
||||||
|
|
||||||
if let Some(config_descriptor) = &manifest.config {
|
if let Some(config_descriptor) = &manifest.config {
|
||||||
let config_data = self.pull_blob(image_ref, config_descriptor, auth).await?;
|
let config_data = self.pull_blob(image_ref, config_descriptor, auth).await?;
|
||||||
|
|
@ -956,12 +1022,75 @@ impl RegistryClient {
|
||||||
|
|
||||||
pub async fn get_image_platforms(
|
pub async fn get_image_platforms(
|
||||||
&mut self,
|
&mut self,
|
||||||
_image_ref: &str,
|
image_ref: &str,
|
||||||
_auth: &RegistryAuth,
|
auth: &RegistryAuth,
|
||||||
) -> Result<Vec<String>> {
|
) -> Result<Vec<String>> {
|
||||||
// For now, return default platforms - this would need to be enhanced
|
let reference = ImageReference::parse(image_ref)?;
|
||||||
// to actually fetch and parse image indexes
|
let token = self
|
||||||
Ok(vec!["linux/amd64".to_string(), "linux/arm64".to_string()])
|
.authenticate(&reference.registry, &reference.repository, auth)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let manifest_ref = if let Some(digest) = &reference.digest {
|
||||||
|
digest.clone()
|
||||||
|
} else {
|
||||||
|
reference.tag.as_deref().unwrap_or("latest").to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!(
|
||||||
|
"https://{}/v2/{}/manifests/{}",
|
||||||
|
reference.registry, reference.repository, manifest_ref
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut req = self.client
|
||||||
|
.get(&url)
|
||||||
|
.header("Accept", "application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json");
|
||||||
|
|
||||||
|
if let Some(token) = token {
|
||||||
|
req = req.header("Authorization", format!("Bearer {}", token));
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = req.send().await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to fetch manifest for platform detection: {}",
|
||||||
|
response.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = response.bytes().await?;
|
||||||
|
|
||||||
|
// Try to parse as an image index
|
||||||
|
if let Ok(image_index) = serde_json::from_slice::<OciImageIndex>(&body) {
|
||||||
|
let platforms: Vec<String> = image_index
|
||||||
|
.manifests
|
||||||
|
.iter()
|
||||||
|
.filter_map(|entry| {
|
||||||
|
entry.platform.as_ref().map(|p| {
|
||||||
|
if let Some(variant) = &p.variant {
|
||||||
|
format!("{}/{}/{}", p.os, p.architecture, variant)
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", p.os, p.architecture)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(platforms)
|
||||||
|
} else if let Ok(manifest) = serde_json::from_slice::<OciImageManifest>(&body) {
|
||||||
|
// Single-platform image — read the config to determine its platform
|
||||||
|
let config_descriptor = manifest.config.as_ref().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("Single-platform manifest has no config descriptor")
|
||||||
|
})?;
|
||||||
|
let config_data = self.pull_blob(image_ref, config_descriptor, auth).await?;
|
||||||
|
let config = serde_json::from_slice::<crate::image::ImageConfig>(&config_data)
|
||||||
|
.context("Failed to parse image config for platform detection")?;
|
||||||
|
Ok(vec![format!("{}/{}", config.os, config.architecture)])
|
||||||
|
} else {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Response is neither a valid image index nor image manifest; \
|
||||||
|
cannot detect platforms"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a layered image where only the top layer is new
|
/// Push a layered image where only the top layer is new
|
||||||
|
|
@ -1055,7 +1184,7 @@ impl RegistryClient {
|
||||||
annotations: None,
|
annotations: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (_, digest) = self.push_manifest(repository, &oci_manifest, auth).await?;
|
let digest = self.push_manifest(repository, &oci_manifest, auth).await?;
|
||||||
let digest_ref = format!(
|
let digest_ref = format!(
|
||||||
"{}/{}@{}",
|
"{}/{}@{}",
|
||||||
target_reference.registry, target_reference.repository, digest
|
target_reference.registry, target_reference.repository, digest
|
||||||
|
|
@ -1092,11 +1221,7 @@ impl RegistryClient {
|
||||||
media_type: m.media_type.clone(),
|
media_type: m.media_type.clone(),
|
||||||
digest: m.digest.clone(),
|
digest: m.digest.clone(),
|
||||||
size: m.size,
|
size: m.size,
|
||||||
platform: Some(Platform {
|
platform: Some(m.platform.clone()),
|
||||||
architecture: m.platform.architecture.clone(),
|
|
||||||
os: m.platform.os.clone(),
|
|
||||||
variant: m.platform.variant.clone(),
|
|
||||||
}),
|
|
||||||
annotations: None,
|
annotations: None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue