use clap::{Parser, Subcommand};
use thiserror::Error;
mod common;
mod import;
mod push;
use oci_core::client::{Client, ClientConfig};
pub const STATE_DIR: &str = ".docker-image-pusher";
pub const STREAM_BUFFER_SIZE: usize = 8 * 1024 * 1024;
pub const PROGRESS_LAYER_THRESHOLD_BYTES: u64 = 500 * 1024 * 1024;
pub const PROGRESS_UPDATE_INTERVAL_SECS: u64 = 3;
pub const CHUNKED_LAYER_SIZE_BYTES: usize = 10 * 1024 * 1024;
pub const MAX_CHUNKED_LAYER_SIZE_BYTES: usize = 256 * 1024 * 1024;
pub const LARGE_LAYER_THRESHOLD_BYTES: u64 = 500 * 1024 * 1024;
pub const LARGE_LAYER_THRESHOLD_MB: f64 = 500.0;
pub const ESTIMATED_SPEED_MBPS: f64 = 10.0;
pub const LARGE_LAYER_PROGRESS_INTERVAL_SECS: u64 = 3;
pub const NORMAL_LAYER_PROGRESS_INTERVAL_SECS: u64 = 1;
pub const RATE_LIMIT_DELAY_MS: u64 = 250;
pub const GZIP_MAGIC_BYTES: [u8; 2] = [0x1F, 0x8B];
#[derive(Parser, Debug)]
#[command(
name = "docker-image-pusher",
version,
about = "Stream large Docker/OCI images through a tiny local cache"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Push {
#[arg(long, value_name = "TAR")]
tar: Option<String>,
#[arg(long, value_name = "DIR")]
root: Option<String>,
#[arg(long, value_name = "NS", default_value = "default")]
namespace: String,
#[arg(long, value_name = "IMAGE")]
image: Option<String>,
#[arg(short, long)]
target: Option<String>,
#[arg(long)]
registry: Option<String>,
#[arg(long)]
username: Option<String>,
#[arg(long)]
password: Option<String>,
#[arg(long = "blob-chunk", value_name = "MB")]
blob_chunk: Option<usize>,
},
Save {
#[arg(long, value_name = "DIR")]
root: Option<String>,
#[arg(long, value_name = "NS", default_value = "default")]
namespace: String,
#[arg(value_name = "IMAGE", num_args = 1..)]
images: Vec<String>,
#[arg(long, value_name = "OUT")]
out: String,
#[arg(long, value_name = "DIGEST")]
digest: Option<String>,
},
ListContainerd {
#[arg(long, value_name = "DIR")]
root: Option<String>,
#[arg(long, value_name = "NS", default_value = "default")]
namespace: String,
},
Login {
#[arg(value_name = "REGISTRY")]
registry: String,
#[arg(long)]
username: String,
#[arg(long)]
password: String,
},
}
#[derive(Error, Debug)]
pub enum PusherError {
#[error("Pull error: {0}")]
PullError(String),
#[error("Push error: {0}")]
PushError(String),
#[error("Cache error: {0}")]
CacheError(String),
#[error("Cache entry not found")]
CacheNotFound,
#[error("Tar error: {0}")]
TarError(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
}
impl From<serde_json::Error> for PusherError {
fn from(value: serde_json::Error) -> Self {
PusherError::CacheError(format!("JSON error: {}", value))
}
}
impl PusherError {
pub fn push_error(message: impl Into<String>) -> Self {
PusherError::PushError(message.into())
}
pub fn tar_error(message: impl Into<String>) -> Self {
PusherError::TarError(message.into())
}
}
#[tokio::main]
async fn main() -> Result<(), PusherError> {
let cli = Cli::parse();
let client = Client::new(ClientConfig::default());
match cli.command {
Commands::Push {
tar,
root,
namespace,
image,
target,
registry,
username,
password,
blob_chunk,
} => {
if let Some(tar_path) = tar {
push::run_push(
&client, &tar_path, target, username, password, registry, blob_chunk,
)
.await?;
} else {
let image = image.ok_or_else(|| {
PusherError::push_error("--image is required when pushing from containerd")
})?;
import::containerd::run_push_containerd(
&client,
root.as_deref(),
&namespace,
&image,
target,
username,
password,
registry,
blob_chunk,
)
.await?;
}
}
Commands::Save {
root,
namespace,
images,
out,
digest,
} => {
import::containerd::export_images(
root.as_deref(),
&namespace,
&images,
&out,
digest.as_deref(),
)
.await?;
}
Commands::ListContainerd { root, namespace } => {
import::containerd::list_images(root.as_deref(), &namespace).await?;
}
Commands::Login {
registry,
username,
password,
} => {
common::state::store_credentials(®istry, &username, &password).await?;
println!(" Stored credentials for {}", registry);
}
}
Ok(())
}