Response

Zero or more responses of the byte 0 followed by one line of the build’s output; then exactly one response that is the byte 1 followed by the digest and the size of the image, the byte 2 alone, the byte 3 alone when something holds the context volume, the byte 4 alone when the server keeps no client image, or the byte 5 followed by an error as one JSON value; and the response finish.

[0][line_len: varint][line: utf8 …]          per line of output
[1][digest_len: varint][digest: utf8 …][bytes: varint]   once: the image exists
[2]                                          once: insufficient capacity
[3]                                          once, alone: something holds the context volume
[4]                                          once, alone: the server keeps no client image
[5][error JSON …]                            once: an error
  • The sequence. Zero or more responses of the byte 0, each followed by one line; then exactly one response whose first byte is 1, 2, 3, 4 or 5; then the response finish. No response follows that response. No response of the byte 0 precedes a response of the byte 3 or 4: each is the only response of its scope. A response finish that no response of the byte 1 to 5 precedes states that the request was not served, as Endpoints provides; whether the image exists is then not stated, and the client learns it from an images::list scope.
  • Output. A payload whose first byte is 0 carries one line of the build’s output after that byte, as a varint byte length followed by that many bytes of UTF-8. The server sends the lines in the order the build’s tool wrote them. This revision prescribes nothing about their content.
  • Built. A payload whose first byte is 1 states that the image exists, and carries its digest as a varint byte length followed by that many bytes of UTF-8, the manifest digest of the image as the server holds it, followed by bytes as a varint, the size of the image in bytes as the server counts it against its store. The server sends it after the image exists; an images::list scope the client opens after receiving it lists the image, and a list scope the client holds open is sent the image as added.
  • Insufficient capacity. A payload whose first byte is 2 states that the server’s store has no room for the image and that nothing was built. No image exists by that name as a result of the request. The server sends nothing after the byte 2, and a client ignores every byte that follows it.
  • Context held. A payload that is the byte 3 and nothing else states that the volume the request names as its context is held at the time of the request — by an operation, by a running container beyond what the volume’s mode object allows with mount_many, or by a FUSE serve beyond what it allows with mount_and_fuse_serve, as Capabilities states for a build’s context — and that nothing was built. No image exists by that name as a result of the request. The server sends it before the build begins, and bytes after it are not read.
  • Unsupported. A payload that is the byte 4 and nothing else states that the server holds out no images_store capability and keeps no client image, as Capabilities provides. It is the only response of the scope, sent before anything is looked up; bytes after it are not read.
  • The error. A payload whose first byte is 5 carries an error after that byte, running to the end of the payload. The error is exactly one JSON value, in the form defined on the volumes::list response page. This revision prescribes nothing about its content. The error states that the image does not exist as a result of the request, and that the reason is neither capacity nor the context’s hold.
  • Malformed. A payload with no byte, a payload whose first byte is none of 0 to 5, a payload whose bytes after 0 do not decode as one string, a payload whose bytes after 1 do not decode as the digest and the size, and a payload whose bytes after 5 are not one JSON value are malformed.

The response is defined by diverge-sdk-rs/src/provider/endpoints/images/build/server/response/frame.rs:

//! What a server's response frame carries for an image build.

use std::fmt;

use super::Built;
use crate::wire::decode::Decode;
use crate::wire::encode::{Encode, Writer};
use crate::shared::error::Error;

