API-Docker

 view release on metacpan or  search on metacpan

.claude/agents/api-docker-type-writer.md  view on Meta::CPAN

worth more than a tidy model.

## Done means checkable

A class is finished when `maint/spec-drift-check.pl` reports it with no missing and no
extra fields, every attribute carries an `=attr` block, and `prove -lr t/` is green.
Report the drift checker's output, not your impression of the class.

When you are one of several agents writing classes in parallel, stay inside the files you
were given. The registry is shared state at runtime but one file per class on disk;
collisions come from editing the DSL, the drift checker or the prefix map, so say so
rather than doing it.

.claude/rules/api-docker-rules.md  view on Meta::CPAN

   handling reaches every module in `lib/` and the mock harness at once.
7. **Tests verify intent, not just behavior** — a test that can't fail when the logic
   changes is wrong, and a helper that normalises its input before asserting is that
   test. Reproduce a bug before fixing it; leave the regression behind.
8. **Checkpoint after every significant step** — summarize: done / verified / left.
9. **Match conventions** — conformance > taste. Surface a harmful convention; don't fork
   silently.
10. **Fail loud** — "Done" is wrong if anything was skipped. "Tests pass" is wrong if any
    were skipped — and in this repo a skip is the default failure mode, see below.
11. **A red test is a claim before it is a failure** — before changing code to turn a
    test green, say what the test asserts and whether your fix keeps that claim or
    replaces it. If the claim is wrong, fix the claim and say so.

## Delegation

This rule depends on whether the Agent/Task tool is available to you.

- **You can spawn subagents** (orchestrating main agent): Do NOT touch behavior-relevant
  code yourself — delegate. Your lane: coordinate, inspect, plan, review diffs, run
  tests, manage git, edit non-behavioral docs. When in doubt, delegate. Why: only the
  `api-docker-*` agents get their skills force-loaded via `briefing.skills`; you get no
  briefing and would touch internals with too little context.

.claude/skills/api-docker-type-model/SKILL.md  view on Meta::CPAN

older ones for the diff that produces `since`.

    https://docs.docker.com/reference/api/engine/version/v1.51.yaml

Parse it with `YAML::XS`, not `YAML::PP`: Docker's `example:` blocks are
multi-line flow maps whose closing brace is under-indented, and YAML::PP
refuses the file. Both maint scripts read the spec through
`maint/spec-common.pl` so field order comes from one place.

A field's `description` is its `=attr` text — rewrapped, its grammar
straightened, its meaning intact. Where the spec describes nothing, say it is
undocumented upstream rather than inventing a sentence. Where the spec
describes neither a definition nor its schema, the generator derives what it
is from `paths:` or from the definitions that reference it; that is a
measurement of the same file, not an invention.

## Completion criteria

- `perl maint/spec-to-type.pl --verify DIR` — every class compared, every one
  identical, the diff empty.
- `perl maint/spec-drift-check.pl --baseline …` — zero in all seven tiers.

CLAUDE.md  view on Meta::CPAN

The agents carry their skills via `briefing.skills` (see `.claude/agents/`);
the main agent delegates rather than loading them. Skill sources live under
`.claude/skills/` — `api-docker-core` is owned here, the rest are hardlinks
managed with `manage-skills` (`docker-engine-api` lives in the shared library and
is reused by `../p5-dist-zilla-plugin-docker-api`).

Ticket coordination runs on the repo's `karr` board (`karr board`).

## When changing behavior

- Add a `Changes` entry under `{{$NEXT}}`, and say what was measured.
- Update the POD on the affected class. POD lives next to the code
  (`=method`, `=attr`, `=head1 SYNOPSIS` ...) and is woven by the
  `@Author::GETTY` bundle.
- If you change a public method signature or a return shape, check that
  callers in the workspace (notably `../p5-dist-zilla-plugin-docker-api`)
  still build and test green.

lib/API/Docker.pm  view on Meta::CPAN

    );

    # 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

lib/API/Docker/API/Containers.pm  view on Meta::CPAN

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

lib/API/Docker/API/Containers.pm  view on Meta::CPAN


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

    # Inspect container details
    my $container = $docker->containers->inspect($result->{Id});
    say $container->name;

    # Stop and remove
    $docker->containers->stop($result->{Id}, timeout => 10);
    $docker->containers->remove($result->{Id});

    # View logs (ArrayRef of { stream => 'stdout'|'stderr'|'raw', data => ... })
    my $frames = $docker->containers->logs($result->{Id}, tail => 100);
    my $text = join '', map { $_->{data} } @$frames;

    # Attach one-way: replays the same frames and returns (stream => 0 by

