API-Docker

 view release on metacpan or  search on metacpan

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

  # whole on the way to a >= 400 croak is still reported in bytes.
  my $summary = $ctx->{summary} ? $ctx->{summary}->() : undef;

  my $after = $summary
    ? ' after ' . $summary->{delivered} . ' unit'
      . ($summary->{delivered} == 1 ? '' : 's')
    : length($partial)
      ? ' after ' . length($partial) . ' byte'
        . (length($partial) == 1 ? '' : 's')
      : ', nothing arrived at all';

  # Carp hands a reference straight back rather than decorating it, so this
  # croak is a die with an object -- hence the location captured by hand,
  # which names the same frame a croak of a plain string would have named.
  # The object goes into a variable first: `croak CLASS->new(...)` is indirect
  # object syntax and parses as CLASS->croak(new(...)).
  my $error = API::Docker::Error::Timeout->new(
    message  => 'Docker API read timeout (' . $ctx->{endpoint} . '): '
      . $ctx->{timeout} . 's of silence' . $after,
    location => shortmess(''),
    endpoint => $ctx->{endpoint},
    timeout  => $ctx->{timeout},
    partial  => $partial,
    summary  => $summary,
  );
  croak $error;
}

# The other way a response ends before it is finished, and the one that needs
# no option to be armed: the daemon closed mid-sentence (karr k64).
#
# It is deliberately not folded into _croak_timeout. A timeout is the absence
# of an answer inside a bound the caller asked for, and it can fire on a
# response that would have completed; this is a statement about the response
# itself, made by comparing the body against what the response announced, and
# it fires whether or not anything was bounded. The two carry the same
# "here is what did arrive" contract and nothing else.
#
# $ctx->{partial} and $ctx->{summary} are read exactly as _croak_timeout reads
# them, so a buffered read reports bytes and a streamed one reports units,
# with no site having to know which it is.
sub _croak_truncated {
  my ($self, $ctx, %what) = @_;

  my $summary = $ctx->{summary} ? $ctx->{summary}->() : undef;
  my $partial = $ctx->{partial} ? ${ $ctx->{partial} } : '';

  my $arrived = $summary
    ? $summary->{delivered} . ' unit'
      . ($summary->{delivered} == 1 ? '' : 's') . ' delivered'
    : length($partial)
      ? length($partial) . ' byte'
        . (length($partial) == 1 ? '' : 's') . ' arrived'
      : 'nothing arrived at all';

  # Empty when a reader is driven directly rather than through _request, which
  # is how t/role_http.t drives them: an endpoint nobody named is left out of
  # the message rather than interpolated as the empty string.
  my $endpoint = defined $ctx->{endpoint} ? $ctx->{endpoint} : '';

  # The two phases with an announced length say the same sentence about it, so
  # they name the piece and let this write it; the two without pass the whole
  # detail, there being no count to put in one.
  my $detail = $what{detail};
  unless (defined $detail) {
    my $short = $what{expected} - $what{received};
    $detail = $what{piece} . ' stopped ' . $short . ' byte'
      . ($short == 1 ? '' : 's') . ' short of the ' . $what{expected}
      . ' it announced';
  }

  # Carp hands a reference straight back rather than decorating it, so this
  # croak is a die with an object -- hence the location captured by hand,
  # which names the same frame a croak of a plain string would have named.
  # The object goes into a variable first: `croak CLASS->new(...)` is indirect
  # object syntax and parses as CLASS->croak(new(...)).
  my $error = API::Docker::Error::Truncated->new(
    message => 'Docker API response truncated'
      . (length $endpoint ? ' (' . $endpoint . ')' : '') . ': '
      . $detail . '; ' . $arrived,
    location => shortmess(''),
    endpoint => $endpoint,
    phase    => $what{phase},
    expected => $what{expected},
    received => $what{received},
    partial  => $partial,
    summary  => $summary,
  );
  croak $error;
}

