use std::time::Duration;
use futures_util::{Stream, StreamExt};
use tracing::{info, warn};
use crate::compose::types::ComposeFile;
use crate::error::{ComposeError, Result};
use crate::libpod::types::image::ImagePullProgress;
use crate::libpod::{urlencoded, PodmanError, API_PREFIX};
use super::super::Engine;
const PUSH_STALL_TIMEOUT: Duration = Duration::from_secs(120);
async fn drain_push_stream<S>(
mut stream: S,
image: &str,
quiet: bool,
stall: Duration,
) -> Result<()>
where
S: Stream<Item = std::result::Result<ImagePullProgress, PodmanError>> + Unpin,
{
let mut stream_error: Option<String> = None;
loop {
match tokio::time::timeout(stall, stream.next()).await {
Ok(Some(Ok(progress))) => {
if !progress.stream.is_empty() && !quiet {
info!("{}", progress.stream.trim_end());
}
if !progress.error.is_empty() {
stream_error = Some(progress.error.clone());
}
}
Ok(Some(Err(e))) => stream_error = Some(e.to_string()),
Ok(None) => break,
Err(_elapsed) => {
return Err(ComposeError::Build(format!(
"push {image}: no progress from the registry for {}s; aborting \
(registry unreachable or unresponsive)",
stall.as_secs()
)));
}
}
}
match stream_error {
Some(err) => Err(ComposeError::Build(format!("push {image}: {err}"))),
None => {
if quiet {
tracing::debug!("pushed {image}");
} else {
info!("pushed {image}");
}
Ok(())
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PushOptions {
pub ignore_failures: bool,
pub tls_verify: Option<bool>,
}
impl Engine {
pub async fn push(
&self,
file: &ComposeFile,
target_services: &[String],
opts: PushOptions,
) -> Result<()> {
self.push_with_quiet(file, target_services, opts, false)
.await
}
pub async fn push_with_quiet(
&self,
file: &ComposeFile,
target_services: &[String],
opts: PushOptions,
quiet: bool,
) -> Result<()> {
for svc in target_services {
if !file.services.contains_key(svc) {
return Err(ComposeError::ServiceNotFound(svc.clone()));
}
}
for (name, service) in &file.services {
if !target_services.is_empty() && !target_services.iter().any(|t| t == name) {
continue;
}
let Some(image) = service.image.as_deref() else {
tracing::debug!("{name}: no image to push, skipping");
continue;
};
if let Err(e) = self.push_image(image, &opts, quiet).await {
if opts.ignore_failures {
warn!("push {image} failed (ignored): {e}");
} else {
return Err(e);
}
}
}
Ok(())
}
async fn push_image(&self, image: &str, opts: &PushOptions, quiet: bool) -> Result<()> {
if quiet {
tracing::debug!("pushing {image}");
} else {
info!("pushing {image}");
}
let mut query = format!("destination={}", urlencoded(image));
if let Some(tls) = opts.tls_verify {
query.push_str(&format!("&tlsVerify={tls}"));
}
let path = format!("{API_PREFIX}/images/{}/push?{query}", urlencoded(image));
let resp = self
.client
.post_empty_stream(&path)
.await
.map_err(ComposeError::Podman)?;
let stream = crate::libpod::parse_json_lines::<ImagePullProgress>(resp.into_body());
drain_push_stream(stream, image, quiet, PUSH_STALL_TIMEOUT).await
}
}
#[cfg(test)]
mod tests {
use super::{drain_push_stream, ImagePullProgress};
use crate::error::ComposeError;
use crate::libpod::PodmanError;
use futures_util::StreamExt;
use std::time::Duration;
fn progress(stream: &str, error: &str) -> ImagePullProgress {
ImagePullProgress {
stream: stream.to_string(),
error: error.to_string(),
}
}
#[tokio::test]
async fn drain_ok_when_stream_completes_cleanly() {
let items = vec![Ok(progress("pushing", "")), Ok(progress("done", ""))];
let stream = futures_util::stream::iter(items);
drain_push_stream(stream, "img", false, Duration::from_secs(5))
.await
.unwrap();
}
#[tokio::test]
async fn drain_surfaces_mid_stream_error_line() {
let items = vec![Ok(progress("", "denied: unauthorized"))];
let stream = futures_util::stream::iter(items);
let err = drain_push_stream(stream, "img", false, Duration::from_secs(5))
.await
.unwrap_err();
assert!(matches!(err, ComposeError::Build(m) if m.contains("denied: unauthorized")));
}
#[tokio::test]
async fn drain_times_out_on_an_unresponsive_stream() {
let first = futures_util::stream::iter(vec![Ok(progress("pushing", ""))]);
let stream = first.chain(futures_util::stream::pending::<
std::result::Result<ImagePullProgress, PodmanError>,
>());
let err = drain_push_stream(stream, "img", false, Duration::from_millis(20))
.await
.unwrap_err();
assert!(matches!(err, ComposeError::Build(m) if m.contains("no progress")));
}
}