lib/API/Docker/API/Containers.pm  view on Meta::CPAN

    my $container = $containers->inspect($id);

Get detailed information about a container. Returns an
L<API::Docker::Type::ContainerInspectResponse> -- see L</"The two container
shapes">.

=head2 start

    $containers->start($id);

    say 'was already running' unless $containers->start($id);

Start a container. Returns 1 when the container was started and 0 when it was
already running: the engine answers a state change with 204 and a no-op with
B<304 Not Modified>, and both carry an empty body, so until now both came back
as C<undef>.

The no-op keeps the falsy value this method always returned -- 0 where it used
to be C<undef> -- so a caller that ignores the return or tests it for falseness
is unaffected; only a caller testing C<defined> sees a difference. A failure is
still a croak, never a 0.

=head2 stop

    $containers->stop($id, timeout => 10);

    say 'was already stopped' unless $containers->stop($id);

Stop a container. Returns 1 when the container was stopped and 0 when it was
already stopped -- the engine answers the no-op with B<304 Not Modified>. See
L</start> for what that 0 replaces.

Options:

=over

=item * C<timeout> - Seconds to wait before killing (default 10)

lib/API/Docker/API/Containers.pm  view on Meta::CPAN

caller cannot check afterwards, because checking afterwards is the thing that
stops working. A guard is worth an unclosable race when the alternative is
unrecoverable, and is not worth it when the caller can simply ask again.

What can be caught for free already is: Podman reports its refusal in the body
and this method croaks on it, with no extra request and no race.

=head2 changes

    for my $change (@{ $containers->changes($id) }) {
        say $KIND[ $change->{Kind} ], ' ', $change->{Path};
    }

Report which paths in the container's filesystem differ from the image it was
created from -- the endpoint behind C<docker diff>. Returns an ArrayRef of
HashRefs, each with C<Path> and C<Kind>:

    [ { Path => '/etc/hostname', Kind => 0 },
      { Path => '/tmp/new',      Kind => 1 },
      { Path => '/etc/gone',     Kind => 2 } ]

lib/API/Docker/API/Containers.pm  view on Meta::CPAN


=head2 get_archive

    use Path::Tiny;
    my $tar = $containers->get_archive($id, path => '/etc/hostname');
    path('hostname.tar')->spew_raw($tar);

    # and what the path was, without a second request
    my %stat;
    my $tar = $containers->get_archive($id, path => '/var/log', stat => \%stat);
    say $stat{name};

Read a path out of a container as a tar archive -- the outbound half of
C<docker cp>. Returns the raw archive bytes, never decoded and never modified.

A file comes back as a one-member archive named after its basename; a
directory comes back as the directory and everything under it, with paths
relative to its parent. The whole archive is buffered in memory.

Options:

lib/API/Docker/API/Containers.pm  view on Meta::CPAN


=item * C<copyUIDGID> - Keep the UID and GID recorded in the archive instead
of mapping the members to the container user

=back

=head2 stat_archive

    my $stat = $containers->stat_archive($id, path => '/etc/hostname');

    say $stat->{name};                        # hostname
    say $stat->{size};                        # 13
    printf "%04o\n", $stat->{mode} & 0777;    # 0644

Stat a path inside a container without transferring it -- C<HEAD> on the same
endpoint L</get_archive> uses. Returns a HashRef, or C<undef> when the engine
answered without the header. A path that does not exist is a croak from the
transport's status handling, not an C<undef>.

The response has no body at all: the answer is the
C<X-Docker-Container-Path-Stat> header, base64-encoded JSON, which this method
decodes. Its keys are the engine's, passed through as they arrive:

lib/API/Docker/API/Images.pm  view on Meta::CPAN

    use Path::Tiny;
    my $tar = path('context.tar')->slurp_raw;
    $docker->images->build(context => $tar, t => 'myapp:latest');

    # Pull an image
    $docker->images->pull(fromImage => 'nginx', tag => 'latest');

    # List images
    my $images = $docker->images->list;
    for my $image (@$images) {
        say $image->id;
        say join ', ', @{$image->repo_tags};
    }

    # Inspect image details
    my $image = $docker->images->inspect('nginx:latest');

    # Tag and push
    $docker->images->tag('nginx:latest', repo => 'myrepo/nginx', tag => 'v1');
    $docker->images->push('myrepo/nginx', tag => 'v1');

    # Remove image