# ---------------------------------------------------------------------------
# Reading, in one buffer regime
#
# Every byte of a response is taken off the handle by _pull and by nothing
# else, and every reader below is served out of the buffer _pull fills. That
# is not an optimisation, it is the only shape that works (karr k60).
#
# What forced it: perl's read() is fread-shaped. It loops until it has the
# LENGTH it was asked for or the stream ends -- it does not return what has
# arrived. Measured on an AF_UNIX socketpair whose peer writes 6 bytes, waits
# half a second, writes 6 more and closes: read($sock, $buf, 65536) came back
# with 12 after 0.90s, having waited for the close, while sysread came back
# with 6 in 0.00s. On the endpoints with neither a Content-Length nor chunked
# encoding -- attach, logs(follow), exec/start, all
# application/vnd.docker.raw-stream -- the reader asks for $READ_SIZE, so
# read() delivered nothing to an on_frame/on_chunk callback until 64K had
# piled up or the daemon hung up. On a stream that never ends it would deliver
# nothing at all. The POD promised those callbacks the bytes as they arrive,
# and that promise was not kept.
#
# Why it could not be fixed at the one site that had the bug: _read_head read
# the status line and the headers with <$sock>, and PerlIO reads ahead. The
# bytes past the header block were sitting in a buffer this code cannot reach
# -- there is no supported way to take them back out; ungetc is layer-
# dependent, seek does not work on a socket, and select/MSG_PEEK see the
# kernel's buffer rather than PerlIO's. So switching only the body reads to
# sysread would have silently dropped the start of every body. Either all read
# sites move together or none do.
#
# The buffer lives on the handle rather than on the client or in the context:

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN


    # errorDetail is a HashRef carrying the message. The engine sends a flat
    # `error` next to it with the same text; that is the fallback, not the
    # trigger -- the trigger is errorDetail, and nothing else.
    my $reason = ref $detail eq 'HASH' ? $detail->{message} : undef;
    $reason = $event->{error}   unless defined $reason && length $reason;
    $reason = 'no message given' unless defined $reason && length $reason;
    # Engine messages end in a newline, and Carp appends no location to a
    # message that already does.
    $reason =~ s/\s+\z//;

    # Carp hands a reference straight back rather than decorating it, so this
    # croak is a die with an object -- hence the location captured by hand,
    # which names the same frame a croak of a plain string would have named.
    # The object goes into a variable first: `croak CLASS->new(...)` is
    # indirect object syntax and parses as CLASS->croak(new(...)).
    my $error = API::Docker::Error::Stream->new(
      message  => 'Docker API stream error (' . $endpoint . '): ' . $reason,
      events   => $events,
      location => shortmess(''),
    );
    croak $error;
  }

  return;
}

sub _assert_header_name {
  my ($self, $name) = @_;

  return if defined $name && $name =~ $HEADER_NAME;

  my $display = defined $name ? $name : '';
  $display =~ s/([^\x20-\x7E])/sprintf('\\x%02X', ord $1)/ge;
  croak __PACKAGE__ . '->_request invalid header name "' . $display . '": a '
    . 'header name must be an RFC 9110 token (letters, digits and '
    . '!#$%&\'*+-.^_`|~). A name is rejected rather than sanitised: unlike a '
    . 'value, there is no benign way for one to carry CR, LF, a space or a '
    . 'colon, and rewriting it would send a header the caller never wrote';
}

sub _assert_request_path {
  my ($self, $path) = @_;

  return if defined $path && $path =~ $REQUEST_PATH;

  my $display = defined $path ? $path : '';
  $display =~ s/([^\x20-\x7E])/sprintf('\\x%02X', ord $1)/ge;
  croak __PACKAGE__ . '->_request invalid request path "' . $display . '": a '
    . 'path may hold only request-target characters (letters, digits, -._~ '
    . 'and :/@!$&\'()*+,;=%). It is spliced straight into the request line, so '
    . 'a space, CR, LF, ? or # in a container name or image reference would '
    . 'rewrite the request rather than name a resource. A path is rejected '
    . 'rather than sanitised: percent-encoding it here cannot tell a path '
    . 'separator from data -- pass query parameters as `params`, not in the '
    . 'path';
}

sub _read_response {
  my ($self, $sock, $method, $ctx) = @_;
  # A context is what _request builds to say how long a silence may last and
  # what the exception has to name. It defaults to an empty one -- no timeout,
  # every read exactly as it was -- so the readers stay drivable directly, as
  # t/role_http.t drives them.
  $ctx ||= {};

  my $head = $self->_read_head($sock, $ctx);
  return [ @$head, $self->_read_body($sock, $head->[2], $method, $ctx) ];
}

