use core::pin::Pin;
use core::task::{Context, Poll};
use std::future::poll_fn;
use bytes::Bytes;
use foundation_deployment::providers::common::{ByteStream, JsonLinesStream};
use futures_lite::Stream as FuturesStream;
use serde::de::DeserializeOwned;
use crate::error::DockerError;
use crate::generated::create::CreateImageInfo;
use crate::generated::events::EventMessage;
use crate::generated::push::PushImageInfo;
use crate::generated::stats::ContainerStatsResponse;
use crate::generated::shared::{BuildInfo, ErrorDetail};
use crate::streaming::decoder::{LogFrameDecoder, LogOutput};
pub trait ProgressItem: DeserializeOwned + Unpin {
fn error_message(&self) -> Option<String>;
}
fn detail_message(detail: Option<&ErrorDetail>) -> Option<String> {
let detail = detail?;
Some(match (&detail.message, detail.code) {
(Some(message), _) => message.clone(),
(None, Some(code)) => format!("error code {code}"),
(None, None) => "unspecified error".to_string(),
})
}
impl ProgressItem for CreateImageInfo {
fn error_message(&self) -> Option<String> {
detail_message(self.error_detail.as_ref())
}
}
impl ProgressItem for PushImageInfo {
fn error_message(&self) -> Option<String> {
detail_message(self.error_detail.as_ref())
}
}
impl ProgressItem for BuildInfo {
fn error_message(&self) -> Option<String> {
detail_message(self.error_detail.as_ref())
}
}
pub struct ProgressStream<T> {
lines: JsonLinesStream<T>,
on_error: fn(String) -> DockerError,
done: bool,
}
impl<T: ProgressItem> ProgressStream<T> {
#[must_use]
pub fn new(lines: JsonLinesStream<T>, on_error: fn(String) -> DockerError) -> Self {
Self {
lines,
on_error,
done: false,
}
}
pub async fn next(&mut self) -> Option<Result<T, DockerError>> {
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
}
pub async fn finish(mut self) -> Result<(), DockerError> {
while let Some(item) = self.next().await {
item?;
}
Ok(())
}
}
impl ProgressStream<BuildInfo> {
pub async fn image_id(mut self) -> Result<Option<String>, DockerError> {
let mut image_id = None;
while let Some(item) = self.next().await {
let item = item?;
if let Some(text) = item.stream.as_deref() {
tracing::debug!(target: "docker::build", "{}", text.trim_end());
}
if let Some(id) = item.aux.and_then(|aux| aux.id) {
image_id = Some(id);
}
}
Ok(image_id)
}
}
impl<T> core::fmt::Debug for ProgressStream<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ProgressStream")
.field("lines", &self.lines)
.field("done", &self.done)
.finish_non_exhaustive()
}
}
impl<T: ProgressItem> FuturesStream for ProgressStream<T> {
type Item = Result<T, DockerError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.done {
return Poll::Ready(None);
}
let item = match Pin::new(&mut this.lines).poll_next(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => {
this.done = true;
return Poll::Ready(None);
}
Poll::Ready(Some(item)) => item,
};
Poll::Ready(Some(match item {
Ok(item) => match item.error_message() {
Some(message) => {
this.done = true;
Err((this.on_error)(message))
}
None => Ok(item),
},
Err(e) => {
this.done = true;
Err(e.into())
}
}))
}
}
pub struct EventStream {
lines: JsonLinesStream<EventMessage>,
}
impl EventStream {
#[must_use]
pub fn new(lines: JsonLinesStream<EventMessage>) -> Self {
Self { lines }
}
pub async fn next(&mut self) -> Option<Result<EventMessage, DockerError>> {
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
}
}
impl core::fmt::Debug for EventStream {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("EventStream")
.field("lines", &self.lines)
.finish()
}
}
impl FuturesStream for EventStream {
type Item = Result<EventMessage, DockerError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.get_mut().lines)
.poll_next(cx)
.map(|item| item.map(|r| r.map_err(DockerError::from)))
}
}
pub struct StatsStream {
lines: JsonLinesStream<ContainerStatsResponse>,
}
impl StatsStream {
#[must_use]
pub fn new(lines: JsonLinesStream<ContainerStatsResponse>) -> Self {
Self { lines }
}
pub async fn next(&mut self) -> Option<Result<ContainerStatsResponse, DockerError>> {
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
}
}
impl core::fmt::Debug for StatsStream {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StatsStream")
.field("lines", &self.lines)
.finish()
}
}
impl FuturesStream for StatsStream {
type Item = Result<ContainerStatsResponse, DockerError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.get_mut().lines)
.poll_next(cx)
.map(|item| item.map(|r| r.map_err(DockerError::from)))
}
}
pub struct ByteBody {
bytes: ByteStream,
}
impl ByteBody {
#[must_use]
pub fn new(bytes: ByteStream) -> Self {
Self { bytes }
}
pub async fn next(&mut self) -> Option<Result<Bytes, DockerError>> {
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
}
pub async fn drain(self) -> Result<u64, DockerError> {
Ok(self.bytes.drain().await?)
}
}
impl core::fmt::Debug for ByteBody {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ByteBody")
.field("bytes", &self.bytes)
.finish()
}
}
impl FuturesStream for ByteBody {
type Item = Result<Bytes, DockerError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.get_mut().bytes)
.poll_next(cx)
.map(|item| item.map(|r| r.map_err(DockerError::from)))
}
}
pub struct LogStream {
bytes: ByteStream,
decoder: LogFrameDecoder,
max_frame: usize,
done: bool,
}
impl LogStream {
#[must_use]
pub fn new(bytes: ByteStream, max_frame: usize) -> Self {
Self {
bytes,
decoder: LogFrameDecoder::new(),
max_frame,
done: false,
}
}
pub async fn next(&mut self) -> Option<Result<LogOutput, DockerError>> {
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
}
pub async fn read_into(
mut self,
stdout: &mut String,
stderr: &mut String,
) -> Result<(), DockerError> {
while let Some(frame) = self.next().await {
match frame? {
LogOutput::StdOut { message } => {
stdout.push_str(&String::from_utf8_lossy(&message));
}
LogOutput::StdErr { message } => {
stderr.push_str(&String::from_utf8_lossy(&message));
}
LogOutput::StdIn { .. } | LogOutput::Console { .. } => {}
}
}
Ok(())
}
}
impl core::fmt::Debug for LogStream {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LogStream")
.field("buffered", &self.decoder.buffered_len())
.field("max_frame", &self.max_frame)
.field("done", &self.done)
.finish_non_exhaustive()
}
}
impl FuturesStream for LogStream {
type Item = Result<LogOutput, DockerError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
if this.done {
return Poll::Ready(None);
}
if let Some(len) = this.decoder.next_frame_len() {
if len > this.max_frame {
this.done = true;
return Poll::Ready(Some(Err(DockerError::JsonParse(format!(
"multiplexed stream frame of {len} bytes is larger than {} bytes",
this.max_frame
)))));
}
}
let before = this.decoder.buffered_len();
if let Some(frame) = this.decoder.decode() {
this.decoder.compact();
return Poll::Ready(Some(Ok(frame)));
}
if this.decoder.buffered_len() < before {
this.decoder.compact();
continue;
}
match Pin::new(&mut this.bytes).poll_next(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(chunk))) => this.decoder.feed(&chunk),
Poll::Ready(Some(Err(e))) => {
this.done = true;
return Poll::Ready(Some(Err(e.into())));
}
Poll::Ready(None) => {
this.done = true;
let left = this.decoder.buffered_len();
if left > 0 {
return Poll::Ready(Some(Err(DockerError::JsonParse(format!(
"multiplexed stream ended inside a frame ({left} bytes left over)"
)))));
}
return Poll::Ready(None);
}
}
}
}
}