lib/API/Docker/API/Networks.pm  view on Meta::CPAN

    my $docker = API::Docker->new;

    # Create a network
    my $result = $docker->networks->create(
        Name   => 'my-network',
        Driver => 'bridge',
    );

    # List networks
    my $networks = $docker->networks->list;
    say $_->name, ' ', $_->driver for @$networks;

    # Connect/disconnect containers
    $docker->networks->connect($network_id, Container => $container_id);
    $docker->networks->disconnect($network_id, Container => $container_id);

    # Remove network
    $docker->networks->remove($network_id);

=head1 DESCRIPTION

lib/API/Docker/API/Plugins.pm  view on Meta::CPAN


    # Install: look at what the plugin demands, then grant exactly that
    my $privileges = $docker->plugins->privileges('vieux/sshfs:latest');
    $docker->plugins->install('vieux/sshfs:latest',
        privileges => $privileges,
    );
    $docker->plugins->enable('vieux/sshfs:latest');

    # Inspect
    my $plugin = $docker->plugins->inspect('vieux/sshfs:latest');
    say $plugin->name, $plugin->enabled ? ' (enabled)' : ' (disabled)';

    # Configure, upgrade, disable, remove
    $docker->plugins->configure('vieux/sshfs:latest', ['DEBUG=1']);
    $docker->plugins->upgrade('vieux/sshfs:latest', privileges => $privileges);
    $docker->plugins->disable('vieux/sshfs:latest');
    $docker->plugins->remove('vieux/sshfs:latest');

=head1 DESCRIPTION

This module provides access to the Docker managed-plugin endpoints

lib/API/Docker/API/Plugins.pm  view on Meta::CPAN

the moment it flushes the first progress object. A failure before that point
arrives as a real error status -- C<incorrect privileges> is reported this
way, since it is decided before anything is pulled -- and one after it
arrives as an C<errorDetail> object inside the 200 stream, which croaks with
an L<API::Docker::Error::Stream>. C<eval> and inspect C<$@> as a string
rather than testing for the exception class.

=head2 inspect

    my $plugin = $plugins->inspect('vieux/sshfs:latest');
    say $plugin->enabled;
    say join ', ', @{ $plugin->settings->env };

Get detailed information about an installed plugin. Returns an
L<API::Docker::Type::Plugin> -- the same class L</list> returns; see
L</"What this class returns">.

The name may carry a registry host, a repository path and a tag
(C<docker.io/vieux/sshfs:latest>) and is interpolated into the request path
as given: the daemon routes this endpoint as C<< /plugins/{name:.*}/json >>,
so the slashes and the colon must survive unescaped, and they do.

lib/API/Docker/API/Secrets.pm  view on Meta::CPAN


    # Create a secret -- Data is RAW BYTES, this class base64-encodes it
    my $created = $docker->secrets->create(
        Name   => 'my-secret',
        Data   => "hunter2\n",
        Labels => { env => 'prod' },
    );

    # Inspect a secret -- an API::Docker::Type::Secret
    my $secret = $docker->secrets->inspect($created->{ID});
    say $secret->spec->name;

    # Update: the version comes from the inspect above, and is mandatory
    my %spec = %{ $secret->spec->TO_JSON };
    $spec{Labels} = { env => 'staging' };
    $secret->update(%spec);

    # Remove
    $docker->secrets->remove($created->{ID});

=head1 DESCRIPTION

lib/API/Docker/API/System.pm  view on Meta::CPAN

=head1 VERSION

version 0.004

=head1 SYNOPSIS

    my $docker = API::Docker->new;

    # System information
    my $info = $docker->system->info;
    say "Docker version: " . $info->{ServerVersion};

    # API version
    my $version = $docker->system->version;
    say "API version: " . $version->{ApiVersion};

    # Health check
    my $pong = $docker->system->ping;

    # Monitor events
    my $events = $docker->system->events(
        since => time() - 3600,
    );

    # Disk usage
    my $df = $docker->system->df;

    # Check registry credentials before doing the work that needs them
    my $login = $docker->system->auth(
        username      => 'me',
        password      => 'secret',
        serveraddress => 'ghcr.io',
    );
    say $login->{Status};   # Login Succeeded

=head1 DESCRIPTION

This module provides access to Docker system-level operations including daemon
information, version detection, health checks, and event monitoring.