sub _read_head {
  my ($self, $sock, $ctx) = @_;
  $ctx ||= {};

  # Looped so a 1xx informational response is read whole and passed by: it is a
  # complete head -- a status line and an optional field section closed by the
  # blank line -- with no body of its own, sent before the real response
  # (RFC 9110 section 15.2). Without this the reader took the 1xx status as the
  # response and then read the real response as its body. A 100 Continue is the
  # one an HTTP/1.1 client is most likely to be sent; 102 and 103 have the same
  # framing.
  while (1) {
    my $status_line = $self->_read_line($sock, $ctx);
    # A daemon that closed without answering at all, which is the one shape here
    # that was never silent and is left saying exactly what it always said.
    croak "No response from Docker daemon" unless defined $status_line;
    $self->_assert_status_line($ctx, $status_line);
    $status_line =~ s/\r?\n$//;

    my ($proto, $status_code, $status_text) = split /\s+/, $status_line, 3;

    # while(1)-and-assert rather than `while (my $line = ...)`, which is the
    # same shape _read_chunked uses and for the same reason: the loop used to
    # end on anything false, so an end of stream inside the header block left it
    # exactly as the blank line would have. The assert is now the only way out
    # that is not the blank line.
    my %headers;
    while (1) {
      my $line = $self->_read_line($sock, $ctx);
      $self->_assert_header_line($ctx, $line);
      $line =~ s/\r?\n$//;
      last if $line eq '';
      if ($line =~ /^([^:]+):\s*(.*)$/) {
        $headers{lc $1} = $2;
      }
    }

    # The status code is three digits by now (_assert_status_line), so a 1xx is
    # exactly 100..199. Its headers are dropped with it and the next head read.
    next if $status_code >= 100 && $status_code < 200;

    return [$status_code, $status_text, \%headers];
  }
}

# The two ways the head ends early, and neither of them has a byte count to
# compare either (karr k73).
#
# karr k64 left the head out on the grounds that nothing in a status line or a
# header block announces its own length, so there was no announcement to hold
# a short one against. True, and beside the point: an announcement is not what

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

      . "' is not a well-formed HTTP status line")
    unless $stripped =~ m{\AHTTP/[0-9]+\.[0-9]+ [0-9]{3}(?: .*)?\z};

  return;
}

# Both halves here, because the header block has no equivalent of that croak:
# undef is a block that was never closed -- with no headers at all, or after
# some -- and an unterminated line is one cut in the middle of a field.
sub _assert_header_line {
  my ($self, $ctx, $line) = @_;

  $self->_croak_truncated($ctx, phase => 'header-block',
    detail => 'the stream ended where a header line belongs, with no blank '
      . 'line to close the header block')
    unless defined $line;

  $self->_croak_truncated($ctx, phase => 'header-block',
    detail => 'the stream ended inside a header line, after '
      . length($line) . ' byte' . (length($line) == 1 ? '' : 's') . ' of one')
    unless $line =~ /\n\z/;

  return;
}

sub _read_body {
  my ($self, $sock, $headers, $method, $ctx) = @_;
  $ctx ||= {};

  # A HEAD response repeats the header fields the equivalent GET would send --
  # Content-Length and Transfer-Encoding included -- and then sends no body at
  # all. Every branch below would wait for bytes that never arrive: until the
  # daemon closes the connection at best, and forever if it does not. So the
  # body is not read for HEAD, whatever the headers promise.
  return '' if defined $method && uc($method) eq 'HEAD';

  if ($headers->{'transfer-encoding'}
      && lc($headers->{'transfer-encoding'}) eq 'chunked') {
    return $self->_read_chunked($sock, $ctx);
  }

  if (defined $headers->{'content-length'}) {
    my $len = $self->_assert_content_length($ctx, $headers->{'content-length'});
    return '' unless $len > 0;
    my $body = '';
    # What a timeout hands over instead of dropping: see
    # API::Docker::Error::Timeout/partial. localised so the context goes back
    # to carrying nothing once this body is done with.
    local $ctx->{partial} = \$body;
    # An announced length is a promise, and a stream that ends before it is
    # kept is truncation rather than the end of the body; see _read_exact.
    $self->_read_exact($sock, $len, \$body, $ctx, 'content-length',
      'the body');
    return $body;
  }

  # Read until the daemon closes. This used to be a `local $/; <$sock>` slurp;
  # it is a loop over the same primitive as the other two branches now, which
  # is what karr k60 needed and what the timeout wanted anyway -- with $/ undef
  # a whole body and a truncated one are both just bytes, so the slurp's own
  # result could never say which it was. This is the path karr k52's hang is
  # on, an attach whose buffered frames arrive and whose socket then never
  # closes.
  #
  # And the one shape with no completeness check to make: the response
  # announced no end, so the close IS the end (karr k64). Treating an EOF here
  # as truncation would make every attach, every logs(follow) and every
  # exec/start fail on the daemon hanging up, which is how all three finish.
  my $body = '';
  # What a timeout hands over instead of dropping; see the content-length
  # branch above.
  local $ctx->{partial} = \$body;
  while (1) {
    my ($n, $buf) = $self->_read_bytes($sock, $READ_SIZE, $ctx);
    last unless $n;
    $body .= $buf;
  }

  return $body;
}

