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]`

CLAUDE.md  view on Meta::CPAN

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

Changes  view on Meta::CPAN


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

META.json  view on Meta::CPAN

         "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",

META.json  view on Meta::CPAN

            "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",

META.json  view on Meta::CPAN

      "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"
}

META.yml  view on Meta::CPAN

  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'

META.yml  view on Meta::CPAN

      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
);


cpanfile  view on Meta::CPAN

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 )