Accessed via C<< $docker->system >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->system->using(read_timeout => 5) >>.

lib/API/Docker/API/System.pm  view on Meta::CPAN

=head2 Following the feed

An unbounded C</events> is the endpoint this client could not use at all.
Pass C<on_event> and the events are handed over one at a time as the daemon
sends them:

    my $summary = $system->events(
        since    => time - 60,
        on_event => sub {
            my ($event, $stop) = @_;
            say $event->{status};
            $stop->() if $event->{status} eq 'destroy';
        },
    );

    $summary;   # { delivered => 12, stopped => 1 }

With a callback the return value is that summary HashRef, not the events:
C<delivered> is how many went to the callback, C<stopped> is 1 when the
callback ended the feed and 0 when the daemon did. Nothing is accumulated --
a feed that runs for a day must not cost memory in proportion to how long it

lib/API/Docker/API/Volumes.pm  view on Meta::CPAN

    my $volume = $docker->volumes->create(
        Name   => 'my-volume',
        Driver => 'local',
    );

    # List volumes
    my $volumes = $docker->volumes->list;

    # Inspect volume
    my $vol = $docker->volumes->inspect('my-volume');
    say $vol->mountpoint;

    # Remove volume
    $docker->volumes->remove('my-volume');

=head1 DESCRIPTION

This module provides methods for managing Docker volumes including creation,
listing, inspection, and removal.

L</list>, L</inspect> and L</create> all return

lib/API/Docker/Role/Entity/Config.pm  view on Meta::CPAN


=head1 VERSION

version 0.004

=head1 SYNOPSIS

    my $docker = API::Docker->new;
    my ($config) = @{ $docker->configs->list };

    say $config->id;
    say $config->spec->name;
    say $config->spec->data;        # still base64, as the daemon sent it
    say $config->decoded_data;      # the bytes

    my %spec = %{ $config->spec->TO_JSON };
    delete $spec{Data};             # already base64 -- see update
    $spec{Labels} = { app => 'web' };
    $config->update(%spec);

    $config->remove;

=head1 DESCRIPTION

lib/API/Docker/Role/Entity/Container.pm  view on Meta::CPAN


version 0.004

=head1 SYNOPSIS

    my $docker = API::Docker->new;

    # from list: an API::Docker::Type::ContainerSummary
    my ($container) = @{ $docker->containers->list };

    say $container->id;
    say $container->status;             # "Up 2 hours"
    say $container->state;              # "running"

    $container->start;
    $container->stop(timeout => 10);
    my $logs = $container->logs(tail => 100);
    $container->remove(force => 1);

    # from inspect: an API::Docker::Type::ContainerInspectResponse, where
    # the same methods work and `state` is an object
    my $full = $docker->containers->inspect($container->id);
    say $full->state->status;
    say $full->state->exit_code;

    if ($full->is_running) { ... }

=head1 DESCRIPTION

The convenience methods of a container. This role is composed, at load time,
into the two generated classes the daemon answers container requests with:

=over

lib/API/Docker/Role/Entity/Container.pm  view on Meta::CPAN

differences that have bitten. L</is_running> is the one method that reads
both shapes.

Why the methods are a role applied to generated classes rather than a class
of their own: L<API::Docker::Role::Entity/DESCRIPTION>.

=head2 start

    $container->start;

    say 'was already running' unless $container->start;

Start the container. Returns 1 when it was started and 0 when it was already
running. Delegates to L<API::Docker::API::Containers/start>, which documents
what that 0 replaces.

=head2 stop

    $container->stop(timeout => 10);

Stop the container. Returns 1 when it was stopped and 0 when it was already

lib/API/Docker/Role/Entity/Image.pm  view on Meta::CPAN


version 0.004

=head1 SYNOPSIS

    my $docker = API::Docker->new;

    # from list: an API::Docker::Type::ImageSummary
    my ($image) = @{ $docker->images->list };

    say $image->id;
    say join ', ', @{ $image->repo_tags };
    say $image->size;

    $image->tag(repo => 'myrepo/app', tag => 'v1');
    $image->remove(force => 1);

    # from inspect: an API::Docker::Type::ImageInspect, where the same
    # methods work and the extra build metadata is there
    my $full = $image->inspect;
    say $full->architecture;
    say $full->os;
    say $full->config->cmd->[0];

=head1 DESCRIPTION

The convenience methods of an image. This role is composed, at load time,
into the two generated classes the daemon answers image requests with:

=over