# The incremental sibling of _read_response. Same [status, reason, headers,
# body] shape, plus a fifth element: the summary of what the callback was
# handed. The body it returns is empty -- that is the point, nothing is kept --
# except on the two paths that fall back to reading whole, which return undef
# as the summary instead so _request treats them exactly as before.
sub _read_streaming_response {
  my ($self, $sock, $method, $handler, $ctx) = @_;
  $ctx ||= {};

  my ($status_code, $status_text, $headers) = @{ $self->_read_head($sock, $ctx) };

  # Neither of these is a stream. A >= 400 body is a short JSON object naming
  # the failure and _request has to croak with it, so it is read whole and the
  # callback never sees it; a HEAD response has no body at all.
  if ($status_code >= 400 || (defined $method && uc($method) eq 'HEAD')) {
    return [$status_code, $status_text, $headers,
      $self->_read_body($sock, $headers, $method, $ctx), undef];
  }

  # Set only here, past the two branches above, so a timeout while reading an
  # error body is still reported in bytes rather than in units nothing
  # delivered. From this point on an expiry carries the callback's own summary
  # instead: the units are with the caller already, and how many is the part it
  # cannot know otherwise.
  local $ctx->{summary} = $handler->{summary};

  my $feed = $handler->{feed};
  my $more = 1;

  # There is deliberately nothing here to hand the callback the bytes of the
  # read that expires. karr k59 needed that hook because PerlIO's read() could
  # come back with data *and* EAGAIN at once, so a stall could land with the
  # whole response read and none of it fed; sysread cannot (karr k60). Every
  # byte reaches the callback in the pull that delivered it, and the pull that
  # expires delivers none -- so the property k59 established now holds by
  # construction instead of by rescue.

  if ($headers->{'transfer-encoding'}
      && lc($headers->{'transfer-encoding'}) eq 'chunked') {
    while ($more) {

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN


sub _uri_encode {
  my ($str) = @_;
  # Escape a character string by its UTF-8 bytes ('ü' -> %C3%BC, not %FC), and
  # a byte string as it stands. ord() on a character is not its wire byte: a
  # name or tag typed under `use utf8`, or read through a :utf8 layer, arrives
  # as characters and used to escape to a lone high byte or a bare codepoint
  # (%FC, %4E2D) that is not UTF-8 at all. But the encoding cannot be
  # unconditional: encode_json has already handed a HASH param (filters among
  # them) its UTF-8 octets, and re-encoding those would double them
  # (%C3%BC -> %C3%83%C2%BC). The utf8 flag is exactly that distinction -- on
  # for a decoded string, off for encode_json's output -- so a copy is encoded
  # only when it carries one, leaving the caller's own value untouched either
  # way.
  my $bytes = $str;
  utf8::encode($bytes) if utf8::is_utf8($bytes);
  $bytes =~ s/([^A-Za-z0-9\-_.~:\/])/sprintf("%%%02X", ord($1))/ge;
  return $bytes;
}

sub get {
  my ($self, $path, %opts) = @_;
  return $self->_request('GET', $path, %opts);
}


sub post {
  my ($self, $path, $body, %opts) = @_;
  $opts{body} = $body if defined $body;
  return $self->_request('POST', $path, %opts);
}


sub put {
  my ($self, $path, $body, %opts) = @_;
  $opts{body} = $body if defined $body;
  return $self->_request('PUT', $path, %opts);
}


sub delete_request {
  my ($self, $path, %opts) = @_;
  return $self->_request('DELETE', $path, %opts);
}


sub head {
  my ($self, $path, %opts) = @_;
  return $self->_request('HEAD', $path, %opts);
}


sub stream_frames {
  my ($self, $method, $path, %opts) = @_;

  my $tty = delete $opts{tty};

  if (my $cb = delete $opts{on_frame}) {
    # tty is a declaration here, not the hint it is on the buffered path. The
    # sniff below needs the whole body to decide, and the whole body is what a
    # callback stream does not have; so an unframed stream has to say so, and
    # anything not declared is required to be framed.
    return $self->_request($method, $path, %opts,
      $tty
        ? ( on_chunk => sub { $cb->({ stream => 'raw', data => $_[0] }, $_[1]) } )
        : ( on_frame => $cb ),
    );
  }

  my $body = $self->_request($method, $path, %opts, raw => 1);

  return [] unless defined $body && length $body;

  my $frames = $tty ? undef : $self->_demux_frames($body);

  return $frames if $frames;
  return [ { stream => 'raw', data => $body } ];
}


sub _demux_frames {
  my ($self, $body) = @_;

  my $len = length $body;
  my $pos = 0;
  my @frames;

  while ($pos < $len) {
    return undef if $len - $pos < 8;
    my ($type, $pad1, $pad2, $pad3, $size) = unpack 'C4 N', substr($body, $pos, 8);
    return undef if $type > $#STREAM_TYPE;
    return undef if $pad1 || $pad2 || $pad3;
    return undef if $len - $pos - 8 < $size;
    push @frames, {
      stream => $STREAM_TYPE[$type],
      data   => substr($body, $pos + 8, $size),
    };
    $pos += 8 + $size;
  }

  return undef unless @frames;
  return \@frames;
}


1;

__END__

=pod

=encoding UTF-8

=head1 NAME

API::Docker::Role::HTTP - HTTP transport role for Docker Engine API

=head1 VERSION

version 0.004

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

and a certificate with no key cannot be used. A directory holding only
F<ca.pem> is fine -- that is a daemon this client verifies but does not
authenticate to. A C<cert_path> that names nothing is a croak: it is read only
once TLS was asked for, and at that point a path pointing nowhere means the
caller believes certificates are in use that are not.

C<cert_path> defaults from C<DOCKER_CERT_PATH>, so on a machine that also runs
the C<docker> CLI it arrives set. Without C<< tls => 1 >> nothing reads it, so
that costs nothing; with it, pass C<< cert_path => undef >> to use the system
trust store instead of the CLI's private one.

=head3 TLS with no certificates at all

It means B<encrypt and verify against the system trust store>, not an error.

C<tls> asks for a connection that is encrypted and whose far end is
authenticated. It does not ask to authenticate this client, which is what the
files on disk are for, and treating the absence of a client certificate as a
missing precondition would conflate the two. The deployment with no
certificate files is real, and is the one this role's documentation used to
recommend before there was any TLS here: a terminator -- nginx, stunnel,
Traefik -- in front of the daemon, holding a publicly trusted certificate.
There is nothing for a C<cert_path> to point at in that setup.

It is also the safe reading rather than the lax one: verification stays on
either way, so the mode reached by configuring nothing is the verifying mode.
A stock C<dockerd --tlsverify> uses a private CA that the system store does
not have, and such a connection fails with a verification error naming exactly
that -- which is the intended outcome, not a silent downgrade. Point
C<cert_path> at the directory holding its F<ca.pem> and it verifies.

=head3 Turning verification off

C<< tls_insecure => 1 >>, and the name is the whole of the warning. It sets
C<SSL_VERIFY_NONE> and switches the hostname check off, which leaves a
connection that is encrypted against a passive listener and against nothing
else: whoever answers chooses the certificate, so anyone able to redirect the
connection reads and rewrites everything on it -- registry credentials,
image contents, the commands containers are started with.

It exists for a self-signed daemon certificate whose CA is genuinely not to
hand. The better answer to that is nearly always F<ca.pem>: a self-signed
certificate is its own CA and can be used as the anchor directly.

=head3 The dependency

L<IO::Socket::SSL> is a B<recommended>, not a required, dependency, and it is
loaded at the moment the first TLS connection is opened. It brings in
L<Net::SSLeay>, which is XS compiled against libssl, and the C<unix://>
transport -- local Docker, rootless Podman, the default -- never needs any of
it; requiring it would make this client unbuildable on a machine with no
OpenSSL headers for the sake of a transport it is not using. Without it,
C<< tls => 1 >> croaks naming the module and how to install it, at the same
point every other connection failure is reported.

=head2 read_timeout

Seconds of silence after which a request gives up and croaks with an
L<API::Docker::Error::Timeout>. C<undef> -- the default, and what every
existing caller gets -- means no timeout at all and is the behaviour this
distribution has always had. C<0> means the same and is the way to say it
explicitly, so a client carrying a default can be opted out of per request.

    my $docker = API::Docker->new(read_timeout => 30);
    $docker->system->using(read_timeout => 0)->events;   # this one may wait

Per request it is an option of L</get>, L</post>, L</put>, L</delete_request>
and L</head>. A resource class carries it through
L<API::Docker::Role::Using/using>, which clones the class rather than taking
it per method -- up for a slow endpoint, down for a stream that should not
stall, off with C<0>.

See L</"Bounding a request that never ends"> for what it does and does not
cover, and L<API::Docker/"What a timeout covers"> for the same question
asked of both bounds at once.

=head2 connect_timeout

Seconds after which opening the connection gives up and croaks with an
L<API::Docker::Error::Timeout> whose C<< ->phase >> is C<'connect'>. C<undef>
-- the default, and what every existing caller gets -- means no bound and is
the behaviour this distribution has always had; C<0> means the same and is the
way to say it explicitly.

    my $docker = API::Docker->new(connect_timeout => 5);
    $docker->system->using(connect_timeout => 0)->version;  # may wait

Separate from L</read_timeout> rather than folded into it, because the two
bound different things and want different numbers: a connect is either
immediate or broken, while a read is waiting on work the daemon has to do.

Per request it is an option of L</get>, L</post>, L</put>, L</delete_request>
and L</head>. A resource class carries it through
L<API::Docker::Role::Using/using>. See L</"Bounding the connection itself">
for what it does on each transport, which is not the same thing on all
three.

=head2 get

    my $data = $client->get($path, %opts);

Perform HTTP GET request. Returns decoded JSON or raw response body.

Options:

=over

=item * C<params> - HashRef of query parameters; a HashRef value is JSON-encoded

=item * C<headers> - HashRef of extra HTTP headers, e.g.
C<< { 'X-Registry-Auth' => $b64 } >>

=item * C<ndjson> - Parse the body as newline-delimited JSON and always
return an ArrayRef of events, even for a stream carrying a single object.
Named for the format rather than C<stream>, which is already a query
parameter of C</events> and C</containers/{id}/stats>. An C<errorDetail>
event in such a stream croaks; see L</"Failure inside a 200 response">

=item * C<croak_on_error> - Default true, and only consulted with
C<< ndjson => 1 >>. Set it false for a stream whose objects are engine data
rather than the outcome of one operation -- C</events> is the only such
endpoint here

=item * C<raw> - Never decode the body; return the response bytes verbatim

=item * C<response> - HashRef the status line and the response headers are
written into; see L</"Reading the status line and the response headers">

=item * C<on_event>, C<on_frame>, C<on_chunk> - CodeRef called with each unit
of the response as it arrives, instead of the body being buffered and
returned. At most one of the three; see L</"Streaming a response as it
arrives">

=item * C<read_timeout> - Seconds of silence after which this request gives up
and croaks with an L<API::Docker::Error::Timeout>. Overrides the
L</read_timeout> attribute; C<0> means no timeout. See L</"Bounding a request
that never ends">

=item * C<connect_timeout> - Seconds after which opening the connection gives
up and croaks with an L<API::Docker::Error::Timeout> whose C<< ->phase >> is
C<'connect'>. Overrides the L</connect_timeout> attribute; C<0> means no
bound. See L</"Bounding the connection itself">

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN


Options: C<params>, C<headers> and C<response> as for L</get>.

=head2 stream_frames

    my $frames = $client->stream_frames('GET', "/containers/$id/logs", %opts);

Perform a request against one of the engine's framed endpoints
(C<< /containers/{id}/logs >>, C<< /exec/{id}/start >>) and return an ArrayRef
of frames:

    [ { stream => 'stdout', data => "OUT\n" },
      { stream => 'stderr', data => "ERR\n" } ]

C<stream> is C<stdout>, C<stderr> or C<stdin> for a multiplexed stream, and
C<raw> for an unframed one. It is always a plain string, so callers never need
a defined-check. Joining the payloads gives the plain text:

    my $text = join '', map { $_->{data} } @$frames;

The response body is never JSON-decoded, so a container printing JSON lines is
returned verbatim.

Options are those of C<_request> (C<params>, C<body>, C<headers>), plus:

=over

=item * C<tty> - Skip demultiplexing and return the body as a single C<raw>
frame. Set it when the container or exec instance was created with a TTY and
its output is binary; see L</"Detecting a framed stream"> for why.

=item * C<on_frame> - CodeRef called with each frame as it arrives instead of
the whole ArrayRef being returned at the end; see below.

=back

=head2 Following a framed stream

With C<on_frame> the frames are handed over as they arrive and the return
value is the summary HashRef described in L</"Streaming a response as it
arrives">, not an ArrayRef:

    my $summary = $client->stream_frames('GET', "/containers/$id/logs",
      params   => { follow => 1, stdout => 1, stderr => 1 },
      on_frame => sub {
        my ($frame, $stop) = @_;
        print $frame->{data};
        $stop->() if $frame->{data} =~ /listening on/;
      },
    );

This is the only way to use C<< follow => 1 >> at all: without it the request
does not return until the container exits.

The frame shape is the same either way, C<tty> included -- a TTY stream
arrives as a series of C<< { stream => 'raw', ... } >> frames rather than the
single one the buffered path builds, so a caller still never branches on it.

C<tty> is a declaration here rather than the hint it is on the buffered path.
Deciding framing from the bytes needs the whole body, which is precisely what
is not being kept; so an unframed stream must say so, and one that does not
and is not framed croaks instead of inventing frames from its payload.

=head2 Detecting a framed stream

A container created without a TTY produces the Docker stream format -- an
8-byte header per frame (byte 0 the stream type, bytes 4-7 a big-endian uint32
payload length) followed by that many payload bytes. With a TTY there is no
header and the payload is raw pty output.

The engine is supposed to distinguish the two with the response C<Content-Type>
(C<application/vnd.docker.multiplexed-stream> against
C<application/vnd.docker.raw-stream>), but that signal is not dependable.
Measured against Podman 5.4.2 (API 1.41): C<< GET /containers/{id}/logs >>
sends no C<Content-Type> at all, for either kind of container, and
C<< POST /exec/{id}/start >> sends C<application/vnd.docker.raw-stream> for
both -- including the non-TTY exec whose body is in fact multiplexed. Trusting
the header would therefore hand frame headers to the caller on that engine.

The framing is decided from the bytes instead. The body is walked as frames:
each header must have a stream type of 0, 1 or 2, three zero bytes after it,
and a payload length that leaves at least that many bytes in the buffer. The
body is treated as framed only when the walk consumes it exactly and yields at
least one frame; anything else is returned as a single C<raw> frame.

This can be fooled in one direction only. Raw TTY output is misread as framed
if it begins with a byte no greater than C<0x02>, followed by three NUL bytes
and a length that happens to chain exactly to the end of the body. Text output
cannot do that -- a printable character is C<0x20> or above -- so it takes
binary output from a TTY-allocated container. Pass C<< tty => 1 >> for that
case. The reverse mistake cannot happen silently: a genuine frame stream is
only ever reported as raw when its final frame is truncated, which needs the
daemon to close the connection mid-frame.

=head1 SEE ALSO

=over

=item * L<API::Docker> - Main client using this role

=item * L<API::Docker::Error::HTTP> - Raised for a status of 400 or above;
carries the status code

=item * L<API::Docker::Error::Stream> - Raised for a failure reported inside
a 200 event stream

=back

=head1 SUPPORT

=head2 Issues

Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/p5-api-docker/issues>.

=head1 CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

=head1 AUTHOR



( run in 1.046 second using v1.01-cache-2.11-cpan-a49fcb8fa48 )