API-Docker
view release on metacpan or search on metacpan
lib/API/Docker.pm view on Meta::CPAN
has networks => (
is => 'lazy',
builder => sub { API::Docker::API::Networks->new(client => $_[0]) },
);
has volumes => (
is => 'lazy',
builder => sub { API::Docker::API::Volumes->new(client => $_[0]) },
);
has exec => (
is => 'lazy',
builder => sub { API::Docker::API::Exec->new(client => $_[0]) },
);
has distribution => (
is => 'lazy',
builder => sub { API::Docker::API::Distribution->new(client => $_[0]) },
);
has secrets => (
is => 'lazy',
builder => sub { API::Docker::API::Secrets->new(client => $_[0]) },
);
has configs => (
is => 'lazy',
builder => sub { API::Docker::API::Configs->new(client => $_[0]) },
);
has plugins => (
is => 'lazy',
builder => sub { API::Docker::API::Plugins->new(client => $_[0]) },
);
sub negotiate_version {
my ($self, %opts) = @_;
return if $self->_version_negotiated;
return if defined $self->api_version;
$log->debug("Auto-negotiating API version");
my $version_info = $self->_request('GET', '/version',
exists $opts{read_timeout} ? ( read_timeout => $opts{read_timeout} ) : (),
exists $opts{connect_timeout} ? ( connect_timeout => $opts{connect_timeout} ) : (),
);
# The ApiVersion is put straight into every later request path (/v1.44/...),
# so it has to be a JSON object carrying one of the form N.N -- nothing else
# can be trusted there. Three ways a body fails that, each measured against a
# fake daemon: a non-object body reached strict refs ('garbage' died with
# "Can't use string as a HASH ref", [1] with "Not a HASH reference"); an
# object with no ApiVersion set _version_negotiated and then sent every
# request unversioned; and an ApiVersion copied verbatim let 'v1.44/../x'
# become "GET /vv1.44/../x/info". One croak, naming the endpoint and the
# shape, covers all of them.
my $got;
if (!defined $version_info) {
$got = 'nothing';
}
elsif (ref $version_info ne 'HASH') {
$got = ref $version_info ? 'a ' . ref($version_info) . ' reference'
: "the non-object body '" . $version_info . "'";
}
elsif (!defined $version_info->{ApiVersion}) {
$got = 'an object with no ApiVersion field';
}
else {
my $v = $version_info->{ApiVersion};
$got = 'an ApiVersion of '
. (ref $v ? 'a ' . ref($v) . ' reference' : "'" . $v . "'");
}
croak __PACKAGE__ . '->negotiate_version: GET /version must answer with a '
. 'JSON object carrying an ApiVersion of the form N.N (e.g. "1.44"); got '
. $got
unless ref $version_info eq 'HASH'
&& defined $version_info->{ApiVersion}
&& !ref $version_info->{ApiVersion}
&& $version_info->{ApiVersion} =~ /^\d+\.\d+$/;
$self->_set_api_version($version_info->{ApiVersion});
$log->debugf("Negotiated API version: %s", $version_info->{ApiVersion});
$self->_version_negotiated(1);
}
around _request => sub {
my ($orig, $self, $method, $path, %opts) = @_;
# Auto-negotiate before any versioned request, but not for /version itself.
# The triggering request's own bounds are handed to it: the negotiation is a
# pre-flight the caller never wrote, and a caller who asked for a bound and
# then hung in GET /version has been told something untrue (karr k72).
if ($path ne '/version' && !defined $self->api_version && !$self->_version_negotiated) {
$self->negotiate_version(
exists $opts{read_timeout} ? ( read_timeout => $opts{read_timeout} ) : (),
exists $opts{connect_timeout} ? ( connect_timeout => $opts{connect_timeout} ) : (),
);
}
return $self->$orig($method, $path, %opts);
};
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
lib/API/Docker.pm view on Meta::CPAN
API::Docker is a Perl client for the Docker Engine API. It provides a clean
object-oriented interface to manage Docker containers, images, networks, and
volumes.
Key features:
=over
=item * Pure Perl implementation with minimal dependencies
=item * Unix socket and TCP transport, the latter in the clear or over TLS
with client certificates (L</tls>, L</cert_path>)
=item * Automatic API version negotiation
=item * A typed object model generated from Docker's own swagger
(L<API::Docker::Type>) -- complete across all seven resources: C<list> and
C<inspect> return these generated classes, not hashrefs; see
L</Architecture> below
=item * Comprehensive logging via L<Log::Any>
=back
=head2 Architecture
The distribution is organized into several layers:
=over
=item * B<Main Client> - L<API::Docker> - Entry point with API version negotiation
=item * B<API Modules> - Resource-specific API methods:
=over
=item * L<API::Docker::API::System> - System info, version, ping
=item * L<API::Docker::API::Containers> - Container management
=item * L<API::Docker::API::Images> - Image management
=item * L<API::Docker::API::Networks> - Network management
=item * L<API::Docker::API::Volumes> - Volume management
=item * L<API::Docker::API::Exec> - Exec into containers
=item * L<API::Docker::API::Distribution> - Registry manifest lookups
=item * L<API::Docker::API::Secrets> - Swarm secrets
=item * L<API::Docker::API::Configs> - Swarm configs
=item * L<API::Docker::API::Plugins> - Managed plugins
=back
=item * B<Entity Roles> - the convenience methods of a resource, composed at
load time onto the generated L<API::Docker::Type> classes its endpoints
answer with. There is no separate wrapper object: C<< $docker->images->list >>
hands back real L<API::Docker::Type::ImageSummary> objects that also have
C<< ->remove >>. See L<API::Docker::Role::Entity>.
=over
=item * L<API::Docker::Role::Entity::Container> - composed into
L<API::Docker::Type::ContainerSummary> and
L<API::Docker::Type::ContainerInspectResponse>
=item * L<API::Docker::Role::Entity::Image> - composed into
L<API::Docker::Type::ImageSummary> and L<API::Docker::Type::ImageInspect>
=item * L<API::Docker::Role::Entity::Network> - composed into
L<API::Docker::Type::Network>, which serves both C<list> and C<inspect>
=item * L<API::Docker::Role::Entity::Volume> - composed into
L<API::Docker::Type::Volume>, which serves C<list>, C<inspect> and C<create>
=item * L<API::Docker::Role::Entity::Secret> - composed into
L<API::Docker::Type::Secret>
=item * L<API::Docker::Role::Entity::Config> - composed into
L<API::Docker::Type::Config>
=item * L<API::Docker::Role::Entity::Plugin> - composed into
L<API::Docker::Type::Plugin>
=back
=item * B<Roles> - Behaviour shared across more than one class:
=over
=item * L<API::Docker::Role::HTTP> - HTTP transport layer
=item * L<API::Docker::Role::RegistryAuth> - X-Registry-Auth / AuthConfig
encoding, shared by Images, Plugins, Distribution and System
=item * L<API::Docker::Role::Filters> - the C<filters> query parameter,
normalised into the one shape the engine reads
=item * L<API::Docker::Role::Using> - C<using>, the resource class clone that
bounds a run of calls
=item * L<API::Docker::Role::Type> - the instance behaviour of every
generated L<API::Docker::Type> class: serialisation both ways and
C<unknown_fields>
=item * L<API::Docker::Role::Entity> - the client reference an entity
delegates through, composed by a resource-specific entity role such as
L<API::Docker::Role::Entity::Container>
=back
=back
=head2 Swarm orchestration is out of scope
C</swarm>, C</nodes>, C</services> and C</tasks> are deliberately absent, and
lib/API/Docker.pm view on Meta::CPAN
=head2 containers
Returns L<API::Docker::API::Containers> instance for container operations like
C<list>, C<create>, C<start>, C<stop>, and C<remove>.
=head2 images
Returns L<API::Docker::API::Images> instance for image operations like
C<list>, C<pull>, C<push>, and C<remove>.
=head2 networks
Returns L<API::Docker::API::Networks> instance for network operations like
C<list>, C<create>, C<connect>, and C<disconnect>.
=head2 volumes
Returns L<API::Docker::API::Volumes> instance for volume operations like
C<list>, C<create>, and C<remove>.
=head2 exec
Returns L<API::Docker::API::Exec> instance for executing commands in containers.
=head2 distribution
Returns L<API::Docker::API::Distribution> instance for registry manifest
lookups: C<inspect> and C<exists>.
=head2 secrets
Returns L<API::Docker::API::Secrets> instance for secret operations: C<list>,
C<create>, C<inspect>, C<update> and C<remove>.
=head2 configs
Returns L<API::Docker::API::Configs> instance for config operations: C<list>,
C<create>, C<inspect>, C<update> and C<remove>.
=head2 plugins
Returns L<API::Docker::API::Plugins> instance for managed-plugin operations:
C<list>, C<privileges>, C<install>, C<inspect>, C<remove>, C<enable>,
C<disable>, C<upgrade>, C<push> and C<configure>.
=head2 negotiate_version
$docker->negotiate_version;
$docker->negotiate_version(read_timeout => 5, connect_timeout => 2);
Automatically negotiate the highest API version supported by the Docker daemon.
This is called automatically before the first API request if L</api_version>
is not set.
After negotiation, L</api_version> will contain the negotiated version
(e.g., C<1.41>).
C<GET /version> must answer with a JSON object carrying an C<ApiVersion> of
the form C<N.N> -- the value is placed directly into the path of every later
request (C</v1.44/...>). A body that is not such an object croaks, naming the
endpoint and the shape expected: a non-object body, an object with no
C<ApiVersion>, or an C<ApiVersion> that is not two dot-separated numbers. This
replaces three earlier failures on the same path -- a non-object body dying in
C<strict refs>, an object with no C<ApiVersion> silently leaving the client
sending every request unversioned, and a malformed C<ApiVersion> being copied
verbatim into the request path.
Options:
=over
=item * C<read_timeout> - Seconds of silence after which the request gives up
and croaks with an L<API::Docker::Error::Timeout>. Off by default; see
L<API::Docker::Role::HTTP/"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'>. Off by default; see
L<API::Docker::Role::HTTP/"Bounding the connection itself">
=back
Called on its own, with no options, the negotiation is bounded by the
L<API::Docker::Role::HTTP/read_timeout> and
L<API::Docker::Role::HTTP/connect_timeout> attributes of the client, like any
other request. Reached the way it normally is -- automatically, from the first
request -- it inherits that request's own bounds instead; see
L</"What a timeout covers">.
=head1 TIMEOUTS
=head2 What a timeout covers
Two bounds, covering different halves of a request.
L<API::Docker::Role::HTTP/connect_timeout> bounds opening the connection;
L<API::Docker::Role::HTTP/read_timeout> bounds reading the answer. Both are
attributes of the client, and they are set in two places -- which are two
levels, not two spellings of one thing:
# the rule, for this client
my $docker = API::Docker->new(connect_timeout => 2, read_timeout => 30);
# the exception, for this run of calls
$docker->containers->using(read_timeout => 5)->list;
$docker->system->using(read_timeout => 0)->events;
L<API::Docker::Role::Using/using> returns a clone of the resource class
carrying the bounds, and every request made through that clone is given them.
There is deliberately no third way: the individual methods take no timeout
options, so their arguments are the request and nothing else.
Both are off by default, which is the behaviour this distribution has always
had. C<0> means the same as unset -- no bound -- and is how a client-wide
default is turned off for a run of calls: what C<using> carries is read with
C<exists> rather than for truth, so a C<0> reaches the transport instead of
vanishing into "no opinion".
Three things they do not do:
=over
lib/API/Docker.pm view on Meta::CPAN
bounds the hang, which is what it is for. C<connect_timeout> over TLS bounds
the TCP connect and not the handshake that follows it.
=back
An expiry croaks with an L<API::Docker::Error::Timeout> carrying what did
arrive; it never returns a truncated response.
L<API::Docker::Role::HTTP/"Bounding a request that never ends"> and
L<API::Docker::Role::HTTP/"Bounding the connection itself"> have the
per-transport measurements behind all of this.
=head2 Where a bound applies
Every public method of every resource class that reaches the daemon -- all of
them, with no exception for the ones whose arguments are the request body --
makes its request with the bounds in force. That is what the clone buys: the
method builds the request and the resource class it was called on says how
long to wait for it, so there is no list of methods that forward a bound and
no list of methods that cannot.
The requests a method makes on the caller's behalf without being asked are
bounded too, and for the same reason -- they run on the same resource class:
=over
=item * L<API::Docker::API::Containers/attach> asks whether the container is
running before attaching. That check carries the bounds the attach carries.
=item * L<API::Docker::API::Plugins/install> and
L<API::Docker::API::Plugins/upgrade> with C<< accept_privileges => 1 >> fetch
the plugin's privileges first. That fetch carries them too.
=item * L</negotiate_version> runs before the first request of a client with
no L</api_version>, and inherits the bounds of the request that triggered it:
C<< $docker->containers->using(read_timeout => 5)->list >> on a fresh client
bounds the C<GET /version> as well as the list. It is the one place the two
options are still written out per call, because it can be called directly and
is not reached through a resource class:
$docker->negotiate_version(read_timeout => 5);
=back
The entity classes have no C<using> of their own; a bound for
C<< $container->logs >> goes on the resource class instead, see
L<API::Docker::Role::Using/"What has no clone of its own">.
=head1 CONTAINER ENGINES
This client speaks the Docker Engine HTTP API over a socket. It never shells
out to the C<docker> binary, so any engine serving that API works, whether or
not Docker itself is installed.
=head2 Installing Docker
Where the engine is Docker itself, prefer the official packages from
L<https://docs.docker.com/engine/install/> over a distribution package such as
Debian/Ubuntu's C<docker.io>, which is typically a good deal older. The reason
that matters here: L</negotiate_version> only negotiates within whatever API
version the daemon itself reports, so an older daemon still works, but
endpoints and query parameters that need a newer API version are then simply
not there. This is a recommendation about which Docker package to install, not
Docker instead of Podman -- Podman remains fully supported, see L</Podman>
below.
=head2 Podman
Podman ships a Docker-compatible API service. Enable its rootless socket and
point L</host> at it:
systemctl --user enable --now podman.socket
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"
The socket announces API version 1.44, which L</negotiate_version> picks up
like any other daemon. Multi-stage builds are passed through unchanged,
C<target> included, down to skipping the stages the target does not depend on.
=head2 Engines and versions behind the measurements in this POD
Where this distribution's documentation says what an engine does rather than
what the Engine API reference says it should do, that statement was measured
against a real socket, not assumed. Three engines stand behind the
measurements found throughout this POD:
=over
=item * Podman 5.4.2, API 1.41
=item * Podman 5.8.4, API 1.44
=item * Docker 29.7.2, API 1.55
=back
Podman statements have been checked against both 5.4.2 and 5.8.4. Where an
individual statement names no version, it holds for both. A version named at
one particular measurement -- C<"Measured against Podman 5.4.2 (API 1.41):
...">, for instance -- names the engine that measurement was taken I<on>, not
the only engine it is claimed to hold for; read it as provenance, not as a
scope limit. Where a measurement genuinely is version-specific -- superseded
by a later one, or not re-checked on the other version -- the text says so.
=head2 Socket discovery
L</host> resolves in two steps and no more: C<$ENV{DOCKER_HOST}>, then
C<unix:///var/run/docker.sock>. It deliberately does B<not> read Docker
contexts. C<currentContext> in F<~/.docker/config.json> and the matching
F<~/.docker/contexts/meta/*/meta.json> are ignored, so if you switch daemons
with C<docker context use>, that choice is not picked up here. Set
C<DOCKER_HOST> explicitly instead.
Other clients sit at different points on that scale. The C<docker> CLI and
docker-java resolve contexts, with C<DOCKER_HOST> outranking them when set.
docker-py's C<from_env()> reads C<DOCKER_HOST> and otherwise falls back to the
default socket, leaving contexts to a separate API. Testcontainers layers its
own F<~/.testcontainers.properties> and a rootless probe list
(C<$XDG_RUNTIME_DIR/docker.sock>, F<~/.docker/run/docker.sock>,
F<~/.docker/desktop/docker.sock>, C</run/user/$UID/docker.sock>) on top.
What none of them do is guess Podman's socket path: that probe list is for
rootless Docker, not for Podman. Every one of those projects documents
( run in 1.479 second using v1.01-cache-2.11-cpan-f9ab5d97e31 )