=item * L<API::Docker::Type::ImageSummary> -- one entry of
C<GET /images/json>, what L<API::Docker::API::Images/list> returns

lib/API/Docker/Role/Entity/Network.pm  view on Meta::CPAN


=head1 VERSION

version 0.004

=head1 SYNOPSIS

    my $docker = API::Docker->new;
    my ($network) = @{ $docker->networks->list };

    say $network->name;
    say $network->driver;
    say $network->ipam->config->[0]->subnet;

    $network->connect(Container => $container_id);
    $network->disconnect(Container => $container_id);
    $network->remove;

=head1 DESCRIPTION

The convenience methods of a network. This role is composed, at load time,
into L<API::Docker::Type::Network>, the generated class the daemon answers
network requests with.

lib/API/Docker/Role/Entity/Plugin.pm  view on Meta::CPAN


=head1 VERSION

version 0.004

=head1 SYNOPSIS

    my $docker = API::Docker->new;
    my ($plugin) = @{ $docker->plugins->list };

    say $plugin->name;
    say $plugin->enabled ? 'enabled' : 'disabled';
    say join ', ', @{ $plugin->settings->env };

    $plugin->disable;
    $plugin->configure(['DEBUG=1']);
    $plugin->enable;

=head1 DESCRIPTION

The convenience methods of a Docker managed plugin. This role is composed, at
load time, into L<API::Docker::Type::Plugin>, the generated class the daemon
answers plugin requests with -- the same definition for

lib/API/Docker/Role/Entity/Secret.pm  view on Meta::CPAN


=head1 VERSION

version 0.004

=head1 SYNOPSIS

    my $docker = API::Docker->new;
    my ($secret) = @{ $docker->secrets->list };

    say $secret->id;
    say $secret->spec->name;
    say $secret->version_index;

    my %spec = %{ $secret->spec->TO_JSON };
    $spec{Labels} = { env => 'staging' };
    $secret->update(%spec);

    $secret->remove;

=head1 DESCRIPTION

The convenience methods of a Docker secret. This role is composed, at load

lib/API/Docker/Role/Entity/Volume.pm  view on Meta::CPAN


=head1 VERSION

version 0.004

=head1 SYNOPSIS

    my $docker = API::Docker->new;
    my ($volume) = @{ $docker->volumes->list };

    say $volume->name;
    say $volume->driver;
    say $volume->mountpoint;

    $volume->remove;

=head1 DESCRIPTION

The convenience methods of a volume. This role is composed, at load time,
into L<API::Docker::Type::Volume>, the generated class the daemon answers
volume requests with.

=head2 One class for all three calls

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

    : length($partial)
      ? length($partial) . ' byte'
        . (length($partial) == 1 ? '' : 's') . ' arrived'
      : 'nothing arrived at all';

  # Empty when a reader is driven directly rather than through _request, which
  # is how t/role_http.t drives them: an endpoint nobody named is left out of
  # the message rather than interpolated as the empty string.
  my $endpoint = defined $ctx->{endpoint} ? $ctx->{endpoint} : '';

  # The two phases with an announced length say the same sentence about it, so
  # they name the piece and let this write it; the two without pass the whole
  # detail, there being no count to put in one.
  my $detail = $what{detail};
  unless (defined $detail) {
    my $short = $what{expected} - $what{received};
    $detail = $what{piece} . ' stopped ' . $short . ' byte'
      . ($short == 1 ? '' : 's') . ' short of the ' . $what{expected}
      . ' it announced';
  }

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

    . 'and :/@!$&\'()*+,;=%). It is spliced straight into the request line, so '
    . 'a space, CR, LF, ? or # in a container name or image reference would '
    . 'rewrite the request rather than name a resource. A path is rejected '
    . 'rather than sanitised: percent-encoding it here cannot tell a path '
    . 'separator from data -- pass query parameters as `params`, not in the '
    . 'path';
}

sub _read_response {
  my ($self, $sock, $method, $ctx) = @_;
  # A context is what _request builds to say how long a silence may last and
  # what the exception has to name. It defaults to an empty one -- no timeout,
  # every read exactly as it was -- so the readers stay drivable directly, as
  # t/role_http.t drives them.
  $ctx ||= {};

  my $head = $self->_read_head($sock, $ctx);
  return [ @$head, $self->_read_body($sock, $head->[2], $method, $ctx) ];
}

