use std::path::PathBuf;
use eyre::{Context, Result, bail};
use tempfile::TempDir;
use crate::cli::oci::common::perform_build;
use crate::config::Settings;
use crate::oci::{BuildOptions, LayerOwner, registry};
#[derive(Debug, usage_rs::Args)]
#[usage(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP,
example(r###"mise oci push ghcr.io/me/devenv:latest"###, help = r###"Build and push to GHCR:"###),
example(r###"mise oci build -o ./img
mise oci push --image-dir ./img ghcr.io/me/devenv:v1"###, help = r###"Push an image built earlier:"###))]
pub(super) struct Push {
#[usage(value_name = "REF")]
reference: String,
#[usage(long, value_name = "REF", conflicts = &["no_cache", "image_dir"])]
cache_from: Option<String>,
#[usage(long)]
from: Option<String>,
#[usage(long, value_hint = ValueHint::DirPath, conflicts = &["from", "mount_point", "no_mise", "owner", "include_global"])]
image_dir: Option<PathBuf>,
#[usage(long)]
include_global: bool,
#[usage(long)]
mount_point: Option<String>,
#[usage(long)]
no_cache: bool,
#[usage(long)]
no_mise: bool,
#[usage(long, value_name = "UID[:GID]")]
owner: Option<LayerOwner>,
#[usage(long)]
update_index: bool,
}
impl Push {
pub(super) async fn run(self) -> Result<()> {
Settings::get().ensure_experimental("mise oci push")?;
if !self.reference.contains('/') {
bail!(
"push destination must be a fully-qualified reference \
(e.g. `ghcr.io/you/devenv:tag`); got {:?}",
self.reference
);
}
let mut reused_layers = 0;
let (image_dir, _tempdir_guard): (PathBuf, Option<TempDir>) =
if let Some(d) = &self.image_dir {
if !d.join("index.json").is_file() {
bail!(
"{}: does not look like an OCI image layout (missing index.json)",
d.display()
);
}
(d.clone(), None)
} else {
let td = TempDir::with_prefix("mise-oci-push-")
.wrap_err("creating temp dir for oci build output")?;
let out_dir = td.path().join("image");
let opts = BuildOptions {
out_dir: out_dir.clone(),
from: self.from.clone(),
tag: Some(self.reference.clone()),
mount_point: self.mount_point.clone(),
owner: self.owner,
include_mise: !self.no_mise,
copy: vec![],
reuse_from: self.fetch_layer_cache().await?,
};
let built = perform_build(opts, self.include_global).await?;
reused_layers = built.tool_layers.iter().filter(|l| l.reused).count();
info!("built image: {}", built.manifest_digest);
(out_dir, Some(td))
};
let summary = registry::push_image(&image_dir, &self.reference, self.update_index).await?;
let mut extras = String::new();
if summary.mounted > 0 {
extras.push_str(&format!(", {} mounted from base repo", summary.mounted));
}
if reused_layers > 0 {
extras.push_str(&format!(
", {reused_layers} tool layer(s) reused from previous image"
));
}
miseprintln!(
"pushed {} to {} ({} blob(s) uploaded, {} already present{extras})",
summary.manifest_digest,
self.reference,
summary.uploaded,
summary.skipped
);
if let Some(index_digest) = &summary.index_digest {
miseprintln!("updated image index: {index_digest}");
}
Ok(())
}
async fn fetch_layer_cache(&self) -> Result<Option<registry::RemoteImage>> {
if self.no_cache {
return Ok(None);
}
let cache_ref = self.cache_from.as_deref().unwrap_or(&self.reference);
if let Some(cache_from) = &self.cache_from {
let dest = registry::Reference::parse(&self.reference)?;
let cache = registry::Reference::parse(cache_from)?;
if dest.registry != cache.registry || dest.repository != cache.repository {
bail!(
"--cache-from must reference the same repository as the destination \
(got {}/{}, destination is {}/{})",
cache.registry,
cache.repository,
dest.registry,
dest.repository
);
}
}
match registry::fetch_remote_image(cache_ref).await {
Ok(remote) => {
if remote.is_none() {
debug!("no previous image at {cache_ref} — building all layers locally");
}
Ok(remote)
}
Err(e) => {
warn!("could not fetch layer cache from {cache_ref}: {e} — building all layers");
Ok(None)
}
}
}
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r###"<bold><underline>Auth:</underline></bold>
Credentials are resolved the same way docker/podman resolve them:
<bold>$REGISTRY_AUTH_FILE</bold>, <bold>$XDG_RUNTIME_DIR/containers/auth.json</bold>,
<bold>~/.config/containers/auth.json</bold>, then <bold>~/.docker/config.json</bold>
(inline auths and credential helpers). Log in with either:
$ <bold>docker login ghcr.io</bold>
$ <bold>podman login ghcr.io</bold>"###
);