API-Docker
view release on metacpan or search on metacpan
lib/API/Docker/API/Containers.pm view on Meta::CPAN
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,
lib/API/Docker/API/Containers.pm view on Meta::CPAN
params => \%params,
raw_body => $raw,
content_type => 'application/x-tar',
%{ $self->_request_options },
);
}
sub stat_archive {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
croak "Path required" unless defined $opts{path} && length $opts{path};
my %response;
$self->client->head("/containers/$id/archive",
params => { path => $opts{path} },
response => \%response,
%{ $self->_request_options },
);
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.
lib/API/Docker/API/Containers.pm view on Meta::CPAN
=head2 list
my $containers = $containers->list(%opts);
List containers. Returns an ArrayRef of
L<API::Docker::Type::ContainerSummary> objects -- see L</"The two container
shapes"> for what a summary carries and L</inspect> does not.
Options:
=over
=item * C<all> - Show all containers (default shows just running)
=item * C<limit> - Limit results to N most recently created containers
=item * C<size> - Include size information
=item * C<filters> - HashRef of filter name to ArrayRef of string values, e.g.
C<< { status => ['running'], label => ['stage=build'] } >>. Shape-checked and
normalised by L<API::Docker::Role::Filters>
=back
=head2 create
my $result = $containers->create(
Image => 'nginx:latest',
name => 'my-nginx',
Cmd => ['/bin/sh'],
Env => ['FOO=bar'],
);
Create a new container. Returns hashref with C<Id> and C<Warnings>.
The C<name> parameter is extracted and passed as query parameter. All other
parameters are Docker container configuration (see Docker API documentation).
Common config keys: C<Image>, C<Cmd>, C<Env>, C<ExposedPorts>, C<HostConfig>.
Boolean flags may be given as a Perl C<1>/C<0> or as a JSON boolean; either
goes out as a real JSON C<true>/C<false>, which the engine's body type-check
requires. This applies to the top-level flags (C<Tty>, C<OpenStdin>,
C<AttachStdin>, C<AttachStdout>, C<AttachStderr>, C<StdinOnce>,
C<NetworkDisabled>, C<ArgsEscaped>) and to the C<HostConfig> flags
(C<Privileged>, C<PublishAllPorts>, C<ReadonlyRootfs>, C<AutoRemove>, C<Init>,
C<OomKillDisable>).
=head2 inspect
my $container = $containers->inspect($id);
Get detailed information about a container. Returns an
L<API::Docker::Type::ContainerInspectResponse> -- see L</"The two container
shapes">.
=head2 start
$containers->start($id);
say 'was already running' unless $containers->start($id);
Start a container. Returns 1 when the container was started and 0 when it was
already running: the engine answers a state change with 204 and a no-op with
B<304 Not Modified>, and both carry an empty body, so until now both came back
as C<undef>.
The no-op keeps the falsy value this method always returned -- 0 where it used
to be C<undef> -- so a caller that ignores the return or tests it for falseness
is unaffected; only a caller testing C<defined> sees a difference. A failure is
still a croak, never a 0.
=head2 stop
$containers->stop($id, timeout => 10);
say 'was already stopped' unless $containers->stop($id);
Stop a container. Returns 1 when the container was stopped and 0 when it was
already stopped -- the engine answers the no-op with B<304 Not Modified>. See
L</start> for what that 0 replaces.
Options:
=over
=item * C<timeout> - Seconds to wait before killing (default 10)
=item * C<signal> - Signal to send (default SIGTERM)
=back
=head2 restart
$containers->restart($id, timeout => 10);
Restart a container. Optionally specify C<timeout> in seconds.
Reports 1/0 like L</start>, but a restart has no no-op state to report: the
engine restarts a stopped container as readily as a running one. Measured
against Podman 5.4.2 (API 1.41) it answers 204 in both cases, and the Docker
Engine API documents no 304 for this endpoint either, so 0 is not expected
here. The value is reported the same way rather than specially, so an engine
that does answer 304 is not silently read as a change.
=head2 kill
$containers->kill($id, signal => 'SIGKILL');
$containers->kill($id, signal => 'SIGUSR1'); # not necessarily a stop
Send a signal to a container. Default signal is C<SIGKILL>.
Returns nothing -- unlike L</start>, L</stop>, L</restart>, L</pause> and
L</unpause>, which report 1/0 through their shared C<_state_change> path.
Those methods have two outcomes worth telling apart: a change (204) and a
no-op (304, where the engine sends one). C<kill> has only one, because
C<_request> croaks on any C<< status >= 400 >>, so the B<409> a non-running
container gets back never reaches this method's C<return>. A boolean with a
single possible value is not worth adding.
More importantly, B<204 does not mean the container stopped.> Measured on
B<both> engines -- Docker 29.7.2 (API 1.55) and rootless Podman 5.4.2 (API
1.41), same machine, identical behavior: sending a signal the container traps
or ignores -- C<< signal => 'SIGUSR1' >> against a process with a handler
installed for it -- is delivered, the container keeps running, the handler's
output turns up in L</logs>, and the engine still answers 204 exactly as it
does for a signal that does end the process. A caller that needs to know
whether the container is still running after a C<kill> has to ask
L</inspect>; that is also why this returns nothing rather than a plain C<1>
-- a 1 here would claim a state change that a trapped signal never made.
B<A paused container is where the two engines part.> Both answer 204 to
C<< signal => 'SIGUSR1' >> against a paused container, and then:
=over
lib/API/Docker/API/Containers.pm view on Meta::CPAN
container that is not running. C<on_event> plus a C<< $stop->() >> is the
only way out, and there the callback has to decide for itself that a reading
is not one: that stream carries nothing to croak on.
=head2 On Docker the stream does not end when the container does
Worse, and measured since: the container does B<not> have to be stopped when
the call is made. A C<< stream => 1 >> opened on a container that was
genuinely B<running> on Docker 29.7.2 does not end when that container exits.
It degrades. One 20 s probe against a container that exited after 3 s:
3 real readings, then 13 zero-filled ones, connection still open at 20 s
Podman ends the same stream on the container's exit -- 5.0 s for the same
probe, the last object a whole reading.
So on Docker C<< stream => 1 >> has B<no> terminator tied to the container at
all, and the readings turn to zeros without anything in the stream saying so.
A caller that follows a container's stats until it stops is asking for
something this endpoint does not offer on that engine: give C<on_event> its
own stopping condition -- a reading count, a deadline, or the Go zero time in
C<read> -- and do not wait for the stream to end on its own.
B<C<read_timeout> does not bound this.> It is an idle timeout -- silence since
the last byte -- and this stream is never silent: it keeps producing a
zero-filled reading once a second, indefinitely, so the clock that C<read_timeout>
measures never runs out. C<read_timeout> bounds a daemon that goes quiet; a
Docker stats stream after container exit does the opposite -- it keeps talking,
just not truthfully. C<on_event> still has to notice the Go zero time in
C<read> and call C<< $stop->() >> itself; do not rely on C<read_timeout> to end
this case.
=head2 Why this is documented and not guarded
L</attach> refuses a container that is not running (see
L</"This method refuses a container that is not running">). This method does
B<not>, and the difference is deliberate rather than an inconsistency.
A pre-flight L</inspect> can only answer I<is it running now>. For L</attach>
that is the whole question: the damage needs the container to be stopped
already, so the check leaves a window one round trip wide. For this method it
is the wrong question -- the hazard is the container stopping at B<any> point
in a stream that may run for hours, which the measurement above is exactly a
case of. A guard here would have returned "running, go ahead" and the caller
would have hung anyway. Its blind spot is not a round trip, it is the entire
stream.
The second difference is that nothing here is destroyed. This is a read: after
a hang or a pocketful of zeros, L</inspect> still reports the truth and the
exit status is still there. That is precisely what L</attach> takes away -- a
caller cannot check afterwards, because checking afterwards is the thing that
stops working. A guard is worth an unclosable race when the alternative is
unrecoverable, and is not worth it when the caller can simply ask again.
What can be caught for free already is: Podman reports its refusal in the body
and this method croaks on it, with no extra request and no race.
=head2 changes
for my $change (@{ $containers->changes($id) }) {
say $KIND[ $change->{Kind} ], ' ', $change->{Path};
}
Report which paths in the container's filesystem differ from the image it was
created from -- the endpoint behind C<docker diff>. Returns an ArrayRef of
HashRefs, each with C<Path> and C<Kind>:
[ { Path => '/etc/hostname', Kind => 0 },
{ Path => '/tmp/new', Kind => 1 },
{ Path => '/etc/gone', Kind => 2 } ]
C<Kind> is an integer, not a word, and the engine documents no names for the
three values:
=over
=item * C<0> - B<modified>. The path exists in both and its contents or
metadata changed
=item * C<1> - B<added>. The path exists only in the container
=item * C<2> - B<deleted>. The path existed in the image and is gone
=back
A container with nothing changed comes back as an empty ArrayRef; the engine
answers that case with a JSON C<null> rather than an empty list.
Measured against Podman 5.4.2 (API 1.41): the endpoint is served, but an
unknown container is answered with B<500> and
C<< {"cause":"layer not known","message":"<id> not found: layer not known"} >>
rather than the 404 every other container endpoint gives -- so a caller
distinguishing "no such container" from a real failure cannot do it on the
status code alone on that engine.
=head2 export
use Path::Tiny;
path('container.tar')->spew_raw($containers->export($id));
Export the container's whole filesystem as a tar archive -- the endpoint
behind C<docker export>. Returns the raw archive bytes, never decoded and
never modified.
The archive is buffered whole in memory, so this costs the size of the
container's filesystem in RAM. There is no streaming variant here.
Unlike L<API::Docker::API::Images/get>, the result is a plain filesystem tar:
no C<manifest.json>, no layers, no image metadata. L<API::Docker::API::Images/load>
will not take it back -- importing a flat filesystem is
C<< POST /images/create?fromSrc=- >>, which this distribution does not expose.
=head2 resize
$containers->resize($id, h => 40, w => 120);
Resize the TTY of a container, so a program inside it sees the new terminal
size. Form-identical to L<API::Docker::API::Exec/resize>, which resizes the
TTY of an exec instance instead.
Only meaningful for a container created with C<< Tty => 1 >>; the engine
lib/API/Docker/API/Containers.pm view on Meta::CPAN
This endpoint is also the reason the error check on L</stats> matches
C<cause>, C<message> and C<response> case-sensitively: a I<successful> wait
is a 2xx body with a top-level C<Error> key in it, and a rule matching
C<error> case-insensitively would turn every one of them into a failure.
Options:
=over
=item * C<condition> - What to wait for: C<not-running> (the engine's own
default), C<next-exit> or C<removed>. Sent only when given
=back
=head2 pause
$containers->pause($id);
Pause all processes in a container.
Reports 1/0 like L</start>, but pausing an already-paused container is an
error rather than a 304: measured against Podman 5.4.2 (API 1.41) it answers
C<500> with C<< "..." is already paused: container state improper >>, which
croaks. The Docker Engine API documents no 304 for this endpoint either. So
this method returns 1 or croaks in practice.
=head2 unpause
$containers->unpause($id);
Unpause all processes in a container. Reports 1/0 like L</start>; as with
L</pause>, the no-op is an error and not a 304 -- Podman 5.4.2 answers
unpausing a running container with C<500>.
=head2 rename
$containers->rename($id, 'new-name');
Rename a container.
=head2 update
$containers->update($id, Memory => 314572800);
Update container resource limits and configuration.
The boolean flags (C<Init>, C<OomKillDisable>) may be given as a Perl C<1>/C<0>
or as a JSON boolean; either goes out as a real JSON C<true>/C<false>, which
the engine's body type-check requires.
=head2 get_archive
use Path::Tiny;
my $tar = $containers->get_archive($id, path => '/etc/hostname');
path('hostname.tar')->spew_raw($tar);
# and what the path was, without a second request
my %stat;
my $tar = $containers->get_archive($id, path => '/var/log', stat => \%stat);
say $stat{name};
Read a path out of a container as a tar archive -- the outbound half of
C<docker cp>. Returns the raw archive bytes, never decoded and never modified.
A file comes back as a one-member archive named after its basename; a
directory comes back as the directory and everything under it, with paths
relative to its parent. The whole archive is buffered in memory.
Options:
=over
=item * C<path> - Path inside the container to read. Required
=item * C<stat> - HashRef the C<X-Docker-Container-Path-Stat> header is
decoded into. The engine sends it on this response as well as on the HEAD
one, so asking for it here saves the extra round trip L</stat_archive> would
cost. Emptied when the engine sent no such header. See L</stat_archive> for
the keys
=back
=head2 put_archive
use Path::Tiny;
$containers->put_archive($id, path('payload.tar')->slurp_raw,
path => '/opt/app');
Write a tar archive into a path inside the container -- the inbound half of
C<docker cp>. The archive is the request body; pass it as raw bytes or as a
scalar reference to them, the way L<API::Docker::API::Images/load> takes its
archive. Returns nothing: the engine answers a success with an empty body.
C<path> must name a B<directory that already exists> in the container; the
archive's members are unpacked into it. Writing a single file means putting
that file in a one-member archive and naming its parent directory as C<path> --
there is no "write these bytes to this filename" form of this endpoint.
The archive is sent as one buffered request body, so this costs its full size
in RAM.
Options:
=over
=item * C<path> - Directory inside the container to unpack into. Required
=item * C<noOverwriteDirNonDir> - Refuse the request rather than replace an
existing directory with a non-directory, or the other way round. Without it
the engine replaces either with the other
=item * C<copyUIDGID> - Keep the UID and GID recorded in the archive instead
of mapping the members to the container user
=back
=head2 stat_archive
my $stat = $containers->stat_archive($id, path => '/etc/hostname');
say $stat->{name}; # hostname
say $stat->{size}; # 13
printf "%04o\n", $stat->{mode} & 0777; # 0644
Stat a path inside a container without transferring it -- C<HEAD> on the same
endpoint L</get_archive> uses. Returns a HashRef, or C<undef> when the engine
answered without the header. A path that does not exist is a croak from the
transport's status handling, not an C<undef>.
The response has no body at all: the answer is the
C<X-Docker-Container-Path-Stat> header, base64-encoded JSON, which this method
decodes. Its keys are the engine's, passed through as they arrive:
=over
=item * C<name> - The path's basename. For a symlink the two engines
disagree: Docker reports the requested path's own basename, Podman the
resolved target's
=item * C<size> - Size in bytes
=item * C<mode> - Go's C<os.FileMode> bits, B<not> a POSIX mode word, on both
engines. The permission bits are the low nine (C<< $stat->{mode} & 0777 >>);
the type bits above them are Go's own numbering, so a directory's C<mode> is
C<os.ModeDir> (C<< 1<<31 >>) plus the permission bits -- C<2147484141> for a
C<0755> directory -- rather than POSIX's C<S_IFDIR>, which for the same
directory would give C<16877>
=item * C<mtime> - Modification time, RFC 3339
=item * C<linkTarget> - The symlink target. Docker sends the literal,
unresolved link content, and leaves this empty for anything that is not a
symlink exactly as the Engine API reference documents; Podman sends the
fully I<resolved> path instead, and was measured populating it even for a
plain regular file, where Docker leaves it empty
=item * C<isDir> - Boolean, true when the path is a directory. B<Podman
only> -- Docker was measured never sending this key, not even for a
directory, so it is not part of the Docker Engine API's own answer
=back
This shape is confirmed against Podman, not assumed: measured against the
rootless socket, C<stat_archive> on Podman returns exactly these six keys.
For a symlink such as F<hnlink> pointing at F</etc/hostname>, Docker reports
C<name> as C<hnlink> (the link's own basename) and C<linkTarget> as
C</etc/hostname> (the raw, unresolved content); Podman reports C<name> as
C<hostname> (the resolved target's basename) and the fully resolved path in
C<linkTarget>. The route itself was measured the same way on Podman: an
unknown container answers 404, and that 404 announces a C<Content-Length>
while sending no body -- which is why L<API::Docker::Role::HTTP/head> never
reads one.
Options:
=over
=item * C<path> - Path inside the container to stat. Required
=back
=head2 prune
( run in 1.077 second using v1.01-cache-2.11-cpan-a49fcb8fa48 )