sub _read_head {

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

    # kept is truncation rather than the end of the body; see _read_exact.
    $self->_read_exact($sock, $len, \$body, $ctx, 'content-length',
      'the body');
    return $body;
  }

  # Read until the daemon closes. This used to be a `local $/; <$sock>` slurp;
  # it is a loop over the same primitive as the other two branches now, which
  # is what karr k60 needed and what the timeout wanted anyway -- with $/ undef
  # a whole body and a truncated one are both just bytes, so the slurp's own
  # result could never say which it was. This is the path karr k52's hang is
  # on, an attach whose buffered frames arrive and whose socket then never
  # closes.
  #
  # And the one shape with no completeness check to make: the response
  # announced no end, so the close IS the end (karr k64). Treating an EOF here
  # as truncation would make every attach, every logs(follow) and every
  # exec/start fail on the daemon hanging up, which is how all three finish.
  my $body = '';
  # What a timeout hands over instead of dropping; see the content-length
  # branch above.

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN



sub stream_frames {
  my ($self, $method, $path, %opts) = @_;

  my $tty = delete $opts{tty};

  if (my $cb = delete $opts{on_frame}) {
    # tty is a declaration here, not the hint it is on the buffered path. The
    # sniff below needs the whole body to decide, and the whole body is what a
    # callback stream does not have; so an unframed stream has to say so, and
    # anything not declared is required to be framed.
    return $self->_request($method, $path, %opts,
      $tty
        ? ( on_chunk => sub { $cb->({ stream => 'raw', data => $_[0] }, $_[1]) } )
        : ( on_frame => $cb ),
    );
  }

  my $body = $self->_request($method, $path, %opts, raw => 1);

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

it; requiring it would make this client unbuildable on a machine with no
OpenSSL headers for the sake of a transport it is not using. Without it,
C<< tls => 1 >> croaks naming the module and how to install it, at the same
point every other connection failure is reported.

=head2 read_timeout

Seconds of silence after which a request gives up and croaks with an
L<API::Docker::Error::Timeout>. C<undef> -- the default, and what every
existing caller gets -- means no timeout at all and is the behaviour this
distribution has always had. C<0> means the same and is the way to say it
explicitly, so a client carrying a default can be opted out of per request.

    my $docker = API::Docker->new(read_timeout => 30);
    $docker->system->using(read_timeout => 0)->events;   # this one may wait

Per request it is an option of L</get>, L</post>, L</put>, L</delete_request>
and L</head>. A resource class carries it through
L<API::Docker::Role::Using/using>, which clones the class rather than taking
it per method -- up for a slow endpoint, down for a stream that should not
stall, off with C<0>.

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN

See L</"Bounding a request that never ends"> for what it does and does not
cover, and L<API::Docker/"What a timeout covers"> for the same question
asked of both bounds at once.

=head2 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'>. C<undef>
-- the default, and what every existing caller gets -- means no bound and is
the behaviour this distribution has always had; C<0> means the same and is the
way to say it explicitly.

    my $docker = API::Docker->new(connect_timeout => 5);
    $docker->system->using(connect_timeout => 0)->version;  # may wait

Separate from L</read_timeout> rather than folded into it, because the two
bound different things and want different numbers: a connect is either
immediate or broken, while a read is waiting on work the daemon has to do.

Per request it is an option of L</get>, L</post>, L</put>, L</delete_request>
and L</head>. A resource class carries it through

lib/API/Docker/Role/HTTP.pm  view on Meta::CPAN


This is the only way to use C<< follow => 1 >> at all: without it the request
does not return until the container exits.

The frame shape is the same either way, C<tty> included -- a TTY stream
arrives as a series of C<< { stream => 'raw', ... } >> frames rather than the
single one the buffered path builds, so a caller still never branches on it.

C<tty> is a declaration here rather than the hint it is on the buffered path.
Deciding framing from the bytes needs the whole body, which is precisely what
is not being kept; so an unframed stream must say so, and one that does not
and is not framed croaks instead of inventing frames from its payload.

=head2 Detecting a framed stream

A container created without a TTY produces the Docker stream format -- an
8-byte header per frame (byte 0 the stream type, bytes 4-7 a big-endian uint32
payload length) followed by that many payload bytes. With a TTY there is no
header and the payload is raw pty output.

The engine is supposed to distinguish the two with the response C<Content-Type>

lib/API/Docker/Role/Type.pm  view on Meta::CPAN


sub _entity_attribute_index {
  my $class = ref($_[0]) || $_[0];
  return $ENTITY_CACHE{$class} //= do {
    my %mine = $class->can('_entity_attributes')
      ? (map { ($_ => 1) } $class->_entity_attributes)
      : ();
    my $reg  = _docker_attr_registry($class);
    my $wire = _docker_wire_index($class);
    # Both sets reach the same constructor, so a name in both is an ambiguity
    # nobody can resolve at runtime -- say so instead of picking one.
    for my $name (sort keys %mine) {
      croak __PACKAGE__ . ": $class has '$name' as an entity attribute and as "
        . 'a daemon field; one of the two has to be renamed'
        if $reg->{$name} || defined $wire->{$name};
    }
    \%mine;
  };
}

