API-Docker
view release on metacpan or search on metacpan
lib/API/Docker/API/Containers.pm view on Meta::CPAN
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
lib/API/Docker/API/Containers.pm view on Meta::CPAN
# 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
# guess about prose rather than a reading of what the engine said.
sub _podman_error_object {
my ($self, $value) = @_;
return unless ref $value eq 'HASH';
return unless exists $value->{cause}
&& exists $value->{message}
&& exists $value->{response};
my $response = $value->{response};
return if ref $response;
return unless defined $response && $response =~ /\A[0-9]+\z/;
return unless $response >= 400;
return $value;
}
# API::Docker::Error::HTTP rather than ::Stream: the one-shot call is not a
# stream at all, so "Docker API stream error" would be the wrong sentence for
# it and ->events would be a fabricated list. What the caller wants instead is
# exactly what this class carries -- ->status for the code Podman named, and
# ->data for the object, whose `cause` key that attribute's own POD already
# points at. Two of its attributes are left at their defaults on this path, on
# purpose: ->reason, because the status line's reason phrase was "OK" and
# putting that on a 500 would mislead, and ->body, because the bytes were
# decoded by the transport before this check ever saw them.
sub _assert_no_podman_error {
my ($self, $endpoint, $value) = @_;
my $error = $self->_podman_error_object($value) or return $value;
my $reason = $error->{message};
$reason = $error->{cause} unless defined $reason && length $reason;
$reason = 'no message given' unless defined $reason && length $reason;
# Carp appends no location to a message that already ends in a newline.
$reason =~ s/\s+\z//;
# The object goes into a variable first: `croak CLASS->new(...)` is indirect
# object syntax and parses as CLASS->croak(new(...)). Carp hands a reference
# straight back rather than decorating it, so the location is captured by
# hand, naming the frame a croak of a plain string would have named.
my $err = API::Docker::Error::HTTP->new(
message => 'Docker API error (' . $error->{response} . '): ' . $reason
. ' -- reported inside a 200 response to ' . $endpoint,
location => shortmess(''),
status => $error->{response},
data => $error,
);
croak $err;
}
sub stats {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my $stream = $opts{stream} ? 1 : 0;
my %params = ( stream => $stream );
# one-shot asks the engine not to wait for a second sampling cycle, which
# only means anything to a single reading. It is sent for the one-shot call
# alone, the way it always was, and never beside stream => 1.
$params{'one-shot'} = 1 unless $stream;
my $endpoint = 'GET /containers/' . $id . '/stats';
# The guard has to sit on both sides of the callback split, because the same
# body arrives either way: buffered it is the return value, streamed it goes
# to the callback and is never returned at all. Wrapping puts the check in
# front of the caller's callback, so no caller is handed the error object as
# though it were a reading. Only a CodeRef is wrapped -- anything else is
# passed through untouched, so the transport still raises its own "on_event
# option must be a CodeRef" instead of this method dying on a closure it
# built around a non-callback.
my $on_event = $opts{on_event};
if (exists $opts{on_event} && ref $on_event eq 'CODE') {
my $cb = $on_event;
$on_event = sub {
my ($reading, $stop) = @_;
$self->_assert_no_podman_error($endpoint, $reading);
return $cb->($reading, $stop);
};
}
my $result = $self->client->get("/containers/$id/stats",
params => \%params,
%{ $self->_request_options },
exists $opts{on_event} ? ( on_event => $on_event )
: $stream ? ( ndjson => 1 )
: (),
);
# A HashRef for the one-shot call and an ArrayRef of readings for
# stream => 1 without a callback -- both are the buffered body and both can
# be that error object. With a callback the return value is the summary
# HashRef, which carries none of the three keys and passes untouched.
$self->_assert_no_podman_error($endpoint, $_)
lib/API/Docker/API/Containers.pm view on Meta::CPAN
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{condition} = $opts{condition} if defined $opts{condition};
return $self->client->post("/containers/$id/wait", undef,
params => \%params,
%{ $self->_request_options },
);
}
sub pause {
my ($self, $id) = @_;
croak "Container ID required" unless $id;
return $self->_state_change("/containers/$id/pause",
%{ $self->_request_options },
);
}
sub unpause {
my ($self, $id) = @_;
croak "Container ID required" unless $id;
return $self->_state_change("/containers/$id/unpause",
%{ $self->_request_options },
);
}
sub rename {
my ($self, $id, $name) = @_;
croak "Container ID required" unless $id;
croak "New name required" unless $name;
return $self->client->post("/containers/$id/rename", undef,
params => { name => $name },
%{ $self->_request_options },
);
}
# The update body is Resources + RestartPolicy; the booleans are the two
# Resources flags. Normalised on the way out, as for create.
my @UPDATE_BOOLS = qw( Init OomKillDisable );
sub update {
my ($self, $id, %config) = @_;
croak "Container ID required" unless $id;
$self->_json_bools(\%config, @UPDATE_BOOLS);
return $self->client->post("/containers/$id/update", \%config);
}
# The engine reports what a path is in a response header rather than a body,
# so both GET and HEAD carry it and only HEAD has nothing else to say. The
# header is base64-encoded JSON; handing the caller the base64 would make
# every one of them write this.
sub _decode_path_stat {
my ($self, $response) = @_;
my $header = $response->{headers}{'x-docker-container-path-stat'};
return undef unless defined $header && length $header;
# Docker encodes this one with Go's base64.StdEncoding -- unlike
# X-Registry-Auth, which is URLEncoding. Decoded tolerantly rather than
# strictly: translating the two URL-safe characters first costs nothing and
# means an engine that reached for the other alphabet is still read.
$header =~ tr{-_}{+/};
my $stat = eval { decode_json(decode_base64($header)) };
croak "Cannot decode X-Docker-Container-Path-Stat header: $@"
unless ref $stat eq 'HASH';
return $stat;
}
sub get_archive {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
croak "Path required" unless defined $opts{path} && length $opts{path};
croak "The stat option must be a HashRef"
if exists $opts{stat} && ref $opts{stat} ne 'HASH';
my %response;
my $tar = $self->client->get("/containers/$id/archive",
params => { path => $opts{path} },
raw => 1,
response => \%response,
%{ $self->_request_options },
);
if (my $out = $opts{stat}) {
%$out = %{ $self->_decode_path_stat(\%response) // {} };
}
return $tar;
}
sub put_archive {
my ($self, $id, $tar, %opts) = @_;
croak "Container ID required" unless $id;
croak "Path required" unless defined $opts{path} && length $opts{path};
croak "Tar archive required (raw bytes or a scalar ref)" unless defined $tar;
my %params = ( path => $opts{path} );
$params{noOverwriteDirNonDir} = $opts{noOverwriteDirNonDir} ? 1 : 0
if defined $opts{noOverwriteDirNonDir};
$params{copyUIDGID} = $opts{copyUIDGID} ? 1 : 0
if defined $opts{copyUIDGID};
my $raw = ref $tar eq 'SCALAR' ? $$tar : $tar;
return $self->client->put("/containers/$id/archive", undef,
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});
( run in 3.769 seconds using v1.01-cache-2.11-cpan-54e63673c56 )