/// A line the build said; the image built; the word that the store
/// has no room for it; or the news that the provider could not build
/// it.
///
/// A build is a stream with an end. Zero or more
/// [`Output`](Self::Output) frames, each a line the provider's build
/// tool wrote, as it wrote them; then exactly one of
/// [`Built`](Self::Built), [`InsufficientCapacity`](Self::InsufficientCapacity),
/// [`ContextHeld`](Self::ContextHeld), [`Unsupported`](Self::Unsupported)
/// and [`Error`](Self::Error); then the finish, and nothing after it.
/// A payload leads with one byte saying which — `0` for `Output`,
/// `1` for `Built`, `2` for `InsufficientCapacity`, `3` for
/// `ContextHeld`, `4` for `Unsupported`, `5` for `Error` — and the
/// rest is that variant's own bytes; the three bare refusals carry
/// nothing, and come with no output before them.
///
/// | the scope ends with | means |
/// |----------------------|-------|
/// | [`Built`](Self::Built), then a finish | the image exists under the name, and a listing will show it |
/// | [`InsufficientCapacity`](Self::InsufficientCapacity), then a finish | the store has no room for it, and nothing exists |
/// | [`ContextHeld`](Self::ContextHeld), then a finish | something holds the context volume, and nothing exists |
/// | [`Unsupported`](Self::Unsupported), then a finish | the provider has no image store, and nothing exists |
/// | an [`Error`](Self::Error), then a finish | it does not exist, for some other reason — a build that failed, a name taken, a context refused — and the output before it says what the build said |
///
/// # The refusals are answers, not errors
///
/// A provider whose store has no room for the image says so with
/// [`InsufficientCapacity`](Self::InsufficientCapacity) rather than
/// with an [`Error`](Self::Error) a caller could not tell from any
/// other failure. The distinction is what a caller acts on: a store
/// with no room is one to delete an image from and ask again, and a
/// failure is not. Whether the provider knows before it builds or
/// only after is the provider's; either way nothing exists. A context
/// volume that something holds — a running container, a FUSE serve,
/// or an operation that has it to itself — is
/// [`ContextHeld`](Self::ContextHeld), a thing to stop or wait for;
/// a provider with no image store at all is
/// [`Unsupported`](Self::Unsupported), another provider to ask.
///
/// # Output is text
///
/// Each line is a string, and the provider decodes what its tool
/// wrote before sending it: a caller shows it to a person and does
/// not parse it. Nothing promises what the lines say, how many there
/// are, or that there are any.
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
    /// A line of the build's output, as the build tool wrote it. Tag
    /// `0`.
    Output(String),
    /// The image exists. Tag `1`.
    Built(Built),
    /// The store has no room for the image, and nothing exists. Tag
    /// `2`.
    InsufficientCapacity,
    /// Something holds the volume the context names — a running
    /// container, a FUSE serve, or a stat, a read, a write, a filetree,
    /// an edit or a delete that has it to itself — and nothing exists.
    /// Tag `3`.
    ///
    /// Carries nothing. A build takes the shared hold a run takes,
    /// and a volume that will not take it is this; a caller stops the
    /// holder, or waits for it, and asks again.
    ContextHeld,
    /// The provider has no image store — its `images_store` is `false`,
    /// see [`info`](crate::provider::endpoints::info) — and nothing
    /// exists. Tag `4`.
    ///
    /// Carries nothing. A caller asks another provider; nothing about
    /// the request is the cause.
    Unsupported,
    /// A failure. Tag `5`.
    ///
    /// See [`shared::error::Error`](crate::shared::error::Error) for
    /// why it says so little; the output before it says more.
    Error(Error),
}

/// Tag for [`Frame::Output`].
const OUTPUT: u8 = 0;

/// Tag for [`Frame::Built`].
const BUILT: u8 = 1;

/// Tag for [`Frame::InsufficientCapacity`].
const INSUFFICIENT_CAPACITY: u8 = 2;

/// Tag for [`Frame::ContextHeld`].
const CONTEXT_HELD: u8 = 3;

/// Tag for [`Frame::Unsupported`].
const UNSUPPORTED: u8 = 4;

/// Tag for [`Frame::Error`].
const ERROR: u8 = 5;

/// Two formats, and the tag chooses between them: a line and the
/// image built are postcard, as the rest of the store is, and the
/// error is JSON, since a [`serde_json::Value`] cannot come back out
/// of a format with no self-description.
impl Encode for Frame {
    /// One failure per half, and they are different libraries'.
    type Error = FrameEncodeError;

    // Spelled out rather than `Self::Error`: this enum has a variant
    // called `Error`, so the associated type is ambiguous by that name.
    fn encode(&self, out: &mut Writer<'_>) -> Result<(), FrameEncodeError> {
        match self {
            Frame::Output(line) => {
                out.extend_from_slice(&[OUTPUT]);
                postcard::to_io(line, &mut *out).map(|_| ()).map_err(FrameEncodeError::Output)
            }
            Frame::Built(built) => {
                out.extend_from_slice(&[BUILT]);
                postcard::to_io(built, &mut *out).map(|_| ()).map_err(FrameEncodeError::Built)
            }
            Frame::InsufficientCapacity => {
                out.extend_from_slice(&[INSUFFICIENT_CAPACITY]);
                Ok(())
            }
            Frame::ContextHeld => {
                out.extend_from_slice(&[CONTEXT_HELD]);
                Ok(())
            }
            Frame::Unsupported => {
                out.extend_from_slice(&[UNSUPPORTED]);
                Ok(())
            }
            Frame::Error(error) => {
                out.extend_from_slice(&[ERROR]);
                error.encode(out).map_err(FrameEncodeError::Error)
            }
        }
    }
}