# --- merged views over @ISA ------------------------------------------------

lib/API/Docker/Type.pm  view on Meta::CPAN


my %SCALAR_TYPE = (
  Str  => Str,
  Int  => Int,
  Num  => Num,
  Bool => Bool,
);

# Names API::Docker::Role::Type already occupies. A generated attribute that
# collided with one of these would silently replace it, so it is refused
# instead -- the generator has to pick another Perl name and say so with an
# explicit `wire`.
my %RESERVED = map { ($_ => 1) } qw(
  new BUILDARGS unknown_fields rejected_fields from_data from_json TO_JSON to_json
  docker_attributes docker_attribute_order docker docker_extends
);

sub import {
  my ($class) = @_;
  $class->_setup_class(scalar caller);
  return;

lib/API/Docker/Type/ContainerBlkioStats.pm  view on Meta::CPAN

cgroups v1. On a cgroup v2 host, all fields other than
C<io_service_bytes_recursive> are omitted or C<null>.

This type is only populated on Linux and omitted for Windows containers.

=head2 io_service_bytes_recursive

Undocumented upstream. Bytes transferred, one entry per device and
operation. It is the only one of the eight arrays a cgroup v2 host fills in,
which is also why it is the only one carrying no description: the other
seven each say they are cgroup v1 only, and this one had nothing left to
qualify. See L<API::Docker::Type::ContainerBlkioStatEntry>. Serialised as
C<io_service_bytes_recursive> -- spelled out, because deriving it from the
Perl name would produce C<IoServiceBytesRecursive>.

=head2 io_serviced_recursive

This field is only available when using Linux containers with cgroups v1. It
is omitted or C<null> when using cgroups v2. See
L<API::Docker::Type::ContainerBlkioStatEntry>. Serialised as
C<io_serviced_recursive> -- spelled out, because deriving it from the Perl

lib/API/Docker/Type/ImageHistoryResponseItem.pm  view on Meta::CPAN

version 0.004

=head1 DESCRIPTION

Generated from the C<ImageHistoryResponseItem> definition of
C<spec/v1.51.yaml>. The swagger describes none of the six fields, but the
example response it gives for C<GET /images/{name}/history> shows all six
across three layers. That example is not the last word: measured against
Podman 5.8.4 (API 1.44), a locally built image answers ten entries that
contradict it on three of the six, so L</id>, L</tags> and L</comment> each
say what the spec shows and what the engine sent.

=head2 id

Undocumented upstream. The swagger's example response spells this as a bare
hex digest with no C<sha256:> prefix. The engine does not. Measured against
Podman 5.8.4 (API 1.44), a locally built image answers ten entries of which
exactly one -- the image's own top layer -- carries a real C<sha256:...>
digest; the other nine carry the literal string C<< sha256:<missing> >>,
which is a marker and not an ID. Handing that to C<GET /images/{id}/json>
looks up nothing, so test for it before treating this field as a reference

t/lib/Test/API/Docker/Mock.pm  view on Meta::CPAN

# whose line framing is the thing under test. Both must come back byte-exact.
sub load_fixture_raw {
  my ($name) = @_;
  my $file = $FIXTURES_DIR->child($name);
  croak "Fixture not found: $file" unless $file->exists;
  return $file->slurp_raw;
}

# The status line and the response headers reach a caller through the
# `response` out-parameter of _request, which the mock replaces wholesale --
# so without this a mocked route cannot say 304, and the very distinction
# API::Docker::API::Containers/start now makes would be untestable offline.
# A plain route keeps working and gets a status inferred from its value.
#
# The error phrases are here because API::Docker::Error::HTTP carries
# `reason` as well: a mocked 404 falling through to 'Unknown' would put a
# value on the exception that no engine ever sends.
my %REASON = (
  200 => 'OK',
  204 => 'No Content',
  304 => 'Not Modified',

t/read_timeout.t  view on Meta::CPAN

    isa_ok $err, 'API::Docker::Error::Timeout';
    is_deeply attr($err, "summary"), { delivered => 2, stopped => 0 },
      'the units the callback did get, counted the way a clean end counts them';
    is attr($err, "partial"), '',
      'and no body: a streamed request keeps none by design';
    like attr($err, "message"), qr/2 units/, 'the message says so too';
  };

  subtest 'everything that arrived is delivered before the expiry' => sub {
    # This is the shape karr k52 actually has: everything the daemon had to
    # say arrives, and the socket then stays open and silent. Measured against
    # Podman 5.8.4 on an attach to an exited container: two frames, 42 bytes,
    # delivered 0 before karr k59 and 2 after.
    #
    # Under read() those 42 bytes and the expiry were one call, and k59 had to
    # rescue them out of it. Under sysread they are two -- the delivery, then
    # the silence -- and the property holds without a rescue, which is why the
    # script below has two entries where it used to have one. What is asserted
    # is the property, not the mechanism: at the moment the exception is
    # raised, the caller is holding everything the daemon sent.
    my @got;

t/secrets_configs.t  view on Meta::CPAN

use API::Docker::Role::Entity::Secret;
use API::Docker::Error::HTTP;
use lib 't/lib';
use Test::API::Docker::Mock;

check_live_access();

# Most of what this file asserts is the shape of the *outgoing* request --
# that Data leaves as base64 and that Version.Index leaves as the `version`
# query parameter. Neither is visible from a response, so those subtests are
# mock-only and say so rather than passing vacuously against a daemon.
my $REQUEST_SHAPE = 'asserts the outgoing request; only the mock can see it';

# The Engine API reference's shape for a config, kept inline instead of in
# t/fixtures/: the files there are captured from a real daemon, and no engine
# reachable from this repo serves /configs. Podman answers the collection GET
# (/configs) with a plain-text "Not Found" 404, unchanged since it was first
# measured on 5.4.2 / API 1.41 -- but every item-scoped route under it
# (create, inspect, update, remove) answers 503 JSON instead, not the same
# 404 (re-measured live, Podman 5.8.4 / API 1.44, karr k62; see the
# $CONFIGS_UNSERVED comment below for the detail). No engine serving

t/stream_incremental.t  view on Meta::CPAN

    sub { push @got, $_[0] }, 1);

  $client->_read_streaming_response($fh, 'GET', $handler, {});

  is join('', @got), 'first second third', 'the whole chunk arrived';
  cmp_ok scalar @got, '>=', 3,
    'in at least as many calls as it arrived in (' . scalar(@got) . ')';
};

