API-Docker
view release on metacpan or search on metacpan
lib/API/Docker.pm view on Meta::CPAN
package API::Docker;
# ABSTRACT: Perl client for the Docker Engine API
our $VERSION = '0.004';
use Moo;
use Carp qw( croak );
use Log::Any qw( $log );
use API::Docker::API::System;
use API::Docker::API::Containers;
use API::Docker::API::Images;
use API::Docker::API::Networks;
use API::Docker::API::Volumes;
use API::Docker::API::Exec;
use API::Docker::API::Distribution;
use API::Docker::API::Secrets;
use API::Docker::API::Configs;
use API::Docker::API::Plugins;
use namespace::clean;
has host => (
is => 'ro',
default => sub { $ENV{DOCKER_HOST} // 'unix:///var/run/docker.sock' },
);
has api_version => (
is => 'rwp',
default => undef,
);
has tls => (
is => 'lazy',
);
sub _build_tls {
my ($self) = @_;
# The docker CLI's own rule, read off cli/flags/options.go:
# dockerTLSVerify = os.Getenv(client.EnvTLSVerify) != ""
# Every non-empty value turns TLS on, DOCKER_TLS_VERIFY=0 included. Perl
# truthiness would read that '0' as off and disagree with the CLI on exactly
# the value a user is most likely to type for "off", so the test is
# defined-and-not-empty rather than a boolean one.
return 0 unless defined $ENV{DOCKER_TLS_VERIFY}
&& $ENV{DOCKER_TLS_VERIFY} ne '';
# And the CLI ignores TLS on a socket host without saying so
# (cli/context/docker/load.go, "there's no need to configure TLS for a
# socket connection"). Ignoring it here is not politeness: BUILD croaks on
# tls => 1 with a non-tcp:// host, so a host-blind default would make a bare
# API::Docker->new die on every unix:// machine that exports the variable.
return $self->host =~ m{^tcp://} ? 1 : 0;
}
has cert_path => (
is => 'ro',
default => sub { $ENV{DOCKER_CERT_PATH} },
);
has tls_insecure => (
is => 'ro',
default => 0,
lib/API/Docker.pm view on Meta::CPAN
has containers => (
is => 'lazy',
builder => sub { API::Docker::API::Containers->new(client => $_[0]) },
);
has images => (
is => 'lazy',
builder => sub { API::Docker::API::Images->new(client => $_[0]) },
);
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
API::Docker - Perl client for the Docker Engine API
=head1 VERSION
version 0.004
=head1 SYNOPSIS
use API::Docker;
# Connect to local Docker daemon via Unix socket
my $docker = API::Docker->new;
# Or connect to remote Docker daemon
my $docker = API::Docker->new(
host => 'tcp://192.168.1.100:2375',
);
# System information
my $info = $docker->system->info;
my $version = $docker->system->version;
# Container management -- list/inspect return generated
# API::Docker::Type::* objects with snake_case accessors, not hashrefs
my $containers = $docker->containers->list(all => 1);
for my $container (@$containers) {
say $container->id;
say $container->status;
}
my $result = $docker->containers->create(
Image => 'nginx:latest',
name => 'my-nginx',
);
$docker->containers->start($result->{Id});
my $inspected = $docker->containers->inspect($result->{Id});
say $inspected->state->running ? 'running' : 'not running';
# Image operations
$docker->images->pull(fromImage => 'nginx', tag => 'latest');
my $images = $docker->images->list;
# Network and volume management
my $networks = $docker->networks->list;
my $volumes = $docker->volumes->list;
=head1 DESCRIPTION
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>
lib/API/Docker.pm view on Meta::CPAN
started. A stream that keeps producing runs as long as it likes; one that
stops producing is cut off. So it bounds a daemon that goes quiet -- it does
not bound a long transfer, and it does not bound a stream that keeps sending
without saying anything, which is what C<< containers->stats >> degrades into
on Docker after the container exits.
=item * B<Neither of them bounds writing the request.> Sending the bytes out
is unbounded on every transport. In practice that matters for one thing: a
large C</build> context or C<< images->load >> archive being written to a
daemon that has stopped reading.
=item * B<Under TLS, C<read_timeout> is not quite an idle timer on the
plaintext.> It is C<SO_RCVTIMEO> on the socket, which bounds each blocking
receive on the underlying connection, and one plaintext read can consume
several of those while a TLS record arrives in pieces -- so a record dribbling
in slowly enough resets the clock without a byte reaching the caller. It still
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
( run in 1.066 second using v1.01-cache-2.11-cpan-4ef0a570458 )