API-Docker
view release on metacpan or search on metacpan
lib/API/Docker/API/Containers.pm view on Meta::CPAN
}
sub stop {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{t} = $opts{timeout} if defined $opts{timeout};
$params{signal} = $opts{signal} if defined $opts{signal};
return $self->_state_change("/containers/$id/stop",
params => \%params,
%{ $self->_request_options },
);
}
sub restart {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{t} = $opts{timeout} if defined $opts{timeout};
return $self->_state_change("/containers/$id/restart",
params => \%params,
%{ $self->_request_options },
);
}
sub kill {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{signal} = $opts{signal} if defined $opts{signal};
return $self->client->post("/containers/$id/kill", undef,
params => \%params,
%{ $self->_request_options },
);
}
sub remove {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{v} = $opts{volumes} ? 1 : 0 if defined $opts{volumes};
$params{force} = $opts{force} ? 1 : 0 if defined $opts{force};
$params{link} = $opts{link} ? 1 : 0 if defined $opts{link};
return $self->client->delete_request("/containers/$id",
params => \%params,
%{ $self->_request_options },
);
}
sub logs {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{follow} = $opts{follow} ? 1 : 0 if defined $opts{follow};
$params{stdout} = defined $opts{stdout} ? ($opts{stdout} ? 1 : 0) : 1;
$params{stderr} = defined $opts{stderr} ? ($opts{stderr} ? 1 : 0) : 1;
$params{since} = $opts{since} if defined $opts{since};
$params{until} = $opts{until} if defined $opts{until};
$params{timestamps} = $opts{timestamps} ? 1 : 0 if defined $opts{timestamps};
$params{tail} = $opts{tail} if defined $opts{tail};
# exists, not truth: an unset callback is a caller bug, and quietly falling
# back to the buffered path for it would answer a follow with a hang.
return $self->client->stream_frames('GET', "/containers/$id/logs",
params => \%params,
defined $opts{tty} ? ( tty => $opts{tty} ) : (),
%{ $self->_request_options },
exists $opts{on_frame} ? ( on_frame => $opts{on_frame} ) : (),
);
}
# The guard behind attach's require_running, and a pre-flight check is all it
# is: it asks the engine what the container is doing now, and the container may
# still stop between that answer and the attach landing. That race is not
# closable from a client -- the engine offers no attach-if-running -- and the
# check earns its round trip anyway, because the condition it tests is exactly
# the condition that does the damage. Measured on Podman 5.4.2 (API 1.41) and
# Docker 29.7.2 (API 1.55), one container per row, each exiting with status 4:
#
# attach to an ALREADY-EXITED container Podman: status destroyed
# attach while RUNNING, exits under the call Podman: status intact (4)
# either of those Docker: status intact (4)
#
# So "running at the moment of the call" is the whole of the condition. A
# container that is still running when attach is sent stays safe even when it
# exits a millisecond later, which is why the pre-flight answer is worth having
# despite being one round trip stale.
#
# It fails open on anything it does not recognise: a State it cannot read is
# not evidence that the container is stopped, and a guard that is unsure must
# not be the thing that breaks a working call.
sub _assert_container_running {
my ($self, $id) = @_;
# A State the model could not use is one more shape the check does not
# recognise, and it arrives as one: the generated classes type their fields
# from the swagger, and a State that is not the object
# ContainerInspectResponse declares -- the bare status string of the list
# shape, say -- leaves ->state unset and keeps the raw value in
# unknown_fields rather than taking the response down with it. So there is
# nothing to catch here; an error that does reach this line, the daemon's
# own 404 included, is the caller's and goes up.
my $inspected = $self->inspect($id);
# An API::Docker::Type::ContainerState, or undef where the daemon sent no
# State at all -- which is the "does not recognise" case above, not a stopped
# container.
my $state = $inspected->state;
return unless blessed($state) && defined $state->running;
return if $state->running;
my $status = $state->status;
$status = 'not running' unless defined $status && length $status;
croak __PACKAGE__ . '->attach refused: container ' . $id . ' is ' . $status
. '. Attaching to a container that is not running destroys its exit status '
. 'on Podman, irrecoverably -- the engine keeps no copy -- and with '
. 'stream => 1 never returns on either engine. Read its output with logs() '
. 'instead, or pass require_running => 0 to attach anyway';
}
sub attach {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
# Pre-flight, and deliberately before the request is built: the call itself
# is what destroys the exit status, so a check made afterwards could only
# report the loss rather than prevent it. Turned off it costs nothing at all,
# not even the round trip.
my $require_running
= defined $opts{require_running} ? $opts{require_running} : 1;
# The pre-flight is a request the caller never wrote, and one that hangs is
# exactly what a bound was set to prevent -- so it carries the same one. It
# does that by itself here: the check runs on $self, which is the clone
# ->using returned when there was one (karr k74).
$self->_assert_container_running($id) if $require_running;
my %params;
$params{stream} = $opts{stream} ? 1 : 0;
$params{logs} = defined $opts{logs} ? ($opts{logs} ? 1 : 0) : 1;
$params{stdout} = defined $opts{stdout} ? ($opts{stdout} ? 1 : 0) : 1;
$params{stderr} = defined $opts{stderr} ? ($opts{stderr} ? 1 : 0) : 1;
$params{stdin} = $opts{stdin} ? 1 : 0 if defined $opts{stdin};
return $self->client->stream_frames('POST', "/containers/$id/attach",
params => \%params,
defined $opts{tty} ? ( tty => $opts{tty} ) : (),
%{ $self->_request_options },
exists $opts{on_frame} ? ( on_frame => $opts{on_frame} ) : (),
);
}
sub top {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{ps_args} = $opts{ps_args} if defined $opts{ps_args};
return $self->client->get("/containers/$id/top",
params => \%params,
%{ $self->_request_options },
);
}
# The Podman compatibility path, and deliberately only that. Podman answers
# GET /containers/{id}/stats for a container that is not running with an error
# object inside a response it has already committed to 200 --
# {"cause":"container is stopped","message":"container is stopped",
# "response":500}, chunked, for the one-shot call and for stream => 1 alike
# (measured on Podman 5.4.2, API 1.41). Neither guard in the transport sees
# it: the >= 400 croak reads the status line, which says 200, and the stream
# check triggers on errorDetail, which this object does not carry. Docker
# 29.7.2 (API 1.55) answers the same call with a real, zero-filled reading and
# never produces this shape at all -- so this belongs here, next to the one
# endpoint and the one engine it was measured on, and not in Role::HTTP, where
# it would be a heuristic on daemon prose sitting under all twelve modules.
#
# All four clauses have to hold. The narrowness is the point, not an accident:
#
# 1. the status was 2xx. True by construction on both paths this guards:
# _request croaks before returning for >= 400, and
# _read_streaming_response reads such a body whole rather than handing it
# to a callback, so nothing that failed the status line reaches here
# 2. the decoded value is a HashRef
# 3. it carries all three of cause, message and response, exactly
# lower-cased. Never case-insensitively, and this is the counter-example
# that fixes it: POST /containers/{id}/wait answers its SUCCESS case with
# a top-level `Error` key -- Podman sends "Error":null on every wait --
# so a rule matching /error/i would turn every successful wait into a
# failure. Measured over fifteen read endpoints per engine and every
# fixture in t/fixtures: no 2xx body on either engine carries even one of
# these three lower-cased at the top level
# 4. `response` is a non-ref scalar reading as an integer >= 400. That is
# what makes the rule self-evidencing rather than a guess about prose:
# the object is an error because Podman says so inside it. Known miss:
# Podman's GET /plugins answers {"cause":"","message":"Path ... is not
# supported","response":0}, which clause 4 rejects -- but it arrives with
# 404 on the status line and the transport croaks it long before this
# runs, so the miss goes in the conservative direction and costs nothing
#
# A bare {message => ...} deliberately does not trigger: that is the ordinary
# Docker error body, and treating one inside a 2xx as a failure would be a
lib/API/Docker/API/Containers.pm view on Meta::CPAN
return $self->_decode_path_stat(\%response);
}
sub prune {
my ($self, %opts) = @_;
my %params;
$params{filters} = $self->_normalise_filters($opts{filters})
if defined $opts{filters};
return $self->client->post('/containers/prune', undef,
params => \%params,
%{ $self->_request_options },
);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
API::Docker::API::Containers - Docker Engine Containers API
=head1 VERSION
version 0.004
=head1 SYNOPSIS
my $docker = API::Docker->new;
# List containers
my $containers = $docker->containers->list(all => 1);
for my $container (@$containers) {
say $container->id;
say $container->status;
}
# Create and start a container
my $result = $docker->containers->create(
Image => 'nginx:latest',
name => 'my-nginx',
ExposedPorts => { '80/tcp' => {} },
);
$docker->containers->start($result->{Id});
# Inspect container details
my $container = $docker->containers->inspect($result->{Id});
say $container->name;
# Stop and remove
$docker->containers->stop($result->{Id}, timeout => 10);
$docker->containers->remove($result->{Id});
# View logs (ArrayRef of { stream => 'stdout'|'stderr'|'raw', data => ... })
my $frames = $docker->containers->logs($result->{Id}, tail => 100);
my $text = join '', map { $_->{data} } @$frames;
# Attach one-way: replays the same frames and returns (stream => 0 by
# default -- stream => 1 on a stopped container never returns). On Podman,
# attaching to a container that has ALREADY EXITED destroys its exit
# status; use logs() for that case, see attach()
my $attached = $docker->containers->attach($result->{Id});
# Copy a file out, and a tar archive in (what docker cp is built on)
my $tar = $docker->containers->get_archive($result->{Id},
path => '/etc/hostname');
$docker->containers->put_archive($result->{Id}, $tar, path => '/tmp');
=head1 DESCRIPTION
This module provides methods for managing Docker containers including creation,
lifecycle operations (start, stop, restart), inspection, logs, and more.
C<list> and C<inspect> return generated L<API::Docker::Type> objects carrying
the convenience methods of L<API::Docker::Role::Entity::Container>, so
C<< $container->start >> and C<< $container->logs >> work on either. Which
class each returns, and where the two disagree, is below.
Accessed via C<< $docker->containers >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->containers->using(read_timeout => 5) >>.
=head2 The two container shapes
The daemon describes a container two ways and the swagger has two
definitions for it, so this class returns two classes:
=over
=item * L</list> returns L<API::Docker::Type::ContainerSummary> objects --
one per entry of C<GET /containers/json>.
=item * L</inspect> returns an L<API::Docker::Type::ContainerInspectResponse>
-- the body of C<GET /containers/{id}/json>.
=back
They overlap but do not line up, and the field names are the swagger's own
spelling in snake_case (C<Id> is C<< ->id >>, C<SizeRootFs> is
C<< ->size_root_fs >>). The differences worth knowing before reading a value
off the wrong one:
=over
=item * C<< ->image >> is the name the container was created from on a
summary (C<nginx:latest>) and the resolved C<sha256:> digest on an inspect.
A summary reports that digest separately as C<< ->image_id >>; an inspect
has no such field.
=item * C<< ->created >> is an integer Unix epoch on a summary and an
RFC 3339 string on an inspect. Same field name, two types -- C<Int> and
C<Str> in the model, which is the swagger's own answer, not a normalisation
this client applies.
lib/API/Docker/API/Containers.pm view on Meta::CPAN
So C<kill> is a state change for a paused container on Docker and is not one
on Podman -- with the same 204 on both.
Killing a container that is not running -- stopped, exited or just created --
croaks B<409>; it does not return a falsy value. An unknown container ID
croaks B<404>. Neither is reachable through a return value, and no case
answers 304: moby's swagger for this endpoint (C<operationId: ContainerKill>)
documents only 204, 404, 409 and 500.
The B<text> of either is engine prose, so branch on
L<API::Docker::Error::HTTP/status> and not on the message. For one and the
same stopped container:
=over
=item * Docker -- C<cannot kill container: E<lt>nameE<gt>: container E<lt>idE<gt> is
not running>
=item * Podman -- C<can only kill running containers. E<lt>idE<gt> is in state
exited: container state improper>. C<container state improper> is Podman's
separate C<cause> field, reachable as C<< $err->data->{cause} >>, not a
phrase Docker uses anywhere
=back
The 404 differs too, and on Docker it differs I<per endpoint>: C<kill>
against a missing ID answers C<cannot kill container: E<lt>nameE<gt>: No such
container: E<lt>nameE<gt>> where L</inspect> answers the bare C<No such container:
E<lt>nameE<gt>>. Podman sends one sentence for both.
Options:
=over
=item * C<signal> - Signal to send (default C<SIGKILL>)
=back
=head2 remove
$containers->remove($id, force => 1, volumes => 1);
Remove a container.
Options:
=over
=item * C<force> - Force removal (kill if running)
=item * C<volumes> - Remove associated volumes
=item * C<link> - Remove specified link
=back
=head2 logs
my $frames = $containers->logs($id, tail => 100, timestamps => 1);
# stdout and stderr, in the order the engine emitted them
my $text = join '', map { $_->{data} } @$frames;
# stderr only
my @errors = grep { $_->{stream} eq 'stderr' } @$frames;
Get container logs. Returns an ArrayRef of frames, each a HashRef with
C<stream> and C<data>:
[ { stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" } ]
A container created without a TTY multiplexes stdout and stderr into a single
framed stream, and this method demultiplexes it -- without that, the 8-byte
frame headers end up in the caller's log text. A container created B<with> a
TTY writes to one pty and the engine sends no frame headers, so its whole
output arrives as a single frame with C<< stream => 'raw' >>: with a TTY there
is no stdout/stderr distinction left to report. C<stream> is always a plain
string, so C<< $_->{stream} eq 'stderr' >> is safe on any frame.
Framing is detected from the response bytes, because the engine's
C<Content-Type> cannot be trusted for it -- see
L<API::Docker::Role::HTTP/"Detecting a framed stream"> for the rule and its one
failure mode.
Options:
=over
=item * C<follow> - Keep the connection open and send new output as the
container writes it. Only usable with C<on_frame>; see below
=item * C<stdout> - Include stdout (default 1)
=item * C<stderr> - Include stderr (default 1)
=item * C<since> - Show logs since timestamp
=item * C<until> - Show logs before timestamp
=item * C<timestamps> - Include timestamps
=item * C<tail> - Number of lines from end (e.g., C<100> or C<all>)
=item * C<tty> - Set to 1 when the container was created with a TTY and its
output is binary, to skip demultiplexing. Not needed for text output. The
container's own setting is C<Config.Tty> from C<< $containers->inspect($id) >>.
With C<on_frame> it is a declaration rather than a hint; see below
=item * C<on_frame> - CodeRef called with each frame as it arrives, instead of
the ArrayRef being collected and returned; see below
=back
=head2 Following the log
C<< follow => 1 >> asks the daemon to keep sending as the container writes.
Pass C<on_frame> with it and the frames are handed over as they arrive:
my $summary = $containers->logs($id,
follow => 1,
tail => 0,
on_frame => sub {
my ($frame, $stop) = @_;
print $frame->{data};
$stop->() if $frame->{data} =~ /listening on/;
},
);
$summary; # { delivered => 4, stopped => 1 }
With a callback the return value is that summary HashRef, not the frames:
C<delivered> is how many went to the callback, C<stopped> is 1 when the
callback ended the stream and 0 when the daemon did. Nothing is accumulated --
a followed log is unbounded by construction, and the callback has been handed
every frame already. See
L<API::Docker::Role::HTTP/"Streaming a response as it arrives">.
B<Without a callback, C<< follow => 1 >> blocks> until the container exits or
the daemon closes the connection, because the whole response is read before
anything is parsed. Use it with C<on_frame> or not at all.
C<tty> means something stronger on this path. The buffered path decides
framing by walking the whole body (see
L<API::Docker::Role::HTTP/"Detecting a framed stream">), which is exactly what
a streamed one does not have; so with C<on_frame> the flag is a promise about
the container rather than a hint, and an undeclared stream that turns out not
to be framed croaks instead of being handed back raw. Read C<Config.Tty> from
C<< $containers->inspect($id) >> and pass it. The frame shape is the same
either way -- a TTY stream arrives as a series of C<< stream => 'raw' >>
frames rather than the single one the buffered path builds.
=head2 attach
my $frames = $containers->attach($id);
my $text = join '', map { $_->{data} } @$frames;
Attach to a container's streams and return everything they produced, as an
ArrayRef of frames in the same shape L</logs> returns:
[ { stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" } ]
A container created without a TTY multiplexes its output into one framed
stream, which this method demultiplexes; one created with a TTY arrives as a
single C<< stream => 'raw' >> frame. See L</logs> and
L<API::Docker::Role::HTTP/"Detecting a framed stream">.
B<The container must be running.> Attaching to one that has already exited
destroys its exit status on Podman, so this method checks first and croaks
rather than attaching -- read the output of a finished container with
L</logs>. Both halves of that are worth knowing before the call: see
L</"On Podman this destroys a stopped container's exit status"> and
L</"This method refuses a container that is not running">.
=head2 On Podman this destroys a stopped container's exit status
B<Attaching to a container that has already exited loses its exit code on
Podman, and nothing reports it.> Measured before and after a single attach
against one container that exited with 4, on Podman 5.4.2 (API 1.41) and
Docker 29.7.2 (API 1.55), same machine:
PODMAN before inspect: exited 4 wait: { StatusCode => 4 }
PODMAN after inspect: created 0 wait: { StatusCode => -1 }
DOCKER before inspect: exited 4 wait: { StatusCode => 4 }
DOCKER after inspect: exited 4 wait: { StatusCode => 4 }
Podman reverts the container to C<created>, resets C<ExitCode> to 0, and
answers a later L</wait> with the sentinel C<-1> inside a 200. The real value
is gone from the engine; there is nothing to read it back from. B<All three
variants do it> -- C<< stream => 1 >> with and without C<logs>, and the
C<< stream => 0, logs => 1 >> this method now sends by default, which is the
one that returns cleanly in milliseconds. It is the call that does it, not
the hang.
L</logs> does B<not> do it, and Docker does not do it at all. So on Podman
the sequence "attach to collect the output, then L</wait> for the exit code"
cannot work: read the output with L</logs> instead, or take the exit code
before attaching.
=head2 This method refuses a container that is not running
Because of the above, C<attach> asks L</inspect> whether the container is
running and B<croaks instead of attaching> when it is not:
API::Docker::API::Containers->attach refused: container x is exited. ...
C<< require_running => 0 >> turns that off and attaches anyway; the check is
then not performed at all, so opting out costs no round trip either.
With the guard off, the hang this section exists to explain becomes reachable:
attaching to a container that has already exited never returns on rootless
Podman (measured 5.4.2, still true on 5.8.4, API 1.44). A bound on the
resource class is how to survive that call instead of blocking on it forever:
$docker->containers->using(read_timeout => 2)
->attach($id, require_running => 0);
See L<API::Docker::Role::Using> and
L<API::Docker::Role::HTTP/"Bounding a request that never ends">.
B<What the check does not do is close the race.> It is a pre-flight question,
lib/API/Docker/API/Containers.pm view on Meta::CPAN
that is what C<docker attach> uses, and it is what lets a caller type into the
container's stdin. Sent B<without> those headers -- which is what this method
does -- the engine answers B<200> and streams the container's output one way,
in exactly the frames L</logs> returns.
This method implements the second one only, because the transport here buffers
a whole response before returning it (see
L<API::Docker::Role::HTTP/"What the transport does not do">). Two consequences
a caller has to plan around:
=over
=item * B<You cannot write to the container.> C<< stdin => 1 >> is passed to
the engine, but this client sends no bytes after the request headers and then
reads until the daemon closes, so there is no moment at which input could be
supplied. Use L<API::Docker::API::Exec> to run something interactive-shaped,
or wait for the upgraded variant
=item * B<Without a callback it returns when the stream ends, not before.>
With C<< stream => 1 >>, attaching to a container that keeps running blocks
until it exits or the daemon closes the connection -- and on a container that
is B<not> running it never returns at all, see
L</"The defaults follow the engine"> below. Pass C<on_frame> to read the
stream as it arrives and stop where you like, exactly as L</logs> does under
L</"Following the log">; the return value is then the summary HashRef
C<< { delivered => N, stopped => 0|1 } >> rather than the frames, and C<tty>
becomes a declaration the transport takes at its word -- an undeclared
unframed stream croaks. For a running container that need not be attached to,
L</logs> with C<tail> reads the same output and returns immediately
=back
C</containers/{id}/attach/ws>, the WebSocket variant, is not implemented
either.
=head2 The defaults follow the engine
C<stream> defaults to B<0> -- the engine's own default -- and C<logs> to
B<1>, which is the one flag that keeps the call useful without it. So
C<< $containers->attach($id) >> B<replays> what the container has written and
returns.
B<This is a change.> Up to and including the previous release C<stream>
defaulted to 1, so the same call opened an open-ended subscription; a caller
who wants the live stream now has to ask for it with C<< stream => 1 >>.
The reason is that the subscription has exactly one terminator: the container
ending. C<stream> means I<stream attached streams from the time the request
was made onwards>, so on a container that has B<already exited> that
terminator is in the past and will not happen again. attach also hijacks the
connection -- the response carries no C<Content-Length> and no chunked
terminator -- so HTTP framing cannot signal the end either. The transport
reads until EOF, there is no EOF, and the call hangs. C<on_frame> does not
help: nothing will ever call C<< $stop->() >>.
Measured on Podman 5.4.2 (API 1.41), all four against one and the same
container:
=over
=item * C<?logs=1&stdout=1&stderr=1&stream=0>, exited container -- 200, the
frames, connection closed after 13 ms
=item * C<?logs=1&stdout=1&stderr=1&stream=1>, exited container -- 200, the
same frames, then hangs
=item * the same with C<Upgrade: tcp> -- 101 UPGRADED, the same frames, still
hangs
=item * C<?stream=1> while the container is still B<running> and exits three
seconds later -- closes cleanly after 3 s
=back
B<Docker does exactly the same, and that is measured now too.> Against Docker
29.7.2 (API 1.55): C<?logs=1&stdout=1&stderr=1&stream=1> on an exited
container was still open when a 10 s probe gave up, and
C<?logs=1&stdout=1&stderr=1&stream=0> answered 200 with byte-identical frames
and closed in half a millisecond. So the hang is not a Podman quirk to be
worked around -- it is what both engines do with a subscription whose only
terminator is already in the past, on an endpoint whose reference promises a
close in neither direction. It is unspecified behavior on both, which is the
case for the C<< stream => 0 >> default rather than an argument against it.
One more measured difference: Podman refuses C<< stream => 0 >> together with
C<< logs => 0 >> outright, with B<400> C<at least one of Logs or Stream must
be set>, rather than answering an empty 200.
Options:
=over
=item * C<stream> - Subscribe to what the container writes from the time of
the request onwards. Default B<0>, which is the engine's own default.
C<< stream => 1 >> on a container that is not running never returns; see
L</"The defaults follow the engine">
=item * C<logs> - Replay what the container has already written. Default
B<1>, so the call returns something without subscribing; combined with
C<< stream => 1 >> the replay comes first and then transitions seamlessly
into the live output. C<< logs => 0 >> without C<< stream => 1 >> is the
combination the engine refuses (400 on Podman)
=item * C<stdout> - Attach stdout. Default 1 (engine default: false)
=item * C<stderr> - Attach stderr. Default 1 (engine default: false)
=item * C<stdin> - Attach stdin. Sent as asked, but nothing can be written to
it here; see above
=item * C<tty> - Set to 1 when the container was created with a TTY and its
output is binary, to skip demultiplexing. Same meaning as in L</logs>, and
with C<on_frame> the same promise
=item * C<on_frame> - CodeRef called with each frame as it arrives, instead of
the ArrayRef being collected and returned. Same contract as in L</logs>
=item * C<require_running> - Ask L</inspect> whether the container is running
first, and croak rather than attach when it is not. Default B<1>. Set to 0 to
attach to a stopped container anyway, which also skips the round trip; see
L</"This method refuses a container that is not running"> for what the check
does and does not guarantee
=back
=head2 top
my $processes = $containers->top($id, ps_args => 'aux');
List running processes in a container. Returns hashref with C<Titles> and C<Processes> arrays.
Options:
=over
=item * C<ps_args> - Arguments passed to C<ps> inside the container, e.g.
C<'aux'>. Omitted, the engine uses its own default
=back
=head2 stats
my $stats = $containers->stats($id);
Get container resource usage statistics (CPU, memory, network, I/O). With no
options this is the one-shot call it always was: a single reading, returned as
a HashRef.
For a container that is B<not running> the two engines answer differently and
neither says so in the status line -- on Podman this method croaks, on Docker
it returns zeros that look like a reading. See
L</"A container that is not running: a croak on Podman, zeros on Docker">
before calling it on a container that may have stopped.
=head2 Following the stats
C<< stream => 1 >> asks the engine for a reading per sampling cycle for as long
as the container runs. Pass C<on_event> with it and the readings are handed
over as they arrive:
my $summary = $containers->stats($id,
stream => 1,
on_event => sub {
my ($stats, $stop) = @_;
printf "%.1f MB\n", $stats->{memory_stats}{usage} / 1024 ** 2;
$stop->() if ++$seen >= 5;
( run in 0.907 second using v1.01-cache-2.11-cpan-54e63673c56 )