subtest 'one byte per read: every structure boundary is fragmented' => sub {
  # The cheapest way to say "no reader assumes anything arrives whole". At one
  # byte per read the status line, each header line, the blank line, the chunk
  # header, the chunk data, the CRLF after it and the terminating zero chunk
  # are each split across several reads -- and the 8-byte frame header inside
  # the payload is split eight ways.
  my $wire = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n";
  my $body = frame('one') . frame('two');
  $wire .= sprintf("%x\r\n", length $body) . $body . "\r\n0\r\n\r\n";

  my @got;
  my $fh = burst_handle(split //, $wire);

t/type_fixture_passthrough.t  view on Meta::CPAN

  my ($gov, $key) = @_;
  return $gov->{inner} if $gov && $gov->{kind} eq 'map';
  return undef unless $gov && $gov->{kind} eq 'object';
  my $attr = wire_index($gov->{class})->{$key};
  return undef unless defined $attr;
  return gov_of_descriptor($gov->{class}->docker_attributes->{$attr}{type});
}

# Returns two lists: keys that went missing, and keys that went missing under
# the null rule. The second is returned rather than swallowed so the caller
# has to say out loud which drops it expects.
sub compare {
  my ($data, $out, $gov, $path, $lost, $nulled) = @_;
  if (ref $data eq 'HASH') {
    unless (ref $out eq 'HASH') {
      push @$lost, "$path (an object came back as " . (ref($out) || 'a plain value') . ')';
      return;
    }
    my $known = $gov && $gov->{kind} eq 'object' ? wire_index($gov->{class}) : {};
    for my $key (sort keys %$data) {
      unless (exists $out->{$key}) {



( run in 2.305 seconds using v1.01-cache-2.11-cpan-a49fcb8fa48 )