API-Docker
view release on metacpan or search on metacpan
.claude/CLAUDE.md view on Meta::CPAN
âââ Container.pm # Container entity
âââ Image.pm # Image entity
âââ Network.pm # Network entity
âââ Volume.pm # Volume entity
```
## Tech
- **Moo** for OOP
- **IO::Socket::UNIX** for Unix socket transport (no LWP dependency)
- **JSON::MaybeXS** for JSON handling
- **Log::Any** for logging
- **Dist::Zilla** with `[@Author::GETTY]`
tests (create/remove containers, etc.).
## API conventions
- **Resource accessors live under the client:** `$docker->images`,
`$docker->containers`, etc. Each returns a `*::API::*` instance.
- **List/inspect endpoints return entity objects** (e.g.
`$docker->images->list` returns `[API::Docker::Image, ...]`); raw
endpoints (e.g. `tag`, `push`) return the raw daemon response.
- **`$docker->_request($method, $path, %opts)`** is the single transport
entry point. Opts: `body` (auto-JSON-encoded), `raw_body` +
`content_type` (e.g. tarballs), `params` (query string),
`headers` (extra HTTP headers â used by push for `X-Registry-Auth`).
- **`/build`, `/images/create`, `/images/.../push`** are streaming
endpoints. `_request` parses newline-delimited JSON and returns an
arrayref of events; callers iterate and look for `errorDetail`,
`progress`, `aux`, etc.
- **`X-Registry-Auth` is required on every push** by the Docker Engine â
even anonymous attempts. `images->push` always sends the header; pass
`auth => { username, password, serveraddress, identitytoken }` to
authenticate, omit it for the empty-`{}` form.
## Testing notes
- New tests should use the `Test::API::Docker::Mock` helper. Pass a
0.002 2026-05-17 05:36:20Z
- HTTP role: `_request` now accepts a `headers => {}` option to set
extra HTTP request headers. Headers are sanitised against CR/LF
injection. Used by `images->push` to send `X-Registry-Auth`, and
available to any caller that needs custom headers.
- `images->push` now always sends an `X-Registry-Auth` header â the
Docker Engine refuses pushes without it (`HTTP 400: missing
X-Registry-Auth: invalid X-Registry-Auth header: EOF`). A new `auth`
option accepts a hashref of credentials (`username`, `password`,
`serveraddress`, or `identitytoken`) which is JSON-encoded and
base64url-wrapped per the Docker Engine spec. Without `auth` the
header carries an empty JSON object so unauthenticated/public
pushes succeed where they previously failed at the HTTP layer.
0.001 2026-04-29 00:40:43Z
- Initial release as API::Docker
- Docker Engine API client with Unix socket and TCP support
- Auto-negotiate API version from daemon
- Container, Image, Network, Volume, System, and Exec APIs
- Pure Perl implementation with minimal dependencies (no LWP)
- HTTP/1.1 transport with chunked transfer encoding support
"recommends" : {
"Dist::Zilla::PluginBundle::Git::VersionManager" : "0.007"
},
"requires" : {
"Test::Pod" : "1.41"
}
},
"runtime" : {
"requires" : {
"IO::Socket::UNIX" : "0",
"JSON::MaybeXS" : "0",
"Log::Any" : "0",
"MIME::Base64" : "0",
"Moo" : "0",
"URI" : "0",
"namespace::clean" : "0"
}
},
"test" : {
"requires" : {
"Path::Tiny" : "0",
"class" : "Dist::Zilla::Plugin::UploadToCPAN",
"name" : "@Author::GETTY/@Filter/UploadToCPAN",
"version" : "6.037"
},
{
"class" : "Dist::Zilla::Plugin::MetaConfig",
"name" : "@Author::GETTY/MetaConfig",
"version" : "6.037"
},
{
"class" : "Dist::Zilla::Plugin::MetaJSON",
"name" : "@Author::GETTY/MetaJSON",
"version" : "6.037"
},
{
"class" : "Dist::Zilla::Plugin::PodSyntaxTests",
"name" : "@Author::GETTY/PodSyntaxTests",
"version" : "6.037"
},
{
"class" : "Dist::Zilla::Plugin::Test::ChangesHasContent",
"name" : "@Author::GETTY/Test::ChangesHasContent",
"zilla" : {
"class" : "Dist::Zilla::Dist::Builder",
"config" : {
"is_trial" : 0
},
"version" : "6.037"
}
},
"x_authority" : "cpan:GETTY",
"x_generated_by_perl" : "v5.36.0",
"x_serialization_backend" : "Cpanel::JSON::XS version 4.40",
"x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later"
}
ExtUtils::MakeMaker: '0'
dynamic_config: 0
generated_by: 'Dist::Zilla version 6.037, CPAN::Meta::Converter version 2.150010'
license: perl
meta-spec:
url: http://module-build.sourceforge.net/META-spec-v1.4.html
version: '1.4'
name: API-Docker
requires:
IO::Socket::UNIX: '0'
JSON::MaybeXS: '0'
Log::Any: '0'
MIME::Base64: '0'
Moo: '0'
URI: '0'
namespace::clean: '0'
resources:
bugtracker: https://github.com/Getty/p5-api-docker/issues
homepage: https://github.com/Getty/p5-api-docker
repository: https://github.com/Getty/p5-api-docker.git
version: '0.002'
version: '6.037'
-
class: Dist::Zilla::Plugin::UploadToCPAN
name: '@Author::GETTY/@Filter/UploadToCPAN'
version: '6.037'
-
class: Dist::Zilla::Plugin::MetaConfig
name: '@Author::GETTY/MetaConfig'
version: '6.037'
-
class: Dist::Zilla::Plugin::MetaJSON
name: '@Author::GETTY/MetaJSON'
version: '6.037'
-
class: Dist::Zilla::Plugin::PodSyntaxTests
name: '@Author::GETTY/PodSyntaxTests'
version: '6.037'
-
class: Dist::Zilla::Plugin::Test::ChangesHasContent
name: '@Author::GETTY/Test::ChangesHasContent'
version: '0.011'
-
Makefile.PL view on Meta::CPAN
"ABSTRACT" => "Perl client for the Docker Engine API",
"AUTHOR" => "Torsten Raudssus <getty\@cpan.org>",
"CONFIGURE_REQUIRES" => {
"ExtUtils::MakeMaker" => 0
},
"DISTNAME" => "API-Docker",
"LICENSE" => "perl",
"NAME" => "API::Docker",
"PREREQ_PM" => {
"IO::Socket::UNIX" => 0,
"JSON::MaybeXS" => 0,
"Log::Any" => 0,
"MIME::Base64" => 0,
"Moo" => 0,
"URI" => 0,
"namespace::clean" => 0
},
"TEST_REQUIRES" => {
"Path::Tiny" => 0,
"Test::More" => 0
},
"VERSION" => "0.002",
"test" => {
"TESTS" => "t/*.t"
}
);
my %FallbackPrereqs = (
"IO::Socket::UNIX" => 0,
"JSON::MaybeXS" => 0,
"Log::Any" => 0,
"MIME::Base64" => 0,
"Moo" => 0,
"Path::Tiny" => 0,
"Test::More" => 0,
"URI" => 0,
"namespace::clean" => 0
);
requires 'Moo';
requires 'JSON::MaybeXS';
requires 'MIME::Base64';
requires 'IO::Socket::UNIX';
requires 'URI';
requires 'namespace::clean';
requires 'Log::Any';
on test => sub {
requires 'Test::More';
requires 'Path::Tiny';
};
lib/API/Docker/API/Images.pm view on Meta::CPAN
$params{cpushares} = $opts{cpushares} if defined $opts{cpushares};
$params{cpusetcpus} = $opts{cpusetcpus} if defined $opts{cpusetcpus};
$params{cpuperiod} = $opts{cpuperiod} if defined $opts{cpuperiod};
$params{cpuquota} = $opts{cpuquota} if defined $opts{cpuquota};
$params{shmsize} = $opts{shmsize} if defined $opts{shmsize};
$params{networkmode} = $opts{networkmode} if defined $opts{networkmode};
$params{platform} = $opts{platform} if defined $opts{platform};
$params{target} = $opts{target} if defined $opts{target};
if ($opts{buildargs}) {
require JSON::MaybeXS;
$params{buildargs} = JSON::MaybeXS::encode_json($opts{buildargs});
}
if ($opts{labels}) {
require JSON::MaybeXS;
$params{labels} = JSON::MaybeXS::encode_json($opts{labels});
}
my $raw = ref $context eq 'SCALAR' ? $$context : $context;
return $self->client->_request('POST', '/build',
raw_body => $raw,
content_type => 'application/x-tar',
params => \%params,
);
}
lib/API/Docker/API/Images.pm view on Meta::CPAN
undef,
params => \%params,
headers => { 'X-Registry-Auth' => $auth_header },
);
}
sub _build_registry_auth_header {
my ($auth) = @_;
# The Docker Engine requires an X-Registry-Auth header on every push,
# even for anonymous attempts. Encoding is base64url of a JSON object.
require JSON::MaybeXS;
require MIME::Base64;
my $payload;
if (!defined $auth) {
$payload = '{}';
}
elsif (ref $auth eq 'HASH') {
$payload = JSON::MaybeXS::encode_json($auth);
}
else {
# Already pre-built JSON or pre-encoded string. If it looks base64-like
# (no braces), pass through; otherwise encode as-is.
return $auth if $auth =~ /^[A-Za-z0-9+\/=_\-]+$/;
$payload = $auth;
}
my $b64 = MIME::Base64::encode_base64($payload, '');
$b64 =~ tr{+/}{-_};
$b64 =~ s/=+$//;
return $b64;
}
lib/API/Docker/API/Images.pm view on Meta::CPAN
password => 'secret',
serveraddress => 'https://index.docker.io/v1/',
});
Push an image to a registry. Optionally specify C<tag>.
The Docker Engine requires an C<X-Registry-Auth> header on every push,
even for anonymous attempts; the header is always sent. Pass C<auth> as
a hashref of credentials (typical keys: C<username>, C<password>,
C<serveraddress>, or C<identitytoken>), or as a pre-encoded base64 string.
Without C<auth> the header carries an empty JSON object.
=head2 tag
$images->tag('nginx:latest', repo => 'myrepo/nginx', tag => 'v1');
Tag an image with a new repository and/or tag name.
=head2 remove
$images->remove('nginx:latest', force => 1);
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
package API::Docker::Role::HTTP;
# ABSTRACT: HTTP transport role for Docker Engine API
our $VERSION = '0.002';
use Moo::Role;
use IO::Socket::UNIX;
use IO::Socket::INET;
use JSON::MaybeXS qw( encode_json decode_json );
use Carp qw( croak );
use Log::Any qw( $log );
use namespace::clean;
requires 'host';
requires 'api_version';
has _socket => (
is => 'lazy',
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
if ($status_code == 204 || !defined($body) || $body eq '') {
return undef;
}
if ($body =~ /^\s*[\{\[]/) {
my $result = eval { decode_json($body) };
return $result if defined $result;
# Streaming endpoints (e.g. /build, /images/create) return
# newline-delimited JSON objects. Parse each line separately.
my @objects;
for my $line (split /\r?\n/, $body) {
next unless $line =~ /\S/;
my $obj = eval { decode_json($line) };
push @objects, $obj if defined $obj;
}
return \@objects if @objects;
}
return $body;
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
Features:
=over
=item * Unix socket transport (C<unix://...>)
=item * TCP socket transport (C<tcp://host:port>)
=item * HTTP/1.1 chunked transfer encoding
=item * Automatic JSON encoding/decoding
=item * Request/response logging via L<Log::Any>
=item * Automatic connection management
=back
Consuming classes must provide C<host> and C<api_version> attributes.
=head2 get
my $data = $client->get($path, %opts);
Perform HTTP GET request. Returns decoded JSON or raw response body.
Options: C<params> (hashref of query parameters),
C<headers> (hashref of extra HTTP headers, e.g. C<< { 'X-Registry-Auth' => $b64 } >>).
=head2 post
my $data = $client->post($path, $body, %opts);
Perform HTTP POST request. C<$body> is automatically JSON-encoded if provided.
Options: C<params> (hashref of query parameters),
C<headers> (hashref of extra HTTP headers).
=head2 put
my $data = $client->put($path, $body, %opts);
Perform HTTP PUT request. C<$body> is automatically JSON-encoded if provided.
Options: C<params> (hashref of query parameters).
=head2 delete_request
my $data = $client->delete_request($path, %opts);
Perform HTTP DELETE request.
Options: C<params> (hashref of query parameters).
t/images_push_auth.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use JSON::MaybeXS qw( decode_json );
use MIME::Base64 qw( decode_base64 decode_base64url );
use API::Docker::API::Images;
sub b64url_decode {
my ($s) = @_;
$s =~ tr{-_}{+/};
my $pad = (4 - length($s) % 4) % 4;
$s .= '=' x $pad;
return decode_base64($s);
}
subtest 'empty/undef auth -> base64url("{}")' => sub {
my $hdr = API::Docker::API::Images::_build_registry_auth_header(undef);
ok length($hdr), 'header is non-empty for undef';
is_deeply(decode_json(b64url_decode($hdr)), {},
'decodes to empty JSON object');
};
subtest 'hashref auth -> JSON-encoded credentials' => sub {
my $auth = {
username => 'me',
password => 'secret',
serveraddress => 'https://index.docker.io/v1/',
};
my $hdr = API::Docker::API::Images::_build_registry_auth_header($auth);
is_deeply(decode_json(b64url_decode($hdr)), $auth,
'header roundtrips through base64url + JSON');
};
subtest 'identitytoken auth' => sub {
my $auth = { identitytoken => 'tok-123', serveraddress => 'ghcr.io' };
my $hdr = API::Docker::API::Images::_build_registry_auth_header($auth);
is_deeply(decode_json(b64url_decode($hdr)), $auth,
'identitytoken roundtrips');
};
subtest 'pre-encoded base64-like string passes through' => sub {
t/lib/Test/API/Docker/Mock.pm view on Meta::CPAN
package Test::API::Docker::Mock;
use strict;
use warnings;
use JSON::MaybeXS qw( decode_json encode_json );
use Path::Tiny;
use Carp qw( croak );
use Test::More;
use Exporter 'import';
our @EXPORT = qw(
test_docker
load_fixture
is_live
can_write
( run in 1.877 second using v1.01-cache-2.11-cpan-140bd7fdf52 )