impl Decode<'_> for Frame {
    /// Five ways to fail, and each names which half failed.
    type Error = FrameDecodeError;

    // Spelled out for the same reason as `encode` above.
    fn decode(bytes: &[u8]) -> Result<Self, FrameDecodeError> {
        let (tag, rest) = bytes.split_first().ok_or(FrameDecodeError::Empty)?;
        match *tag {
            OUTPUT => postcard::from_bytes(rest).map(Frame::Output).map_err(FrameDecodeError::Output),
            BUILT => postcard::from_bytes(rest).map(Frame::Built).map_err(FrameDecodeError::Built),
            INSUFFICIENT_CAPACITY => Ok(Frame::InsufficientCapacity),
            CONTEXT_HELD => Ok(Frame::ContextHeld),
            UNSUPPORTED => Ok(Frame::Unsupported),
            ERROR => Error::decode(rest).map(Frame::Error).map_err(FrameDecodeError::Error),
            tag => Err(FrameDecodeError::UnknownTag(tag)),
        }
    }
}

/// An image build response that could not be written.
#[derive(Debug)]
pub enum FrameEncodeError {
    /// The line did not serialize.
    Output(postcard::Error),
    /// The image built did not serialize.
    Built(postcard::Error),
    /// The error did not serialize.
    Error(serde_json::Error),
}

impl fmt::Display for FrameEncodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameEncodeError::Output(error) => write!(f, "image build output did not serialize: {error}"),
            FrameEncodeError::Built(error) => write!(f, "image built did not serialize: {error}"),
            FrameEncodeError::Error(error) => write!(f, "image build error did not serialize: {error}"),
        }
    }
}

impl std::error::Error for FrameEncodeError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            FrameEncodeError::Output(error) => Some(error),
            FrameEncodeError::Built(error) => Some(error),
            FrameEncodeError::Error(error) => Some(error),
        }
    }
}

/// An image build response that could not be read.
#[derive(Debug)]
pub enum FrameDecodeError {
    /// No bytes at all, so not even a tag.
    Empty,
    /// A tag that is none of this frame's six.
    UnknownTag(u8),
    /// The line did not parse.
    Output(postcard::Error),
    /// The image built did not parse.
    Built(postcard::Error),
    /// The error did not parse.
    Error(serde_json::Error),
}

impl fmt::Display for FrameDecodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameDecodeError::Empty => f.write_str("image build response frame is empty"),
            FrameDecodeError::UnknownTag(tag) => write!(f, "unknown image build response frame tag {tag}"),
            FrameDecodeError::Output(error) => write!(f, "image build output did not parse: {error}"),
            FrameDecodeError::Built(error) => write!(f, "image built did not parse: {error}"),
            FrameDecodeError::Error(error) => write!(f, "image build error did not parse: {error}"),
        }
    }
}

impl std::error::Error for FrameDecodeError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            FrameDecodeError::Output(error) => Some(error),
            FrameDecodeError::Built(error) => Some(error),
            FrameDecodeError::Error(error) => Some(error),
            FrameDecodeError::Empty | FrameDecodeError::UnknownTag(_) => None,
        }
    }
}

and the image built by diverge-sdk-rs/src/provider/endpoints/images/build/server/response/built.rs:

//! The image, built.

use serde::{Deserialize, Serialize};

/// The image built: what a listing will report it by, less the name
/// the caller already knows and the time, which is now.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Built {
    /// The manifest digest of the image as the provider holds it,
    /// `<algorithm>:<hex>`: what a container request names.
    pub digest: String,
    /// Its size in BYTES, as the provider counts it against the
    /// store's capacity.
    pub bytes: u64,
}