view release on metacpan or search on metacpan
.claude/agents/api-docker-doc-writer.md view on Meta::CPAN
POD is interleaved with the code, each `=attr`/`=method` block directly after the
`has`/`sub` it documents, and every class ends with `=seealso`. Option lists are `=over`
blocks with one `=item * C<name> - meaning` per accepted key â mirror the method's own
`%params`/`%opts` handling, including the defaults it applies (`rm` defaults to true in
`build`, `tag` to `latest` in `pull`).
The two places where accuracy matters most, because a reader cannot discover the truth
from the signature:
- **What a method returns.** `list`/`inspect` hand back entity objects; everything else
hands back the raw daemon response, and the streaming endpoints hand back an arrayref
of newline-delimited JSON events â except when the stream held exactly one object.
- **What the client deliberately does not do.** The `CONTAINER ENGINES` section in
`API::Docker` documents that socket discovery reads `DOCKER_HOST` and the default
socket and consults no Docker contexts, and contrasts that with other clients. That
section is a promise about behavior; keep it true or flag it.
Say what is true, do not describe intent as capability â and do not trust a claim of
incapability written down here either. This paragraph used to assert that `tls` and
`cert_path` were unimplemented. They are implemented: `API::Docker::Role::HTTP` carries
a full `IO::Socket::SSL` path, including `tls_insecure` and the `docker` CLI's cert
.claude/agents/api-docker-engine-worker.md view on Meta::CPAN
---
name: api-docker-engine-worker
description: "Docker Engine API specialist for API::Docker â use whenever the question is what the daemon does or expects: adding or correcting an endpoint, query-parameter and filter semantics, response shapes (204/304, NDJSON event streams, error...
model: inherit
allowed-tools: Read, Edit, Write, Bash, Glob, Grep
briefing:
skills:
- docker-engine-api
- api-docker-core
- getty-perl-core
- getty-perl-moo
- getty-perl-release-author-getty
- getty-git-commit-style
.claude/agents/api-docker-engine-worker.md view on Meta::CPAN
in your own URL is what you asked for, not what the engine is.
Changing a public return shape is a cross-repo change: `../p5-dist-zilla-plugin-docker-api`
consumes `images->build`, `->tag`, `->push` and `->inspect` â verify it, or file a ticket
on its board, before landing.
Podman is a reimplementation: anything beyond the documented surface â event payload
fields, healthcheck details, error message text â is unverified until you have measured
it there, and a difference from Docker is worth writing into the `Changes` entry.
New endpoint methods follow the existing shape: options normalised into `%params`,
`list`/`inspect` wrapped into entity objects, everything else returned raw, POD with an
`=item * C<name> - meaning` per accepted key. A behavior change gets a `Changes` entry
under `{{$NEXT}}` that states what was measured.
## Verification
`prove -lr t/` for the fixture suite. For live checks against the Podman socket, set
`API_DOCKER_TEST_HOST`; only add `API_DOCKER_TEST_WRITE=1` when the task genuinely needs
containers created, and clean up what you create. Never run `images->push` against a real
registry, and never `dzil release`.
.claude/rules/api-docker-rules.md view on Meta::CPAN
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.
| Task | Agent |
|---|---|
| Anything turning on what the daemon does or expects â endpoints, wire formats, filters, registry auth, version gating | `api-docker-engine-worker` |
| The Perl side â Moo, transport internals, entity classes, refactoring, cpanfile | `api-docker-worker` (default) |
| Write/extend tests, add fixtures | `api-docker-test-writer` |
| The generated type model, the `API::Docker::Type` DSL, the drift checker, `spec/` | `api-docker-type-writer` |
| Pre-release audit | `api-docker-release-checker` |
| POD and README | `api-docker-doc-writer` |
The two workers split by *question*, not by file: "what does the engine answer here?"
is the engine-worker's, "how is this distribution built?" is the plain worker's. Only
the engine-worker carries the Engine API reference â the other one guessing at daemon
behavior is how a wrong assumption gets cemented.
.claude/rules/api-docker-rules.md view on Meta::CPAN
block, so an interrupted run leaves them behind. Run only when the task is about live
behavior.
- **`prune` destroys, and `dangling => 0` destroys MORE, not less.** `POST
/images/prune` with `filters => { dangling => ['false'] }` removes every
unused *tagged* image on the engine, locally built ones included, and they
are not recoverable. It reads like a narrowing filter and is the opposite.
This has already cost a locally built image, during what its caller
believed was a read-only probe. **No `prune` of any
kind -- images, containers, networks, volumes, build cache -- and no
`rm -a` or `system reset`, on either engine, ever, unless the user names
the command.** Probing what an endpoint answers is not a reason: measure
it against something you created yourself.
- **`images->push` publishes.** With credentials it writes to a real registry under the
maintainer's account. Never run it â nor any test that does â without explicit
instruction.
- **Streaming endpoints block until the daemon closes, unless given a callback.**
`_request` still buffers a whole response by default, so `system->events` or
`containers->stats` without a bound and without `on_event`/`on_frame`/`on_chunk` never
returns. Bound the window, pass a callback, or wrap a manual probe in `timeout` â a
callback still needs `$stop->()` called from somewhere, or it runs until the daemon
closes the connection on its own.
- **`../p5-dist-zilla-plugin-docker-api` consumes this API.** A public signature or
return-shape change is a cross-repo change: verify that repo, or file a ticket on its
board before landing.
- **`[@Author::GETTY]` gathers through `Git::GatherDir`, which sees only tracked
files.** A new `.pm`, test file or fixture is invisible to `dzil build`/`dzil test`
.claude/skills/api-docker-core/SKILL.md view on Meta::CPAN
---
name: api-docker-core
description: "Use when working on the API::Docker distribution's client architecture â the HTTP transport role and its socket handling (unix://, tcp://, TLS), a resource API under API::Docker::API::*, how list/inspect wrap a daemon response into a ...
---
# API::Docker â architecture and invariants
A pure-Perl client for the Docker Engine HTTP API. It speaks HTTP/1.1 directly
over the daemon's socket and never shells out to the `docker` binary, so any
engine serving that API works (Podman's rootless socket needs nothing but
`DOCKER_HOST`).
## The three layers
.claude/skills/api-docker-core/SKILL.md view on Meta::CPAN
ââ ->images / ->containers / ->networks / ->volumes / ->system / ->exec
API::Docker::API::* one class per resource, holds `client`
ââ _wrap / _wrap_list â $class->from_data($data, client => ...)
on an API::Docker::Type::* class, with
API::Docker::Role::Entity::* composed
onto that class at load time
```
**`_request($method, $path, %opts)` is the only way out of the process.** Every
resource method goes through it (usually via `get`/`post`/`put`/
`delete_request`). A new endpoint never opens its own socket.
Options: `body` (JSON-encoded), `raw_body` + `content_type` (tarballs for
`/build`), `params` (query string), `headers` (extra request headers),
`on_event`/`on_frame`/`on_chunk` (streaming callbacks, see below).
`_request` prefixes the path with `/v$api_version`. `around _request` in
`API::Docker` triggers `negotiate_version` on the first call that is not
`/version` â so a mock that replaces `_request` must strip the `/vX.YZ` prefix
itself, which `Test::API::Docker::Mock` does.
.claude/skills/docker-engine-api/SKILL.md view on Meta::CPAN
base64url of a map from registry hostname to auth object, because a build may
pull from several registries.
## Bodies and paths
`POST /build` is the odd one: the request body is the **tar build context**
(`Content-Type: application/x-tar`), and every option â `t`, `dockerfile`,
`buildargs`, `target`, `platform` â rides in the query string. `buildargs` and
`labels` are themselves JSON-encoded strings inside that query.
Container endpoints accept a name or any unambiguous ID prefix. Image
references keep their slashes and tags inside the path
(`/images/myrepo/app:v1/push`) â percent-encoding them breaks the reference.
Names from `GET /containers/json` arrive with a leading `/`.
`exec` is two calls: `POST /containers/{id}/exec` creates the instance and
returns an `Id`, `POST /exec/{id}/start` runs it. The exit status comes from
`GET /exec/{id}/json` afterwards (`ExitCode`), never from the start call.
## Other engines
not trust a number written down here.
12. **`{{$NEXT}}` in `Changes` is the placeholder for the upcoming
release.** Add entries under it as you change behavior; `dzil
release` replaces it with the version + timestamp.
## What this distribution is
A pure-Perl client for the Docker Engine API. No LWP, no shell-outs â
HTTP/1.1 (incl. chunked) is spoken directly over the daemon's Unix
socket (default) or a TCP endpoint. Any engine serving that API works;
Podman needs nothing but `DOCKER_HOST`.
The synchronous `_request` core lives in
`API::Docker::Role::HTTP`; resource-specific API methods live in
`API::Docker::API::*`. Entities hang off the resource APIs: every resource
returns generated `API::Docker::Type::*` classes with an
`API::Docker::Role::Entity::*` composed onto them (karr k79 step 6/7,
finished in k84). There are no hand-written entity wrapper classes left.
Architecture, transport invariants, the streaming and `X-Registry-Auth`
## Layout
```
lib/API/Docker.pm # main client, version negotiation
lib/API/Docker/Role/HTTP.pm # HTTP/1.1 transport (unix:// + tcp://, TLS)
lib/API/Docker/Role/RegistryAuth.pm # X-Registry-Auth / AuthConfig encoding
lib/API/Docker/Role/Filters.pm # the `filters` query parameter, shape-normalised
lib/API/Docker/Role/Using.pm # `using`, the resource class clone that bounds a run of calls
lib/API/Docker/API/System.pm # /version, /info, /_ping, /auth, /events
lib/API/Docker/API/Containers.pm # container endpoints (incl. archive, attach)
lib/API/Docker/API/Images.pm # image endpoints (build, pull, push, tar, commit, ...)
lib/API/Docker/API/Networks.pm # network endpoints
lib/API/Docker/API/Volumes.pm # volume endpoints
lib/API/Docker/API/Exec.pm # exec endpoints
lib/API/Docker/API/Distribution.pm # /distribution registry manifest lookups
lib/API/Docker/API/Secrets.pm # /secrets
lib/API/Docker/API/Configs.pm # /configs
lib/API/Docker/API/Plugins.pm # /plugins
lib/API/Docker/Type.pm # the DSL and attribute registry behind the generated types
lib/API/Docker/Role/Type.pm # a generated type's own behaviour: serialisation, unknown_fields
lib/API/Docker/Type/ # generated from spec/, one class per swagger definition -- karr k79
lib/API/Docker/Role/Entity.pm # the client an entity delegates through
lib/API/Docker/Role/Entity/Container.pm # container operations, composed onto ContainerSummary + ContainerInspectResponse
lib/API/Docker/Role/Entity/Image.pm # image operations, composed onto ImageSummary + ImageInspect
`ndjson` body.
## Delegation
Don't touch behavior-relevant code yourself â hand it to the right agent.
The principle, the lanes and the repo's hazards are in
`.claude/rules/api-docker-rules.md`.
| Task | Agent |
|---|---|
| What the daemon does or expects â endpoints, wire formats, filters, registry auth | `api-docker-engine-worker` |
| The Perl side â Moo, transport internals, entities, refactoring, cpanfile | `api-docker-worker` (default) |
| Write/extend tests, add fixtures | `api-docker-test-writer` |
| The generated type model â `API::Docker::Type::*`, the DSL, the drift checker, `spec/` | `api-docker-type-writer` |
| Pre-release audit | `api-docker-release-checker` |
| POD and README | `api-docker-doc-writer` |
The two workers split by question, not by file. Only `api-docker-engine-worker`
is briefed with `docker-engine-api`, the shared Engine API reference.
`api-docker-type-writer` is briefed with `api-docker-type-model`, which carries the
(`start`, `stop`, `logs`, ...) keep their signatures and move to
`API::Docker::Role::Entity::*`, composed onto the generated classes; the
old names (`API::Docker::Container`, `::Image`, `::Network`, `::Volume`,
`::Plugin`, `::Secret`, `::Config`) ship as stubs that croak, naming what
replaces them.
- New streaming options `on_event`, `on_frame` and `on_chunk` on the HTTP
verbs, wired into `system->events`, `containers->logs`/`stats`/`attach`,
`exec->start`, the `images` build/pull/push/load/get family and the
`plugins` install/upgrade/push. A callback receives each event, frame or
chunk as it arrives; `$stop->()` ends the stream early. Without one the
unbounded endpoints still block.
- New `read_timeout` and `connect_timeout`, as client attributes and
per-request options, off by default. `read_timeout` is idle time since
the last byte; both croak `API::Docker::Error::Timeout`, which carries
whatever already arrived. A new `TIMEOUTS` section in `API::Docker`
documents what each bounds. `containers->stats` is deliberately not
bounded by `read_timeout` -- its stream keeps producing rather than
going idle.
- New `API::Docker::Role::Using`: `$docker->containers->using(read_timeout
=> 5)->list` clones a resource class to bound a run of calls. Every
request the run makes, version negotiation included, carries the bound;
instead of silently matching nothing.
- JSON request bodies send booleans as real `true`/`false`; a caller may
pass `1`/`0` or a JSON boolean interchangeably.
- `_uri_encode` UTF-8-encodes a decoded character string before
percent-escaping, so a name or tag typed as characters (`ü`, `ä¸`) goes
out as valid UTF-8.
- A request path outside the RFC 3986 origin-form character set is refused
before it reaches the daemon, closing a request-line injection through a
container name or image reference.
- An ArrayRef query parameter expands into one repeated `k=v` pair per
element (`names=a&names=b`), which some endpoints require.
- A bare JSON scalar body (`null`, `true`, a number, a quoted string) is
decoded rather than handed back as raw bytes; `raw` and `ndjson` return
`''` and `[]` for a zero-byte body instead of `undef`.
- Registry credentials reach `images->pull` (`auth`, sent as
`X-Registry-Auth`) and `images->build` (`registry_config`, sent as
`X-Registry-Config`), sent only when given. An already-base64 auth value
in the standard alphabet is respelled URL-safe, which the engine
requires.
- `images->pull` no longer appends a default `tag` onto a reference that
already carries a `:tag` or `@digest`.
- New `images->get`, `->get_all` and `->load`: the image tar roundtrip in
and out of a daemon without a registry. New `images->commit`
(POST /commit) and `images->build_prune` (POST /build/prune, the
BuildKit cache, a different store from the dangling images
`images->prune` deletes).
- New container endpoints: `get_archive`, `put_archive`, `stat_archive`
(the `docker cp` primitives), `changes`, `export`, `resize` and the
one-way half of `attach`. `attach` defaults to `stream => 0, logs => 1`
(replay and return) and refuses a container that is not running unless
`require_running => 0`.
- `containers->start`/`stop`/`restart`/`pause`/`unpause` return 1 when the
call changed the container's state and 0 when it was already in it (the
engine answers a no-op with 304), instead of always undef.
- `containers->stats` croaks `API::Docker::Error::HTTP` when Podman reports
a failure inside a 200 response, instead of handing the error object back
as a reading.
>= 400 path looks for `message`. So on Podman the new check fires
for `build` and the pre-existing status check catches the other two.
All three are loud either way, but catching
`API::Docker::Error::Stream` specifically is not a reliable way to
catch a failed pull or push -- inspect $@ as a string, which both
routes satisfy. The POD on each method says which engine does what.
`system->events` is explicitly exempt and never croaks on stream
content: it is a feed, so an object in it records something that
happened on the engine rather than the outcome of this call. The
check is on by default for the transport's `ndjson` option and
exempting an endpoint is deliberate (`croak_on_error => 0`), because
the operation-shaped streaming endpoints are open-ended while the
feed-shaped ones are `/events` and nothing else.
- `tls => 1` now croaks with "not implemented" instead of being
accepted and ignored. `tls` and `cert_path` were attributes no code
read: `API::Docker::Role::HTTP` builds a plain IO::Socket::INET and
speaks HTTP over it, so a `tcp://` daemon was always addressed in
cleartext and a caller who asked for TLS got an unencrypted
connection with no indication of it -- anyone passing the option was
by definition sending credentials in the clear while believing
otherwise. TLS is still not implemented; the croak names the reason
and the way round it, which is to terminate TLS in front of the
`exec->start` also gained POD saying where the exit status actually
comes from -- `exec->inspect($id)->{ExitCode}`, a separate call --
which the method's documentation never mentioned.
- `images->build`, `->pull` and `->push` now always return an ArrayRef
of events. `_request` used to try `decode_json` on the whole body
first and only fall back to line-by-line parsing, so a stream that
carried exactly one JSON object came back as a HashRef while a
multi-event stream came back as an ArrayRef, and every caller had to
check `ref` before iterating. Measured on Podman: `POST /build?q=1`
emits exactly one object, which is the case that used to change
shape. The ordinary single-JSON-object endpoints (`/version`,
`/containers/{id}/json`, ...) are untouched and still return a
HashRef -- the streaming behaviour is now requested explicitly with
the new `ndjson => 1` transport option rather than guessed from the
body. The option is named for the format and not `stream`, which is
already a query parameter of `/events` and
`/containers/{id}/stats`.
`system->events` takes the same option. It was reaching an ArrayRef
only through the implicit fallback that has now gone, so without it
the endpoint would have quietly started returning an undecoded
string. Measured on Podman for one container create/init/start/
died/remove cycle: five newline-delimited objects, and the body is
not valid JSON as a whole. Its POD now also says to always pass
`until`, since the transport buffers the whole response and an
unbounded event stream therefore never returns.
Note for anyone scanning these events: a failed build is still HTTP
200 with the failure carried as an `errorDetail` object inside the
stream, confirmed on Podman for a Dockerfile whose `RUN` exits 7.
A failed *pull* differs there -- Podman answers 404 with a plain
`{"message":...}` body where Docker streams `errorDetail` on a 200 --
lib/API/Docker/Type/Topology.pm
lib/API/Docker/Type/Volume.pm
lib/API/Docker/Type/Volume/UsageData.pm
lib/API/Docker/Type/VolumeCreateOptions.pm
lib/API/Docker/Type/VolumeListResponse.pm
lib/API/Docker/Volume.pm
t/author-pod-syntax.t
t/basic.t
t/connect_timeout.t
t/containers.t
t/containers_endpoints.t
t/containers_stats_error.t
t/dist_source.t
t/distribution.t
t/entity_container.t
t/entity_roles.t
t/exec.t
t/filters.t
t/fixtures/container_inspect.json
t/fixtures/containers_archive.tar
t/fixtures/containers_list.json
lib/API/Docker.pm view on Meta::CPAN
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}) {
lib/API/Docker.pm view on Meta::CPAN
=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>
lib/API/Docker.pm view on Meta::CPAN
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
lib/API/Docker.pm view on Meta::CPAN
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
lib/API/Docker/API/Configs.pm view on Meta::CPAN
C<GET /configs> with an array of the C<Config> definition and
C<GET /configs/{id}> with that same definition. Field names are the
swagger's own spelling in snake_case -- C<ID> is C<< ->id >>, C<CreatedAt> is
C<< ->created_at >> -- and the nested ones are generated classes rather than
the raw HashRefs the old entity kept: C<< $config->spec >> is an
L<API::Docker::Type::ConfigSpec> and C<< $config->version >> an
L<API::Docker::Type::ObjectVersion>, whose C<< ->index >> is what
C<version_index> reaches.
Configs are L<API::Docker::API::Secrets> without the secrecy: the same five
endpoints, the same spec shape, the same mandatory C<version> on update. The
one behavioural difference is that a config's value can be read back --
L</inspect> returns it in C<< $config->spec->data >>, and
L<API::Docker::Role::Entity::Config/decoded_data> decodes it, where a secret
returns no payload at all. Which is the whole point of the split: put configuration in
a config, and anything you would mind seeing in a C<docker config inspect> in
a secret.
=head2 Data is raw bytes on the way out, base64 on the way back
The wire field C<Data> carries base64. B<L</create> and L</update> encode it
lib/API/Docker/API/Configs.pm view on Meta::CPAN
spec goes back with the one key edited. Note that a spec from C<inspect>
carries C<Data> already base64-encoded, so passing it straight to L</update>
would encode it a second time; delete the key, or pass
L<API::Docker::Role::Entity::Config/decoded_data> in its place, before
sending it back.
=head2 Swarm, and Podman
The Engine API groups C</configs> with Swarm. A Docker daemon that is not a
swarm manager answers B<503> C<"This node is not a swarm manager."> to all of
these endpoints, which this client turns into a croak. That is documented
engine behaviour, not a fault at this end -- the daemon needs
C<docker swarm init>, or a manager to talk to, and a single-node install that
has never run it is the ordinary case, not an edge one.
B<Podman does not serve C</configs>,> though what it answers for "not served"
differs by path. Measured against the rootless socket on Podman 5.8.4 (API
1.44): C<GET /configs> -- the collection listing -- still answers B<404> with
the plain-text body C<Not Found>, not a JSON error, so the croak from this
client reads C<Docker API error (404): Not Found>. Every other path under it
-- C<GET /configs/{id}>, C<POST /configs/create>, C<DELETE /configs/{id}> and
C<POST /configs/{id}/update> -- answers B<503> instead, with a JSON body
naming the route it refuses, e.g. C<< {"cause":"Podman does not support
service: /v1.44/configs/xyz","message":"...","response":503} >>.
An earlier pass measured every path here as a flat 404 against Podman 5.4.2
(API 1.41). That measurement is not reproducible on this machine any more --
5.4.2 is gone from it -- so whether 5.8.4 actually changed this or the
original pass only ever exercised the collection endpoint is not something
this distribution can decide from here; it is recorded as what 5.8.4 answers,
not as a change from 5.4.2. Either way, the split this section used to draw
between the two engines -- Docker's 503 "not a swarm manager" against a flat
Podman 404 -- no longer holds cleanly: most C</configs> paths on Podman answer
503 too now, just with a different body and for a different reason.
There is no Podman-side equivalent to fall back on and no socket setting that
enables it. This remains the one class in the distribution that cannot be
exercised for real against the engine the rest of it is tested on --
L<API::Docker::API::Secrets>, the same five endpoints, is served by Podman
from its own local secret store.
=head2 client
Reference to L<API::Docker> client. Weak reference to avoid circular dependencies.
=head2 list
my $configs = $configs->list;
my $configs = $configs->list(filters => { label => ['app=web'] });
lib/API/Docker/API/Containers.pm view on Meta::CPAN
sub _wrap {
my ($self, $class, $data) = @_;
return $class->from_data($data, client => $self->client);
}
sub _wrap_list {
my ($self, $class, $list) = @_;
return [ map { $self->_wrap($class, $_) } @$list ];
}
# A state-change endpoint answers 204 when it changed something and 304 when
# the container was already in the state asked for. Neither carries a body, so
# the return value of the request is undef either way and the two are
# indistinguishable from it. The status comes out through the `response`
# out-parameter (see API::Docker::Role::HTTP/"Reading the status line and the
# response headers") and becomes the documented 1/0.
sub _state_change {
my ($self, $path, %opts) = @_;
my %response;
$self->client->post($path, undef, %opts, response => \%response);
return 0 if defined $response{status} && $response{status} == 304;
lib/API/Docker/API/Containers.pm view on Meta::CPAN
# The Podman compatibility path, and deliberately only that. Podman answers
# GET /containers/{id}/stats for a container that is not running with an error
# object inside a response it has already committed to 200 --
# {"cause":"container is stopped","message":"container is stopped",
# "response":500}, chunked, for the one-shot call and for stream => 1 alike
# (measured on Podman 5.4.2, API 1.41). Neither guard in the transport sees
# it: the >= 400 croak reads the status line, which says 200, and the stream
# check triggers on errorDetail, which this object does not carry. Docker
# 29.7.2 (API 1.55) answers the same call with a real, zero-filled reading and
# never produces this shape at all -- so this belongs here, next to the one
# endpoint and the one engine it was measured on, and not in Role::HTTP, where
# it would be a heuristic on daemon prose sitting under all twelve modules.
#
# All four clauses have to hold. The narrowness is the point, not an accident:
#
# 1. the status was 2xx. True by construction on both paths this guards:
# _request croaks before returning for >= 400, and
# _read_streaming_response reads such a body whole rather than handing it
# to a callback, so nothing that failed the status line reaches here
# 2. the decoded value is a HashRef
# 3. it carries all three of cause, message and response, exactly
# lower-cased. Never case-insensitively, and this is the counter-example
# that fixes it: POST /containers/{id}/wait answers its SUCCESS case with
# a top-level `Error` key -- Podman sends "Error":null on every wait --
# so a rule matching /error/i would turn every successful wait into a
# failure. Measured over fifteen read endpoints per engine and every
# fixture in t/fixtures: no 2xx body on either engine carries even one of
# these three lower-cased at the top level
# 4. `response` is a non-ref scalar reading as an integer >= 400. That is
# what makes the rule self-evidencing rather than a guess about prose:
# the object is an error because Podman says so inside it. Known miss:
# Podman's GET /plugins answers {"cause":"","message":"Path ... is not
# supported","response":0}, which clause 4 rejects -- but it arrives with
# 404 on the status line and the transport croaks it long before this
# runs, so the miss goes in the conservative direction and costs nothing
#
lib/API/Docker/API/Containers.pm view on Meta::CPAN
# API::Docker::Error::HTTP rather than ::Stream: the one-shot call is not a
# stream at all, so "Docker API stream error" would be the wrong sentence for
# it and ->events would be a fabricated list. What the caller wants instead is
# exactly what this class carries -- ->status for the code Podman named, and
# ->data for the object, whose `cause` key that attribute's own POD already
# points at. Two of its attributes are left at their defaults on this path, on
# purpose: ->reason, because the status line's reason phrase was "OK" and
# putting that on a 500 would mislead, and ->body, because the bytes were
# decoded by the transport before this check ever saw them.
sub _assert_no_podman_error {
my ($self, $endpoint, $value) = @_;
my $error = $self->_podman_error_object($value) or return $value;
my $reason = $error->{message};
$reason = $error->{cause} unless defined $reason && length $reason;
$reason = 'no message given' unless defined $reason && length $reason;
# Carp appends no location to a message that already ends in a newline.
$reason =~ s/\s+\z//;
# The object goes into a variable first: `croak CLASS->new(...)` is indirect
# object syntax and parses as CLASS->croak(new(...)). Carp hands a reference
# straight back rather than decorating it, so the location is captured by
# hand, naming the frame a croak of a plain string would have named.
my $err = API::Docker::Error::HTTP->new(
message => 'Docker API error (' . $error->{response} . '): ' . $reason
. ' -- reported inside a 200 response to ' . $endpoint,
location => shortmess(''),
status => $error->{response},
data => $error,
);
croak $err;
}
sub stats {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my $stream = $opts{stream} ? 1 : 0;
my %params = ( stream => $stream );
# one-shot asks the engine not to wait for a second sampling cycle, which
# only means anything to a single reading. It is sent for the one-shot call
# alone, the way it always was, and never beside stream => 1.
$params{'one-shot'} = 1 unless $stream;
my $endpoint = 'GET /containers/' . $id . '/stats';
# The guard has to sit on both sides of the callback split, because the same
# body arrives either way: buffered it is the return value, streamed it goes
# to the callback and is never returned at all. Wrapping puts the check in
# front of the caller's callback, so no caller is handed the error object as
# though it were a reading. Only a CodeRef is wrapped -- anything else is
# passed through untouched, so the transport still raises its own "on_event
# option must be a CodeRef" instead of this method dying on a closure it
# built around a non-callback.
my $on_event = $opts{on_event};
if (exists $opts{on_event} && ref $on_event eq 'CODE') {
my $cb = $on_event;
$on_event = sub {
my ($reading, $stop) = @_;
$self->_assert_no_podman_error($endpoint, $reading);
return $cb->($reading, $stop);
};
}
my $result = $self->client->get("/containers/$id/stats",
params => \%params,
%{ $self->_request_options },
exists $opts{on_event} ? ( on_event => $on_event )
: $stream ? ( ndjson => 1 )
: (),
);
# A HashRef for the one-shot call and an ArrayRef of readings for
# stream => 1 without a callback -- both are the buffered body and both can
# be that error object. With a callback the return value is the summary
# HashRef, which carries none of the three keys and passes untouched.
$self->_assert_no_podman_error($endpoint, $_)
for ref $result eq 'ARRAY' ? @$result : ($result);
return $result;
}
sub changes {
my ($self, $id) = @_;
croak "Container ID required" unless $id;
my $result = $self->client->get("/containers/$id/changes",
lib/API/Docker/API/Containers.pm view on Meta::CPAN
=head2 restart
$containers->restart($id, timeout => 10);
Restart a container. Optionally specify C<timeout> in seconds.
Reports 1/0 like L</start>, but a restart has no no-op state to report: the
engine restarts a stopped container as readily as a running one. Measured
against Podman 5.4.2 (API 1.41) it answers 204 in both cases, and the Docker
Engine API documents no 304 for this endpoint either, so 0 is not expected
here. The value is reported the same way rather than specially, so an engine
that does answer 304 is not silently read as a change.
=head2 kill
$containers->kill($id, signal => 'SIGKILL');
$containers->kill($id, signal => 'SIGUSR1'); # not necessarily a stop
Send a signal to a container. Default signal is C<SIGKILL>.
lib/API/Docker/API/Containers.pm view on Meta::CPAN
that L</unpause> succeeds
=back
So C<kill> is a state change for a paused container on Docker and is not one
on Podman -- with the same 204 on both.
Killing a container that is not running -- stopped, exited or just created --
croaks B<409>; it does not return a falsy value. An unknown container ID
croaks B<404>. Neither is reachable through a return value, and no case
answers 304: moby's swagger for this endpoint (C<operationId: ContainerKill>)
documents only 204, 404, 409 and 500.
The B<text> of either is engine prose, so branch on
L<API::Docker::Error::HTTP/status> and not on the message. For one and the
same stopped container:
=over
=item * Docker -- C<cannot kill container: E<lt>nameE<gt>: container E<lt>idE<gt> is
not running>
=item * Podman -- C<can only kill running containers. E<lt>idE<gt> is in state
exited: container state improper>. C<container state improper> is Podman's
separate C<cause> field, reachable as C<< $err->data->{cause} >>, not a
phrase Docker uses anywhere
=back
The 404 differs too, and on Docker it differs I<per endpoint>: C<kill>
against a missing ID answers C<cannot kill container: E<lt>nameE<gt>: No such
container: E<lt>nameE<gt>> where L</inspect> answers the bare C<No such container:
E<lt>nameE<gt>>. Podman sends one sentence for both.
Options:
=over
=item * C<signal> - Signal to send (default C<SIGKILL>)
lib/API/Docker/API/Containers.pm view on Meta::CPAN
seconds later -- closes cleanly after 3 s
=back
B<Docker does exactly the same, and that is measured now too.> Against Docker
29.7.2 (API 1.55): C<?logs=1&stdout=1&stderr=1&stream=1> on an exited
container was still open when a 10 s probe gave up, and
C<?logs=1&stdout=1&stderr=1&stream=0> answered 200 with byte-identical frames
and closed in half a millisecond. So the hang is not a Podman quirk to be
worked around -- it is what both engines do with a subscription whose only
terminator is already in the past, on an endpoint whose reference promises a
close in neither direction. It is unspecified behavior on both, which is the
case for the C<< stream => 0 >> default rather than an argument against it.
One more measured difference: Podman refuses C<< stream => 0 >> together with
C<< logs => 0 >> outright, with B<400> C<at least one of Logs or Stream must
be set>, rather than answering an empty 200.
Options:
=over
lib/API/Docker/API/Containers.pm view on Meta::CPAN
B<This method now croaks on it>, with an L<API::Docker::Error::HTTP> carrying
C<< ->status == 500 >> -- the code Podman named in C<response> -- and the
object itself as C<< ->data >>, so C<< $err->data->{cause} >> stays readable.
That is a B<change>: up to and including the previous release the one-shot
call returned this HashRef where a reading was expected, and an C<on_event>
callback was handed it as though it were one. The check runs on the buffered
return value and in front of the callback alike, and only on that exact
shape -- a HashRef inside a 2xx carrying all three of C<cause>, C<message>
and C<response> lower-cased, with C<response> reading as an integer >= 400.
Nothing else in this distribution, in its fixtures, or in a sweep of fifteen
read endpoints per engine carries even one of those keys lower-cased at the
top level. The rule is deliberately B<not> case-insensitive: a I<successful>
L</wait> answers with a top-level C<Error> key, and a looser rule would
report every one of those as a failure.
B<Docker sends a structurally valid reading with everything zeroed> -- 200,
about 825 bytes, no error anywhere in it:
{ id => '...', name => '/...', os_type => 'linux',
read => '0001-01-01T00:00:00Z',
cpu_stats => { cpu_usage => { total_usage => 0, ... }, ... },
lib/API/Docker/API/Containers.pm view on Meta::CPAN
B<running> on Docker 29.7.2 carries C<< num_procs => 0 >> as well. It is zero
on that engine either way and separates nothing.
The engine-independent question is not about the reading at all: ask
L</inspect> whether the container is running. Treat the zero timestamp as a
cheap Docker-specific second opinion, not as the test.
=head2 C<< stream => 1 >> on a container that is not running
The same call in follow mode fails in B<opposite> directions on the two
engines, and the standing advice for a streaming endpoint -- bound the
window -- does not help, because there is no window to bound:
=over
=item * B<Podman> sends the one error object above and closes at once. Without
a callback that arrives as a one-element ArrayRef; either way this method
croaks it
=item * B<Docker> streams one zero-filled reading B<per second, forever>.
Measured: 5 objects in a 5 s probe, 12 in a 12 s probe, the connection never
lib/API/Docker/API/Containers.pm view on Meta::CPAN
It degrades. One 20 s probe against a container that exited after 3 s:
3 real readings, then 13 zero-filled ones, connection still open at 20 s
Podman ends the same stream on the container's exit -- 5.0 s for the same
probe, the last object a whole reading.
So on Docker C<< stream => 1 >> has B<no> terminator tied to the container at
all, and the readings turn to zeros without anything in the stream saying so.
A caller that follows a container's stats until it stops is asking for
something this endpoint does not offer on that engine: give C<on_event> its
own stopping condition -- a reading count, a deadline, or the Go zero time in
C<read> -- and do not wait for the stream to end on its own.
B<C<read_timeout> does not bound this.> It is an idle timeout -- silence since
the last byte -- and this stream is never silent: it keeps producing a
zero-filled reading once a second, indefinitely, so the clock that C<read_timeout>
measures never runs out. C<read_timeout> bounds a daemon that goes quiet; a
Docker stats stream after container exit does the opposite -- it keeps talking,
just not truthfully. C<on_event> still has to notice the Go zero time in
C<read> and call C<< $stop->() >> itself; do not rely on C<read_timeout> to end
lib/API/Docker/API/Containers.pm view on Meta::CPAN
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 } ]
C<Kind> is an integer, not a word, and the engine documents no names for the
three values:
=over
lib/API/Docker/API/Containers.pm view on Meta::CPAN
=item * C<1> - B<added>. The path exists only in the container
=item * C<2> - B<deleted>. The path existed in the image and is gone
=back
A container with nothing changed comes back as an empty ArrayRef; the engine
answers that case with a JSON C<null> rather than an empty list.
Measured against Podman 5.4.2 (API 1.41): the endpoint is served, but an
unknown container is answered with B<500> and
C<< {"cause":"layer not known","message":"<id> not found: layer not known"} >>
rather than the 404 every other container endpoint gives -- so a caller
distinguishing "no such container" from a real failure cannot do it on the
status code alone on that engine.
=head2 export
use Path::Tiny;
path('container.tar')->spew_raw($containers->export($id));
Export the container's whole filesystem as a tar archive -- the endpoint
behind C<docker export>. Returns the raw archive bytes, never decoded and
never modified.
The archive is buffered whole in memory, so this costs the size of the
container's filesystem in RAM. There is no streaming variant here.
Unlike L<API::Docker::API::Images/get>, the result is a plain filesystem tar:
no C<manifest.json>, no layers, no image metadata. L<API::Docker::API::Images/load>
will not take it back -- importing a flat filesystem is
C<< POST /images/create?fromSrc=- >>, which this distribution does not expose.
lib/API/Docker/API/Containers.pm view on Meta::CPAN
false on both. Ask C<defined>, never C<exists>, and take C<StatusCode> as the
answer.
A B<non-null> C<Error> was not produced on either engine by any probe behind
this documentation. The Engine API reference documents it as an object
carrying C<Message>; that shape is B<documented but not measured here>, which
is not the same as unreachable -- do not write code that assumes it cannot
appear, and do not trust its shape without checking.
The call blocks in the client for as long as the engine takes to answer: this
endpoint answers only once the condition is met, and the whole response is
read before anything is parsed. There is no timeout, on this method or in the
transport.
Measured on both engines:
=over
=item * A container that has B<already exited> answers immediately with its
real exit status -- with no condition and with C<not-running> alike
lib/API/Docker/API/Containers.pm view on Meta::CPAN
condition: "...">, Podman C<failed to parse query parameter 'condition' ...>),
and an unknown container ID croaks B<404>
=back
Podman also answers C<< StatusCode => -1 >> for a container whose exit status
L</attach> has destroyed -- see
L</"On Podman this destroys a stopped container's exit status">. The value is
that engine's sentinel for "no status", not an exit code.
This endpoint is also the reason the error check on L</stats> matches
C<cause>, C<message> and C<response> case-sensitively: a I<successful> wait
is a 2xx body with a top-level C<Error> key in it, and a rule matching
C<error> case-insensitively would turn every one of them into a failure.
Options:
=over
=item * C<condition> - What to wait for: C<not-running> (the engine's own
default), C<next-exit> or C<removed>. Sent only when given
lib/API/Docker/API/Containers.pm view on Meta::CPAN
=head2 pause
$containers->pause($id);
Pause all processes in a container.
Reports 1/0 like L</start>, but pausing an already-paused container is an
error rather than a 304: measured against Podman 5.4.2 (API 1.41) it answers
C<500> with C<< "..." is already paused: container state improper >>, which
croaks. The Docker Engine API documents no 304 for this endpoint either. So
this method returns 1 or croaks in practice.
=head2 unpause
$containers->unpause($id);
Unpause all processes in a container. Reports 1/0 like L</start>; as with
L</pause>, the no-op is an error and not a 304 -- Podman 5.4.2 answers
unpausing a running container with C<500>.
lib/API/Docker/API/Containers.pm view on Meta::CPAN
path => '/opt/app');
Write a tar archive into a path inside the container -- the inbound half of
C<docker cp>. The archive is the request body; pass it as raw bytes or as a
scalar reference to them, the way L<API::Docker::API::Images/load> takes its
archive. Returns nothing: the engine answers a success with an empty body.
C<path> must name a B<directory that already exists> in the container; the
archive's members are unpacked into it. Writing a single file means putting
that file in a one-member archive and naming its parent directory as C<path> --
there is no "write these bytes to this filename" form of this endpoint.
The archive is sent as one buffered request body, so this costs its full size
in RAM.
Options:
=over
=item * C<path> - Directory inside the container to unpack into. Required
lib/API/Docker/API/Containers.pm view on Meta::CPAN
=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:
=over
=item * C<name> - The path's basename. For a symlink the two engines
lib/API/Docker/API/Distribution.pm view on Meta::CPAN
},
);
# The same question as a predicate: is that tag already published?
if ($docker->distribution->exists('myrepo/app:1.0', auth => $auth)) {
die "refusing to overwrite a released tag";
}
=head1 DESCRIPTION
This module provides access to the Docker distribution endpoint
(C<GET /distribution/{name}/json>), which asks a I<registry> for the manifest
descriptor of an image reference without pulling the image.
Accessed via C<< $docker->distribution >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->distribution->using(read_timeout => 5) >>.
The reference goes into the path unescaped, so its slashes and its tag stay
readable on the wire (C</distribution/myrepo/app:1.0/json>) -- that is what
the engine parses, and percent-encoding them breaks the reference.
=head2 A 404 means two different things
The endpoint answers 404 both when the registry does not have the reference
and when the engine has no such route, and the two want opposite handling.
The split here is:
=over
=item * L</inspect> is the endpoint, and croaks on B<any> error status, 404
included, the way every other method in this distribution does.
=item * L</exists> is the question, and answers it: true, false, or a croak
when the engine could not ask the registry at all.
=back
L</exists> exists because answering "no" to everything is the failure this
class was added to remove -- see the Podman note below -- and a predicate
that cannot fail loudly would have reintroduced it one layer up.
=head2 Not available on Podman
Measured against the rootless Podman socket (5.4.2, API 1.41):
C<< GET /v1.41/distribution/nginx:latest/json >> answers C<404 Not Found>
with
C<< {"cause":"","message":"Path /v1.41/distribution/nginx:latest/json is not supported","response":0} >>
(the C<1.41> there is this client's negotiated API version, echoed back from
the request path -- it moves with negotiation, not a fixed string),
and so does every other reference, escaped or not -- the compat layer has no
route for this endpoint. This class therefore needs a real Docker daemon.
That 404 is exactly the one a naive predicate would read as "the registry
does not have it", which is why L</exists> tells the engine's own
no-such-route answer apart and croaks on it instead.
=head2 What this class returns
L</inspect> returns the decoded engine response -- a HashRef with
C<Descriptor> and C<Platforms> -- not an entity object, deviating from the
C<inspect> convention the other resource classes follow, because there is no
lib/API/Docker/API/Distribution.pm view on Meta::CPAN
my $descriptor = $distribution->inspect('nginx:latest');
my $descriptor = $distribution->inspect('private/app:1.0', auth => $auth);
Ask the registry for the manifest descriptor of an image reference. The
daemon performs the lookup; nothing is pulled and no local image is touched.
Returns a HashRef with C<Descriptor> -- C<MediaType>, C<digest>, C<size>,
C<URLs> -- and C<Platforms>, the list of C<{ Architecture, OS, ... }> the
reference resolves to.
B<A missing reference croaks.> This method is the endpoint, so it inherits
the transport's rule that any status at or above 400 is an error, and the
registry's "no such reference" is a 404 like any other. Use L</exists> for
the predicate, or eval and read the status:
my %res;
my $d = eval { $distribution->inspect($ref, response => \%res) };
# $res{status} == 404 here means the registry said no *or* the engine
# has no such route -- see L</exists>, which separates the two.
Options:
lib/API/Docker/API/Images.pm view on Meta::CPAN
normalised by L<API::Docker::Role::Filters>
=back
=head2 get
use Path::Tiny;
my $tar = $images->get('alpine:3');
path('alpine.tar')->spew_raw($tar);
Export one image, and the history behind it, as a tar archive -- the endpoint
behind C<docker image save>. Together with L</load> it is the only way in or
out of a daemon that does not go through a registry.
B<The return value is raw bytes, not a decoded structure.> The engine answers
with the tar stream itself, and the transport is told to hand it back
untouched (C<< raw => 1 >>), so what arrives is byte for byte what the daemon
wrote. Write it with a binary-safe file handle -- C<< path(...)->spew_raw >>,
or C<binmode> on a handle of your own. Treating it as text corrupts it, and
nothing about the value announces that it is binary.
lib/API/Docker/API/Images.pm view on Meta::CPAN
=head2 load
use Path::Tiny;
my $events = $images->load(path('alpine.tar')->slurp_raw);
for my $event (@$events) {
print $event->{stream} if defined $event->{stream};
}
Import a tar archive produced by L</get> or L</get_all> -- the endpoint behind
C<docker image load>. The archive is the request body; pass it as raw bytes or
as a scalar reference to them, the way L</build> takes its context.
Returns an ArrayRef of progress events, one per object in the engine's
newline-delimited JSON stream, even when the stream carried a single object.
The last of them names what was imported:
my ($loaded) = grep { ($_->{stream} // '') =~ /^Loaded image/ } @$events;
Options:
lib/API/Docker/API/Images.pm view on Meta::CPAN
Clear the BuildKit build cache. B<This is not L</prune>>, and the two are not
interchangeable: L</prune> deletes unused I<images>, this deletes the
intermediate I<build cache> that L</build> writes. Neither touches the other's
storage, and on a machine that builds often the build cache is usually the
larger of the two.
Returns the raw daemon response, a HashRef with C<CachesDeleted> and
C<SpaceReclaimed>.
B<Podman does not implement this endpoint.> Measured against 5.4.2 (API 1.41):
C<POST /build/prune> answers C<404 Not Found> with a C<text/plain> body of
C<Not Found> -- not the JSON C<< {"message":...} >> shape its other errors use
-- at every version prefix tried, and there is no C<libpod> equivalent either.
The transport croaks with C<Docker API error (404): Not Found>, the plain body
verbatim, because it is not JSON to unwrap. A caller that must work on both
engines has to treat that 404 as "no build cache to clear here" rather than as
a transport fault.
Options:
lib/API/Docker/API/Images.pm view on Meta::CPAN
=item * L<API::Docker> - Main Docker client
=item * L<API::Docker::Role::Entity::Image> - the convenience methods the
returned objects carry
=item * L<API::Docker::Type::ImageSummary> - the fields C<list> returns
=item * L<API::Docker::Type::ImageInspect> - the fields C<inspect> returns
=item * L<API::Docker::Role::RegistryAuth> - the C<X-Registry-Auth>
encoding C<push> uses, shared with the other registry-facing endpoints
=item * L<API::Docker::Error::Stream> - Raised by C<build>, C<pull>, C<push>
and C<load>
=back
=head1 SUPPORT
=head2 Issues
lib/API/Docker/API/Plugins.pm view on Meta::CPAN
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
(C</plugins>).
Accessed via C<< $docker->plugins >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->plugins->using(read_timeout => 5) >>.
=head2 Installing is two calls, and the engine enforces it
C<< POST /plugins/pull >> takes the list of privileges the plugin demands
B<in its request body>, and the daemon compares that list against the one it
lib/API/Docker/API/Plugins.pm view on Meta::CPAN
C<network: host> would fail with an error naming neither. Passing
C<< accept_privileges => 1 >> makes C<install> perform the first call itself
and hand the answer straight back -- a blanket grant, spelled out at the call
site so it is greppable.
The same applies to L</upgrade>, which takes the same body.
=head2 Not available on Podman
Measured against the rootless Podman socket (5.4.2, API 1.41): B<none> of the
C</plugins> endpoints exist there. C<< GET /v1.41/plugins >> answers
C<404 Not Found> with
C<< {"cause":"","message":"Path /v1.41/plugins is not supported","response":0} >>
(the C<1.41> there is this client's negotiated API version, echoed back from
the request path -- it moves with negotiation, not a fixed string in the
daemon's error text),
and every other path in this family -- C</plugins/privileges>,
C</plugins/pull>, C</plugins/{name}/json>, C</plugins/{name}/enable> and the
rest -- answers a bare C<404 Not Found> as C<text/plain>, meaning the compat
layer has no route registered for them at all. Managed plugins are a Docker
feature; Podman's own plugin model is not served here. Everything in this
lib/API/Docker/API/Plugins.pm view on Meta::CPAN
C<docker.io/vieux/sshfs:latest> name the same plugin; C<:latest> is the
default when no tag is given.
Options:
=over
=item * C<auth> - Registry credentials for a plugin in a private registry;
HashRef of C<username> / C<password> / C<serveraddress> / C<identitytoken>,
or a pre-encoded base64 string. Sent as C<X-Registry-Auth>. The Engine API
reference does not document this header on this endpoint, but the daemon
reads it here exactly as it does on the pull
=back
=head2 install
my $privileges = $plugins->privileges('vieux/sshfs:latest');
$plugins->install('vieux/sshfs:latest', privileges => $privileges);
# blanket grant, in one call
lib/API/Docker/API/Plugins.pm view on Meta::CPAN
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.
=head2 remove
$plugins->remove('vieux/sshfs:latest');
$plugins->remove('vieux/sshfs:latest', force => 1);
Remove an installed plugin. A plugin that is still enabled is refused unless
C<force> is set.
lib/API/Docker/API/Plugins.pm view on Meta::CPAN
=item * C<timeout> - Seconds to wait for the plugin to come up, C<0> for no
timeout (the default)
=back
C<timeout> is B<always> sent, whether or not the caller passes it. The Engine
API reference gives it a default of C<0>, but the daemon has none: it reads
the raw query value and parses it with Go's C<strconv.Atoi>, so an absent
parameter is parsed as the empty string and the request fails with
C<strconv.Atoi: parsing "": invalid syntax> as an invalid-parameter error.
This is the one endpoint in the family where omitting an optional parameter
is fatal.
=head2 disable
$plugins->disable('vieux/sshfs:latest');
$plugins->disable('vieux/sshfs:latest', force => 1);
Disable an enabled plugin. Returns C<undef>.
Options:
lib/API/Docker/API/Plugins.pm view on Meta::CPAN
=item * C<on_event> - CodeRef called with each progress event as it arrives --
layer by layer, rather than the whole upload in one silence. The return value
is then the summary HashRef; see L</"Progress as it arrives">
=back
Unlike L<API::Docker::API::Images/push>, which sends C<X-Registry-Auth> on
every call because the engine rejects an image push without it, this sends
the header only when C<auth> is given: the plugin router decodes the header
and discards a decoding failure, so an anonymous push needs no header. The
Engine API reference documents no header on this endpoint at all; the daemon
reads it.
Returns an ArrayRef of progress events, C<[]> when the engine sent no
progress. Failure is reported by the same two routes as L</install>.
C<push> shadows the Perl builtin inside this package, which is why
L<namespace::clean> is loaded. Always call it as a method.
=head2 configure
lib/API/Docker/API/Plugins.pm view on Meta::CPAN
=item * L<API::Docker::Role::Entity::Plugin> - the convenience methods the
returned objects carry
=item * L<API::Docker::Type::Plugin> - the fields L</list> and L</inspect>
return
=item * L<API::Docker> - Main Docker client
=item * L<API::Docker::Role::RegistryAuth> - the C<X-Registry-Auth>
encoding used here, shared with the other registry-facing endpoints
=item * L<API::Docker::API::Images> - Image endpoints, whose C<push>
sends that header on every call rather than only when credentials were
given
=item * L<API::Docker::Error::Stream> - Raised for a failure reported inside
a 200 event stream by L</install>, L</upgrade> and L</push>
=back
=head1 SUPPORT
lib/API/Docker/API/Secrets.pm view on Meta::CPAN
other field of the spec must be sent back unchanged from what C<inspect>
returned. Hence the C<< %spec = %{ $secret->spec->TO_JSON } >> in the
SYNOPSIS -- C<TO_JSON> renders the spec object back into the daemon's own
spelling, and the whole spec goes back with the one key edited, not just the
key you edited.
=head2 Swarm, and what Podman serves instead
The Engine API groups C</secrets> with Swarm. A Docker daemon that is not a
swarm manager answers B<503> C<"This node is not a swarm manager."> to every
one of these endpoints, and this client turns that into a croak. That is the
engine behaving as documented, not a fault at this end: it needs
C<docker swarm init>, or a manager to talk to -- and a single-node install
that has never run it is the ordinary case, not an edge one.
C<GET /info>'s C<Swarm.LocalNodeState> does not tell you which of those two
you are looking at. Measured fresh against both engines with no swarm
initialized anywhere, it reports C<"inactive"> on Docker and on Podman alike
-- and Podman still serves C</secrets> with B<200> and real data in that
state, while Docker still answers B<503>. Whether C</secrets> works is a
property of the engine, not of that field.
lib/API/Docker/API/Secrets.pm view on Meta::CPAN
$secret->update(%spec); # the same call, via the entity
Update a secret. Returns nothing on success -- the daemon answers 200 with an
empty body.
C<$version> is mandatory and is the C<Version.Index> from L</inspect>; see
L</"update takes the current version, and it is mandatory"> for why it cannot
be guessed and why the whole spec goes back.
L<API::Docker::Role::Entity::Secret/update> fills it in from the entity it
was called on. Podman does not implement this
endpoint and answers 501.
=head2 remove
$secrets->remove($id);
Remove a secret by ID or name. The daemon answers 204 with no body, so this
returns nothing; a secret that is not there is a 404 and croaks.
=head1 SEE ALSO
lib/API/Docker/API/System.pm view on Meta::CPAN
Get version information about the Docker daemon and API.
Returns hashref with keys including C<ApiVersion>, C<Version>, C<GitCommit>,
C<GoVersion>, C<Os>, and C<Arch>.
=head2 ping
my $pong = $system->ping;
Health check endpoint. Returns C<OK> string if daemon is responsive.
=head2 events
my $events = $system->events(
since => 1234567890,
until => 1234567900,
filters => { type => ['container'] },
);
Get events from the Docker daemon. Returns an ArrayRef of events, one per
lib/API/Docker/API/System.pm view on Meta::CPAN
L<API::Docker::Role::Filters>; the daemon validates the names here, so a
misspelt one is a failed request rather than a quiet no-match
=item * C<on_event> - CodeRef called with each event as it arrives, instead of
the ArrayRef being collected and returned; see below
=back
=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';
},
lib/API/Docker/API/System.pm view on Meta::CPAN
=item * C<response> - HashRef the status line and the response headers are
written into, as for L<API::Docker::Role::HTTP/get>
=back
Passing neither C<auth> nor any credential key croaks before the request is
made.
=head3 What Podman answers
Measured against the rootless Podman socket (5.4.2, API 1.41): the endpoint
exists, but a failed check is B<500 Internal Server Error>, not Docker's 401,
and the message is the registry's own text wrapped by Podman --
C<< {"message":"login attempt to 127.0.0.1:1 failed with status: ..."} >>.
An empty AuthConfig answers
C<< {"message":"login attempt to failed with status: getting username and
password: cannot prompt for username without stdin"} >>, also 500. So the
croak is reliable on both engines while the status behind it is not: test the
outcome, not the number.
=head1 SEE ALSO
lib/API/Docker/API/Volumes.pm view on Meta::CPAN
Reference to L<API::Docker> client. Weak reference to avoid circular dependencies.
=head2 list
my $volumes = $volumes->list;
my $unused = $volumes->list(filters => { dangling => ['true'] });
List volumes. Returns an ArrayRef of L<API::Docker::Type::Volume> objects,
each carrying the methods of L<API::Docker::Role::Entity::Volume>. The
daemon answers this endpoint with a C<VolumeListResponse> rather than a bare
array; the C<Volumes> key is what comes back here, and the C<Warnings>
beside it are dropped.
Options:
=over
=item * C<filters> - HashRef of filter name to ArrayRef of string values; the
engine accepts C<dangling>, C<driver>, C<label> and C<name> here.
Shape-checked and normalised by L<API::Docker::Role::Filters>
lib/API/Docker/Error/HTTP.pm view on Meta::CPAN
=head2 location
Carp's location suffix (C< at FILE line N.\n>), captured at the point the
error was raised so it names the same frame a plain C<croak> would have named.
Kept apart from L</message> so a caller can have the reason without it.
=head2 status
The HTTP status code: C<404>, C<409>, C<500>. This is the whole point of the
class -- the one part of an engine error that is documented per endpoint and
identical across engines.
It arrives off the status line as a string of digits, exactly as
C<< $res{status} >> from L<API::Docker::Role::HTTP>'s C<response> option does,
so compare it numerically (C<< $err->status == 404 >>) rather than relying on
a type.
=head2 reason
The status line's reason phrase as the engine sent it (C<Not Found>,
lib/API/Docker/Error/Stream.pm view on Meta::CPAN
# ... and carries the whole stream that led up to the failure.
if (ref $err && $err->isa('API::Docker::Error::Stream')) {
for my $event (@{ $err->events }) {
print $event->{stream} if defined $event->{stream};
}
}
}
=head1 DESCRIPTION
The engine's streaming endpoints -- C</build>, C</images/create> (pull) and
C</images/{name}/push> -- report a failed operation as an C<errorDetail> object
B<inside> a stream that was already answered with HTTP 200. A client that only
treats status >= 400 as an error hands a broken build back to its caller as a
success.
L<API::Docker::Role::HTTP> therefore croaks with an object of this class as
soon as an C<errorDetail> event appears in such a stream. The object exists
purely so the progress output is not lost with the failure: the complete event
list, error event included, is available through L</events>.
lib/API/Docker/Error/Timeout.pm view on Meta::CPAN
required => 1,
);
has location => (
is => 'ro',
default => sub { '' },
);
has endpoint => (
is => 'ro',
default => sub { '' },
);
has phase => (
is => 'ro',
default => sub { 'read' },
);
lib/API/Docker/Error/Timeout.pm view on Meta::CPAN
quiet for longer than it -- and, with L</phase> set to C<'connect'>, when a
request was given a L<API::Docker::Role::HTTP/connect_timeout> and the socket
never came up within it.
The rest of this describes the read timeout, which is the one that has
something to hand back. A connect timeout carries no L</partial> and no
L</summary>, for the reason L</phase> gives: nothing was ever sent.
It is an B<idle> timeout, not a deadline: the clock is the time since the last
byte arrived, so a stream that keeps producing runs as long as it likes and one
that stalls is cut off. That is the distinction the endpoints this exists for
need -- a hung C</containers/{id}/attach> has already delivered its buffered
frames before it stalls, so "nothing yet" would never have fired.
=head2 Why it is fatal, on every path
A timeout is not information about the response; it is the absence of it. The
transport cannot know whether the daemon was about to send the rest, so it
cannot decide for the caller that what arrived is usable -- and every return
shape this distribution promises would hide the question if it tried. C<ndjson>
promises an ArrayRef of events, C<raw> promises the response bytes, the default
lib/API/Docker/Error/Timeout.pm view on Meta::CPAN
The request is named without its query string, for the same reason the
C<< >= 400 >> croak names it that way -- C</build> carries its C<buildargs>
there, which can hold credentials and have no business in an exception.
=head2 location
Carp's location suffix (C< at FILE line N.\n>), captured at the point the
error was raised so it names the same frame a plain C<croak> would have named.
Kept apart from L</message> so a caller can have the reason without it.
=head2 endpoint
The request that timed out, as C<"GET /v1.47/containers/json"> -- method and
path, no query string.
=head2 phase
Which of the two bounds fired: C<'read'> for
L<API::Docker::Role::HTTP/read_timeout>, C<'connect'> for
L<API::Docker::Role::HTTP/connect_timeout>. C<'read'> is the default, so an
object built without it describes what every one of them used to describe.
lib/API/Docker/Error/Truncated.pm view on Meta::CPAN
required => 1,
);
has location => (
is => 'ro',
default => sub { '' },
);
has endpoint => (
is => 'ro',
default => sub { '' },
);
has phase => (
is => 'ro',
required => 1,
);
lib/API/Docker/Error/Truncated.pm view on Meta::CPAN
The request is named without its query string, for the same reason the
C<< >= 400 >> croak names it that way -- C</build> carries its C<buildargs>
there, which can hold credentials and have no business in an exception.
=head2 location
Carp's location suffix (C< at FILE line N.\n>), captured at the point the
error was raised so it names the same frame a plain C<croak> would have named.
Kept apart from L</message> so a caller can have the reason without it.
=head2 endpoint
The request that was cut short, as C<"GET /v1.47/images/get"> -- method and
path, no query string. The empty string for a reader driven directly with no
request context, which is how the transport's own tests drive them.
=head2 phase
Which piece of the response framing the stream ended inside. One of:
=over
lib/API/Docker/Role/Entity/Plugin.pm view on Meta::CPAN
answers plugin requests with -- the same definition for
C<GET /plugins> and C<GET /plugins/{name}/json>, so
L<API::Docker::API::Plugins/list> and L<API::Docker::API::Plugins/inspect>
hand back one class and there is no list-versus-inspect shape to keep apart.
=head2 The entity is addressed by name, not by id
Every method here threads C<< ->name >> through to the method of the same
name on L<API::Docker::API::Plugins>, so the options, the return values and
the failure modes are that class's -- documented there, not repeated here.
The class does carry an C<< ->id >>, but the endpoints route on the name,
which is why this role C<requires 'name'>.
C<< ->name >> is the plugin as it is B<installed locally> --
C<vieux/sshfs:latest>, or whatever local name
L<API::Docker::API::Plugins/install> was given. The remote it came from is
C<< ->plugin_reference >> (C<docker.io/vieux/sshfs:latest>), which the engine
sets on the pull, upgrade and create paths only and omits entirely otherwise
rather than sending it as null -- so it reads as C<undef> for a plugin that
never came from a registry, and differs from the name outright for one
installed under a local one. That is exactly the case where L</upgrade>
lib/API/Docker/Role/Entity/Plugin.pm view on Meta::CPAN
C<< $plugin->settings->env >> is a list of C<KEY=value> B<strings>, which is
what L</configure> takes. C<< $plugin->config->env >> is a list of
L<API::Docker::Type::PluginEnv> objects describing those same variables --
same field name, two shapes. The daemon flattens the one into the other when
the plugin is installed. L</configure> writes to the settings, never to the
config.
=head2 Not available on Podman
Managed plugins are a Docker feature: none of these endpoints exist on
Podman, so nothing in this role works against it. See
L<API::Docker::API::Plugins/"Not available on Podman">.
Why the methods are a role applied to a generated class rather than a class
of their own: L<API::Docker::Role::Entity/DESCRIPTION>.
=head2 inspect
my $fresh = $plugin->inspect;
lib/API/Docker/Role/Entity/Secret.pm view on Meta::CPAN
containers, never back over C</secrets>: neither C<list> nor C<inspect>
returns a C<< spec->data >>, so there is nothing here to decode. That is why
this role has no C<decoded_data>, where
L<API::Docker::Role::Entity::Config> does -- the difference is in what the
engine sends, not in what the two choose to offer. In the C<GET /secrets>
response captured from Podman 5.4.2 (API 1.41) in
F<t/fixtures/secrets_list.json>, each object carries C<ID>, C<CreatedAt>,
C<UpdatedAt>, C<Spec> and C<Version>, and the C<Spec> has C<Name>, C<Driver>
and C<Labels> but no C<Data> key whatsoever. The swagger agrees: it documents
C<SecretSpec.Data> as used to I<create> a secret and not returned by other
endpoints.
C<< $secret->spec->data >> therefore exists as an accessor -- the field is in
the definition -- and reads C<undef> on anything an engine sent back.
If you need to read a value back, a secret is the wrong storage -- put it in
a config, see L<API::Docker::API::Configs>.
=head2 The spec goes back as a whole
C<< $secret->spec >> is an L<API::Docker::Type::SecretSpec> object rather
lib/API/Docker/Role/Entity/Secret.pm view on Meta::CPAN
The default is only a default. A C<version> key in the arguments is used
verbatim and removed before the spec goes out -- the spec's own fields are all
capitalised (C<Name>, C<Labels>, C<Data>, ...), so a lowercase C<version>
cannot collide with one:
$secret->update(version => $index, %spec);
Send the whole spec back with the one key edited; the Engine API accepts a
change to C<Labels> only and wants every other field unchanged. Podman does
not implement this endpoint and answers 501.
=head2 remove
$secret->remove;
Remove the secret. The daemon answers 204 with no body, so this returns
nothing.
=head1 SEE ALSO
lib/API/Docker/Role/Entity/Volume.pm view on Meta::CPAN
A volume has B<one> shape. The swagger answers C<GET /volumes/{name}> and
C<POST /volumes/create> with the C<Volume> definition outright, and
C<GET /volumes> with a C<VolumeListResponse> whose C<Volumes> is an array of
that same definition -- which is why
L<API::Docker::API::Volumes/list> unwraps that one key and returns the
entries, and why C<create> hands back an entity where the other resources
return the daemon's raw response.
=head2 The entity is addressed by name, not by id
A volume has no C<Id>. Its name is its identifier on every endpoint, which is
why this role C<requires 'name'> where the container, image and network ones
require C<id>.
Every method here forwards to L<API::Docker::API::Volumes> with the volume's
own C<name> and returns whatever that method returns; the options are that
method's options, undocumented here on purpose so there is one place to
correct when the engine's are found to be something else.
Why the methods are a role applied to a generated class rather than a class
of their own: L<API::Docker::Role::Entity/DESCRIPTION>.
lib/API/Docker/Role/Filters.pm view on Meta::CPAN
sub list {
my ($self, %opts) = @_;
my %params;
$params{filters} = $self->_normalise_filters($opts{filters})
if defined $opts{filters};
return $self->client->get('/whatever', params => \%params);
}
=head1 DESCRIPTION
Every C<list> and C<prune> endpoint of the Engine API takes a C<filters>
query parameter, and every one of them wants the same thing: a JSON B<map of
string to array of string>.
filters => { dangling => ['true'] } correct
filters => { dangling => 'true' } wrong -- not an array
filters => { dangling => 1 } wrong -- not an array
filters => { dangling => [1] } wrong -- a number, not a string
filters => { dangling => [\1] } wrong -- a JSON boolean
The transport JSON-encodes a HashRef C<params> value on its own, so the
lib/API/Docker/Role/Filters.pm view on Meta::CPAN
=head2 Why the boolean rewrite is bound to the type and not to the value
A helper that rewrote every true-ish value to C<'true'> would be wrong more
often than it was right. C<< { exited => [0] } >> asks for containers that
exited with status 0, C<< { stars => [0] } >> for images with no stars, and
C<< { label => [1] } >> for a label whose value is C<1> -- rewriting any of
those to C<'false'>/C<'true'> would silently ask a different question.
Binding it to the filter I<name> instead would need a table of which names
are boolean, per endpoint, kept in step with the daemon -- see
L</"What it deliberately does not do">.
So the rewrite is bound to the value's B<type>: a plain Perl C<1> carries no
claim of being a boolean and becomes the string C<"1">, while
C<< JSON->true >> and C<\1> carry exactly that claim, and are also the two
forms C<encode_json> would otherwise turn into a JSON C<true> -- which the
daemon rejects outright.
That leaves one form this role cannot recognise: perl 5.36's core booleans,
where C<< !!1 >> and C<< $x == $y >> produce a boolean the JSON encoder also
writes as C<true>. Stringified, those are C<"1"> and C<""> -- and C<"1"> is a
value the daemon reads as true, so only the false one needs help. It is the
reason an empty string croaks here rather than travelling on.
=head2 What it deliberately does not do
It does not check filter B<names>. Doing so would need one accepted-name
table per endpoint, and the daemon already has them: measured against Podman
5.x (API 1.41), an unknown name is refused with HTTP 500 by
C</containers/json> (C<bogusname is an invalid filter>), C</images/json>
(C<invalid image filter "danglin">), C</volumes>, C</networks> and
C</events>, and Docker validates C</plugins> the same way -- which is how the
Engine API reference's documented C<enable> turns out to be a hard error
where the daemon wants C<enabled> (see
L<API::Docker::API::Plugins/list>). A client-side table would duplicate that
check, and the first time it lagged the daemon it would refuse a filter the
daemon accepts. That is a worse failure than the one it prevents.
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# _assert_request_path). Query parameters carry the ? and everything after it
# and are assembled separately below, each element run through _uri_encode.
my $REQUEST_PATH = qr{\A[A-Za-z0-9\-._~:/\@!\$&'()*+,;=%]*\z};
# The three units a response can be cut into, one option each. A request picks
# one of them, or none and gets the buffered path; see _stream_handler.
my @STREAM_OPTION = qw( on_event on_frame on_chunk );
# What a response body has to start with to be worth handing to decode_json.
# An object or an array is not the whole of JSON: the engine answers several
# endpoints with a bare JSON scalar, and a `null` used to come back as the
# four-character string 'null'. See _request.
my $JSON_BODY = qr/\A\s*(?:[\[\{"]|-?[0-9]|true|false|null)/;
# How much is asked for per sysread. Strictly an upper bound -- sysread
# returns what has arrived rather than filling to it (see _pull), so on a live
# feed a call typically comes back with one burst, and asking for 64K costs
# nothing but the size of the buffer it lands in.
my $READ_SIZE = 64 * 1024;
has read_timeout => (
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
has connect_timeout => (
is => 'ro',
);
has _socket => (
is => 'lazy',
clearer => '_clear_socket',
);
# The connect timeout and the endpoint it belongs to, on their way to
# _build__socket. It is a lazy builder and so cannot be handed an argument,
# and the value is per request rather than per client -- hence rw, with
# _reconnect as the only writer, setting it immediately before the build and
# clearing it immediately after. Unset is the whole of the old behaviour: no
# Timeout on any constructor, and the plain croak on a failure.
has _pending_connect => (
is => 'rw',
init_arg => undef,
);
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
return 0 unless $timeout;
return 1 if defined $@ && $@ =~ /connect: timeout\z/;
return 1 if $! == EAGAIN || $! == EWOULDBLOCK;
return 0;
}
sub _croak_connect_timeout {
my ($self, $pending, $where) = @_;
my $endpoint = $pending->{endpoint};
# See _croak_timeout for why the object goes into a variable first and why
# the location is captured by hand.
my $error = API::Docker::Error::Timeout->new(
message => 'Docker API connect timeout'
. (defined $endpoint && length $endpoint ? ' (' . $endpoint . ')' : '')
. ': ' . $where . ' did not accept within ' . $pending->{timeout} . 's',
location => shortmess(''),
endpoint => defined $endpoint ? $endpoint : '',
timeout => $pending->{timeout},
phase => 'connect',
);
croak $error;
}
# undef for "no timeout", which is both the default and the explicit 0, so a
# client carrying a default can be opted out of for one request. Anything that
# is not a non-negative number is a caller mistake and is refused rather than
# rounded to something: silently reading a typo as "off" would hand back the
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# Why SO_RCVTIMEO and not select(): a bound that reads the socket cannot see
# what is already buffered above it, and would fire while the data it was
# waiting for was in hand. That was true of PerlIO's read-ahead when this was
# written (measured: after one readline of a socket holding
# "one\ntwo\nthree\n", two whole lines sit in the PerlIO buffer and select()
# says the handle is not ready), and it is true of _read_buffer now. A
# select-based bound would have to be asked only when that buffer is empty,
# which is one more invariant to keep for no gain: SO_RCVTIMEO bounds the one
# syscall in _pull for one setsockopt, and gets idle-since-the-last-byte
# semantics for free, which is the semantics these endpoints need (karr k52:
# the buffered frames arrive, and *then* the socket stalls -- a
# time-to-first-byte bound would never fire).
sub _apply_read_timeout {
my ($self, $sock, $timeout) = @_;
return unless $timeout;
my $packed;
if ($^O eq 'MSWin32' || $^O eq 'cygwin') {
# Winsock takes a DWORD of milliseconds here rather than a struct timeval,
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
? ' after ' . length($partial) . ' byte'
. (length($partial) == 1 ? '' : 's')
: ', nothing arrived at all';
# Carp hands a reference straight back rather than decorating it, so this
# croak is a die with an object -- hence the location captured by hand,
# which names the same frame a croak of a plain string would have named.
# The object goes into a variable first: `croak CLASS->new(...)` is indirect
# object syntax and parses as CLASS->croak(new(...)).
my $error = API::Docker::Error::Timeout->new(
message => 'Docker API read timeout (' . $ctx->{endpoint} . '): '
. $ctx->{timeout} . 's of silence' . $after,
location => shortmess(''),
endpoint => $ctx->{endpoint},
timeout => $ctx->{timeout},
partial => $partial,
summary => $summary,
);
croak $error;
}
# The other way a response ends before it is finished, and the one that needs
# no option to be armed: the daemon closed mid-sentence (karr k64).
#
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
my $arrived = $summary
? $summary->{delivered} . ' unit'
. ($summary->{delivered} == 1 ? '' : 's') . ' delivered'
: 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';
}
# Carp hands a reference straight back rather than decorating it, so this
# croak is a die with an object -- hence the location captured by hand,
# which names the same frame a croak of a plain string would have named.
# The object goes into a variable first: `croak CLASS->new(...)` is indirect
# object syntax and parses as CLASS->croak(new(...)).
my $error = API::Docker::Error::Truncated->new(
message => 'Docker API response truncated'
. (length $endpoint ? ' (' . $endpoint . ')' : '') . ': '
. $detail . '; ' . $arrived,
location => shortmess(''),
endpoint => $endpoint,
phase => $what{phase},
expected => $what{expected},
received => $what{received},
partial => $partial,
summary => $summary,
);
croak $error;
}
# ---------------------------------------------------------------------------
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
#
# Every byte of a response is taken off the handle by _pull and by nothing
# else, and every reader below is served out of the buffer _pull fills. That
# is not an optimisation, it is the only shape that works (karr k60).
#
# What forced it: perl's read() is fread-shaped. It loops until it has the
# LENGTH it was asked for or the stream ends -- it does not return what has
# arrived. Measured on an AF_UNIX socketpair whose peer writes 6 bytes, waits
# half a second, writes 6 more and closes: read($sock, $buf, 65536) came back
# with 12 after 0.90s, having waited for the close, while sysread came back
# with 6 in 0.00s. On the endpoints with neither a Content-Length nor chunked
# encoding -- attach, logs(follow), exec/start, all
# application/vnd.docker.raw-stream -- the reader asks for $READ_SIZE, so
# read() delivered nothing to an on_frame/on_chunk callback until 64K had
# piled up or the daemon hung up. On a stream that never ends it would deliver
# nothing at all. The POD promised those callbacks the bytes as they arrive,
# and that promise was not kept.
#
# Why it could not be fixed at the one site that had the bug: _read_head read
# the status line and the headers with <$sock>, and PerlIO reads ahead. The
# bytes past the header block were sitting in a buffer this code cannot reach
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
sub _request {
my ($self, $method, $path, %opts) = @_;
# Checked while the request is assembled, like a header name: a caller that
# passes something else gets told before anything reaches the daemon,
# instead of after the round trip when the metadata fails to arrive.
croak __PACKAGE__ . '->_request response option must be a HashRef'
if exists $opts{response} && ref $opts{response} ne 'HASH';
# Checked here for the same reason, and one at a time: the three units are
# three shapes the engine's streaming endpoints have, not three views of one
# stream, so a request asking for two of them has no answer.
my @streaming = grep { exists $opts{$_} } @STREAM_OPTION;
croak __PACKAGE__ . '->_request takes one of ' . join(', ', @STREAM_OPTION)
. ', not ' . join(' and ', @streaming) if @streaming > 1;
croak __PACKAGE__ . '->_request ' . $streaming[0] . ' option must be a CodeRef'
if @streaming && ref $opts{$streaming[0]} ne 'CODE';
my $version = $self->api_version;
# Caller data -- a container name, an image reference -- is spliced straight
# into the request line through $path, so it is validated before it can reach
# the wire, exactly as a header name is. See _assert_request_path.
$self->_assert_request_path($path);
my $url_path = defined $version ? "/v$version$path" : $path;
# Kept before the query string is appended: it names the request in an
# error message, and the query string is where /build carries buildargs,
# which can hold credentials and have no business in an exception.
my $endpoint = $method . ' ' . $url_path;
# Definedness, not truth. A raw_body of '' (an empty tar) or of the string
# '0' is a body the caller asked to send, and testing it for truth dropped
# both: they fell through to the body branch, and the request then went out
# with no Content-Length, no Content-Type and no payload at all. Whether
# there is a body is therefore tracked separately from what it says.
my $body_content;
my $content_type = 'application/json';
if (defined $opts{raw_body}) {
$body_content = $opts{raw_body};
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
if ($opts{params}) {
my @pairs;
for my $k (sort keys %{$opts{params}}) {
my $v = $opts{params}{$k};
next unless defined $v;
# An ArrayRef is one parameter given more than once, not one value:
# `names => ['a', 'b']` is `names=a&names=b`. That spelling is the only
# one GET /images/get accepts -- the comma-joined form is read as a
# single image reference and answered with 500 -- and Go's r.Form[k] is
# a list for every parameter, so it is the general shape rather than
# that endpoint's quirk. Element order is the caller's and is kept;
# only the keys are sorted.
for my $item (ref $v eq 'ARRAY' ? @$v : ($v)) {
next unless defined $item;
push @pairs, _uri_encode($k) . '='
. _uri_encode(ref $item eq 'HASH' ? encode_json($item) : $item);
}
}
$url_path .= '?' . join('&', @pairs) if @pairs;
}
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
next unless defined $v;
$v =~ s/[\r\n]//g;
$request .= "$h: $v\r\n";
}
}
$request .= "\r\n";
$request .= $body_content if defined $body_content;
my $handler = @streaming
? $self->_stream_handler($endpoint, $streaming[0], $opts{$streaming[0]},
$opts{croak_on_error} // 1)
: undef;
# Resolved with exists rather than truth, so `read_timeout => 0` is a
# request to wait as long as it takes and can turn a client-wide default off
# for one call -- which `//` would have read as "no opinion" and overridden.
my $timeout = $self->_read_timeout_value(
exists $opts{read_timeout} ? $opts{read_timeout} : $self->read_timeout);
# Same resolution, same reason.
my $connect_timeout = $self->_connect_timeout_value(
exists $opts{connect_timeout}
? $opts{connect_timeout} : $self->connect_timeout);
# The endpoint without its query string, for the same reason the >= 400
# croak uses that form.
my $ctx = { endpoint => $endpoint, timeout => $timeout };
my $sock = $self->_reconnect(
{ timeout => $connect_timeout, endpoint => $endpoint });
# Applied here rather than in _build__socket because the value is per
# request, not per client: a socket is opened and closed for each one, so
# this is the connection the option belongs to.
$self->_apply_read_timeout($sock, $timeout);
print $sock $request;
# Reading can croak, and now does so from further in than it used to: an
# on_event stream raises Error::Stream at the event that reports the
# failure, and an on_frame stream refuses a header that is not one. Without
# the eval those exceptions leave the socket open until the next request
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# Zero bytes is a different answer in each shape a request can ask for, so
# the two options that promise one are answered before the empty-body check
# rather than after it. `raw` promises the response bytes and a body of no
# bytes is '', which a caller can take length() of; `ndjson` promises an
# ArrayRef of events even for a stream carrying a single object, so a stream
# that carried none is []. Returning undef for both broke each promise
# exactly where the engine legitimately says nothing.
$body = '' unless defined $body;
# The framed endpoints (logs, attach, exec/start) carry arbitrary bytes
# that must not be mistaken for JSON -- a TTY container printing a JSON
# line would otherwise come back decoded.
return $body if $opts{raw};
# Streaming endpoints (/build, /images/create, /images/*/push) always
# return an ArrayRef of events, even when the stream carried exactly one
# object. See _decode_stream.
if ($opts{ndjson}) {
my $events = $self->_decode_stream($body);
# A failed build, pull or push is HTTP 200 with the failure buried in the
# stream, so the status line above cannot catch it. Opt out for a stream
# whose objects are engine data rather than the outcome of one operation.
$self->_assert_no_stream_error($endpoint, $events)
if $opts{croak_on_error} // 1;
return $events;
}
# Nothing was asked of the body's shape and there is no body, so there is
# nothing to hand back. 204 says so in the status line and is taken at its
# word even if bytes follow it.
return undef if $status_code == 204 || $body eq '';
# A body that is JSON is decoded, whichever JSON value it is. The guard was
# `{` or `[` alone, which returned a body that is a bare JSON scalar as its
# own bytes: `null` came back as the four-character string 'null'. The
# engine sends exactly that where a Go nil slice or pointer is the whole
# response -- GET /plugins/privileges for a plugin that demands nothing,
# GET /containers/{id}/changes for a container that changed nothing -- and
# the string is neither the ArrayRef those endpoints document nor anything
# a caller can iterate.
#
# The eval decides, not the pattern: a plain-text body that happens to
# start with one of these characters fails to decode and is returned as
# itself. So must the eval's success, not its result -- decode_json('null')
# is a successful decode to undef.
if ($body =~ $JSON_BODY) {
my $decoded;
return $decoded if eval { $decoded = decode_json($body); 1 };
}
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# (a single pretty-printed object), so nothing is silently dropped.
unless (@events) {
my $event = eval { decode_json($body) };
push @events, $event if defined $event;
}
return \@events;
}
sub _assert_no_stream_error {
my ($self, $endpoint, $events) = @_;
for my $event (@$events) {
next unless ref $event eq 'HASH';
my $detail = $event->{errorDetail};
next unless defined $detail;
# errorDetail is a HashRef carrying the message. The engine sends a flat
# `error` next to it with the same text; that is the fallback, not the
# trigger -- the trigger is errorDetail, and nothing else.
my $reason = ref $detail eq 'HASH' ? $detail->{message} : undef;
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# Engine messages end in a newline, and Carp appends no location to a
# message that already does.
$reason =~ s/\s+\z//;
# Carp hands a reference straight back rather than decorating it, so this
# croak is a die with an object -- hence the location captured by hand,
# which names the same frame a croak of a plain string would have named.
# The object goes into a variable first: `croak CLASS->new(...)` is
# indirect object syntax and parses as CLASS->croak(new(...)).
my $error = API::Docker::Error::Stream->new(
message => 'Docker API stream error (' . $endpoint . '): ' . $reason,
events => $events,
location => shortmess(''),
);
croak $error;
}
return;
}
sub _assert_header_name {
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# Only a stream the daemon ended has a tail worth flushing, or a leftover
# worth complaining about. One the caller stopped has bytes in the carry
# buffer by construction, and treating those as truncation would turn every
# early stop into an error.
$handler->{finish}->() unless $handler->{stopped}->();
return [$status_code, $status_text, $headers, '', $handler->{summary}->()];
}
# One unit per call, and the unit is whichever of the three the caller asked
# for. The engine's streaming endpoints do not share one: /events and the
# build/pull/push progress streams are newline-delimited JSON, logs and
# exec/start are 8-byte-framed, and an image export is bytes with no structure
# above them at all. Forcing one unit on all three would mean handing two of
# them back undecoded and calling it streaming.
#
# The three decoders differ only in how they cut the byte stream up; the carry
# buffer, the delivery and the stop handling below are common to all of them.
sub _stream_handler {
my ($self, $endpoint, $option, $cb, $croak_on_error) = @_;
my $carry = '';
my $delivered = 0;
my $stopped = 0;
# Stopping is an explicit call, not a return value, and the callback's
# return value is deliberately never looked at. Every truthiness convention
# has a silent failure mode here: `push @got, $_[0]` returns a count and
# `$last = $event->{status}` returns whatever the engine said -- and a
# container event's status is literally 'stop'. Both would end the stream by
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
my $emit_line = sub {
my ($line) = @_;
$line =~ s/\r\z//;
return 1 unless $line =~ /\S/;
my $event = eval { decode_json($line) };
return 1 unless defined $event;
# Checked per event rather than over the finished list, so a failed
# build croaks at the event that reports it instead of when the daemon
# eventually closes. The Error::Stream then carries that one event: a
# callback stream keeps no history, having been given all of it already.
$self->_assert_no_stream_error($endpoint, [$event]) if $croak_on_error;
return $deliver->($event);
};
$feed = sub {
my ($bytes) = @_;
$carry .= $bytes;
# A JSON string cannot contain a literal newline, so a newline in the
# buffer always ends an event -- and everything after the last one is
# an event still arriving, which stays in the carry for the next read.
while ((my $idx = index($carry, "\n")) >= 0) {
my $line = substr($carry, 0, $idx, '');
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
=item * HTTP/1.1 chunked transfer encoding
=item * Automatic JSON encoding/decoding
=item * Newline-delimited JSON event streams (C<< ndjson => 1 >>), including
the failures the engine reports inside an HTTP 200 body
=item * Demultiplexing of the Docker stream format (L</stream_frames>)
=item * Incremental delivery of a response through a per-request callback, so
the endpoints that never close are usable at all (L</"Streaming a response as
it arrives">)
=item * Request/response logging via L<Log::Any>
=item * Automatic connection management
=back
Consuming classes must provide C<host>, C<api_version>, C<tls>, C<cert_path>
and C<tls_insecure> attributes. The last three are read only by the C<tcp://>
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
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>.
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>
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
=item * C<ndjson> - Parse the body as newline-delimited JSON and always
return an ArrayRef of events, even for a stream carrying a single object.
Named for the format rather than C<stream>, which is already a query
parameter of C</events> and C</containers/{id}/stats>. An C<errorDetail>
event in such a stream croaks; see L</"Failure inside a 200 response">
=item * C<croak_on_error> - Default true, and only consulted with
C<< ndjson => 1 >>. Set it false for a stream whose objects are engine data
rather than the outcome of one operation -- C</events> is the only such
endpoint here
=item * C<raw> - Never decode the body; return the response bytes verbatim
=item * C<response> - HashRef the status line and the response headers are
written into; see L</"Reading the status line and the response headers">
=item * C<on_event>, C<on_frame>, C<on_chunk> - CodeRef called with each unit
of the response as it arrives, instead of the body being buffered and
returned. At most one of the three; see L</"Streaming a response as it
arrives">
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
=head3 It is an idle timeout, not a deadline
The clock measures the time since the last byte arrived, not the time since
the request started. A stream that keeps producing runs as long as it likes;
one that stops producing is cut off. That distinction is the whole point --
both hangs above deliver data first and stall afterwards, so a bound on the
total time would have to be set longer than any legitimate stream, and a bound
on the time to the first byte would never fire at all.
=head3 There is no default, and no per-endpoint default either
Off unless asked for, everywhere. Whether a silence is a stall or normal is a
property of the workload rather than of the endpoint: C</build> with a large
context is legitimately quiet for as long as C</events> is, and a built-in
default on C<attach> would kill a perfectly healthy session at an idle shell
prompt. So no existing call changes behaviour, and picking the number is the
caller's -- who is the only one who knows what the request is for.
For the two endpoints above, if you want a figure to start from: a couple of
seconds is right for C<attach> or C<logs> used to collect what is already
there, and something above the daemon's own emit interval -- Docker sends a
stats reading about once a second -- for C<stats>.
=head3 What happens when it expires
The request croaks, on every path, with an L<API::Docker::Error::Timeout>. It
never returns a truncated response: a short body satisfies every return shape
this role promises and would be indistinguishable from a complete one. The
exception carries what did arrive -- C<< ->partial >> for a buffered request,
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
C<< ->phase >> C<'connect'>, C<< ->timeout >> the value that expired and an
empty C<< ->partial >> -- there is no response to have part of. Every other
connect failure croaks with the plain string it always did: a refused
connection, a missing socket path and a rejected certificate are diagnoses,
not timeouts, and rewriting them as one would name a cause the caller cannot
act on.
=head2 Streaming a response as it arrives
Without one of these options a request is read whole, then parsed. That is
right for a request/response endpoint and wrong for every endpoint whose point
is that it keeps going: C<< logs(follow => 1) >>, C</events> with no C<until>
and C</containers/{id}/stats> with no C<< stream => 0 >> never return, because
the daemon never closes and there is nothing else to wait for.
A callback is half the answer -- it decides what to do with each unit, and it
can stop. L</"Bounding a request that never ends"> is the other half, for the
stream that stops arriving without ever ending.
Pass a callback and the body is handed over piece by piece instead:
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
my ($event, $stop) = @_;
print $event->{status}, "\n";
$stop->() if $event->{status} eq 'destroy';
},
);
$summary; # { delivered => 7, stopped => 1 }
=head3 One unit per call, and three units to choose from
The engine's streaming endpoints do not share a natural unit, so there is an
option per unit and a request picks one:
=over
=item * C<on_event> - one decoded HashRef per newline-delimited JSON object.
For C</events> and the C</build>, C</images/create>, C</images/*/push>
progress streams
=item * C<on_frame> - one C<< { stream => ..., data => ... } >> HashRef per
demultiplexed frame of the Docker stream format. For
C<< /containers/{id}/logs >> and C<< /exec/{id}/start >>; normally reached
through L</stream_frames> rather than directly
=item * C<on_chunk> - the response bytes as they arrive, undecoded and
unbuffered. For an image export, and for anything with no structure this role
knows about
=back
Passing two of them croaks before the request is sent: they are three shapes
different endpoints have, not three views of one stream.
=head3 Saying stop
The callback is called as C<< $cb->($unit, $stop) >> and its return value is
ignored. To end the stream it calls C<< $stop->() >>; C<_request> checks after
the callback returns, delivers nothing further, and comes back.
An explicit closure rather than a return value, because every truthiness
convention has a silent failure mode here. C<< sub { push @got, $_[0] } >>
returns a count and C<< sub { $last = $event->{status} } >> returns whatever
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
=head3 How often the callback is called
Once per unit the daemon has finished sending, as soon as the bytes that
complete it have arrived -- not once per read of a fixed size, and not once
at the end.
That is worth stating because it was not true before karr k60. The reads were
C<read()>, which is C<fread>-shaped: it loops until it has the length it was
asked for or the stream ends, rather than returning what has arrived. On the
raw-stream endpoints -- C<attach>, C<< logs(follow => 1) >>, C<exec/start>,
which carry neither a C<Content-Length> nor chunked encoding -- the reader
asks for 64K, so nothing reached the callback until 64K had accumulated or the
daemon hung up. On a stream that never ends, nothing reached it at all.
Measured on an C<AF_UNIX> socket pair with no daemon involved, a peer writing
three frames 0.15s apart and then closing:
before: 1 call at 0.45s (the moment it closed)
after: 3 calls at 0.15s, 0.30s, 0.45s
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
C<eval>-and-inspect-C<$@> code cannot tell it from the plain croak it
replaces; C<< $err->events >> carries the complete event list, so the progress
output that led up to the failure is not lost with the return value.
The trigger is the C<errorDetail> key alone. The flat C<error> key the engine
sends beside it holds the same text and is used only as a fallback message,
never as the trigger on its own.
C<< croak_on_error => 0 >> turns the scan off for a stream that is a feed
rather than an operation. The check is on by default, and opting out is per
endpoint, because the set of operation-shaped streaming endpoints is
open-ended while the feed-shaped ones are C</events> and nothing else: a new
endpoint added without a thought about this gets the loud behaviour, not the
silent one.
=head2 Failure in the middle of a response
The daemon can also stop saying anything in the middle of saying it. A status
line with no terminator, a header block with no blank line to close it, a body
shorter than its C<Content-Length>, a chunk shorter than its own header, a
chunk header cut in half, a chunked body with no terminating zero chunk: each
of those is a response that ended before it was finished, and each croaks with
an L<API::Docker::Error::Truncated>.
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
C<< HEAD /containers/{id}/archive >> in fact announces no length at all, only
C<X-Docker-Container-Path-Stat> -- but an engine that does announce one is not
waited on either.
Options: C<params>, C<headers> and C<response> as for L</get>.
=head2 stream_frames
my $frames = $client->stream_frames('GET', "/containers/$id/logs", %opts);
Perform a request against one of the engine's framed endpoints
(C<< /containers/{id}/logs >>, C<< /exec/{id}/start >>) and return an ArrayRef
of frames:
[ { stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" } ]
C<stream> is C<stdout>, C<stderr> or C<stdin> for a multiplexed stream, and
C<raw> for an unframed one. It is always a plain string, so callers never need
a defined-check. Joining the payloads gives the plain text:
lib/API/Docker/Role/RegistryAuth.pm view on Meta::CPAN
package API::Docker::Role::RegistryAuth;
# ABSTRACT: AuthConfig encoding shared by the registry-facing endpoints
our $VERSION = '0.004';
use Moo::Role;
use Carp qw( croak );
use JSON::MaybeXS qw( decode_json encode_json );
use MIME::Base64 qw( decode_base64 encode_base64 );
use namespace::clean;
sub _registry_auth_header {
my ($self, $auth) = @_;
lib/API/Docker/Role/RegistryAuth.pm view on Meta::CPAN
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
API::Docker::Role::RegistryAuth - AuthConfig encoding shared by the registry-facing endpoints
=head1 VERSION
version 0.004
=head1 SYNOPSIS
package API::Docker::API::Whatever;
use Moo;
with 'API::Docker::Role::RegistryAuth';
lib/API/Docker/Role/RegistryAuth.pm view on Meta::CPAN
=item * as the plain JSON request body of C<< POST /auth >>
=back
This role carries the conversion in both directions so every class that
speaks to a registry agrees on it, and so a caller can hand the same C<auth>
argument to any of them.
B<It carries the encoding, not the policy.> Whether a header is sent at all
differs per endpoint on purpose and stays with the endpoint:
L<API::Docker::API::Images/push> sends C<X-Registry-Auth> on B<every> push
because the engine rejects an image push without it, while an anonymous
plugin or distribution call sends B<no> header -- their routers decode the
header and discard the error, so an absent one is the anonymous case rather
than a failure.
=head2 The padding is not optional
The engine decodes C<X-Registry-Auth> with Go's C<base64.URLEncoding>, not
C<RawURLEncoding>, so the C<=> padding is required. Stripping it makes every
lib/API/Docker/Role/RegistryAuth.pm view on Meta::CPAN
engine's C<base64.URLEncoding> decoder expects rather than failing there.
C<_registry_config_header($map)> is the same encoding for C<X-Registry-Config>
on C<< POST /build >>. It takes the same shapes, but the HashRef it JSON-encodes
is a B<map> of registry hostname to AuthConfig
(C<< { 'registry.example:5000' => { username => ..., password => ... } } >>),
not a single AuthConfig.
C<_registry_auth_config($auth)> returns the same credentials as a plain
HashRef for a JSON request body. C<undef> gives C<undef> -- whether that is
an error is the endpoint's call, not this role's. A HashRef is copied, a JSON
object is decoded, and a base64url string is decoded back through both
layers. Anything that does not read as an AuthConfig croaks.
=head1 SEE ALSO
=over
=item * L<API::Docker::API::Images> - C<push>, which always sends the header
=item * L<API::Docker::API::System> - C<auth>, which sends the body form
lib/API/Docker/Type/ClusterInfo.pm view on Meta::CPAN
package API::Docker::Type::ClusterInfo;
# ABSTRACT: Information about the swarm as is returned by the "/info" endpoint
our $VERSION = '0.004';
use API::Docker::Type;
use API::Docker::Type::ObjectVersion;
use API::Docker::Type::SwarmSpec;
use API::Docker::Type::TLSInfo;
use namespace::clean;
docker id => Str, wire => 'ID';
lib/API/Docker/Type/ClusterInfo.pm view on Meta::CPAN
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
API::Docker::Type::ClusterInfo - Information about the swarm as is returned by the "/info" endpoint
=head1 VERSION
version 0.004
=head1 DESCRIPTION
Generated from the C<ClusterInfo> definition of C<spec/v1.51.yaml>.
Join-tokens are not included.
lib/API/Docker/Type/ContainerNetworkStats.pm view on Meta::CPAN
docker tx_packets => Int, wire => 'tx_packets', since => '1.51';
docker tx_errors => Int, wire => 'tx_errors', since => '1.51';
docker tx_dropped => Int, wire => 'tx_dropped', since => '1.51';
docker endpoint_id => Str, wire => 'endpoint_id', since => '1.51';
docker instance_id => Str, wire => 'instance_id', since => '1.51';
1;
__END__
=pod
lib/API/Docker/Type/ContainerNetworkStats.pm view on Meta::CPAN
This field is Linux-specific and always zero for Windows containers.
Serialised as C<tx_errors> -- spelled out, because deriving it from the Perl
name would produce C<TxErrors>.
=head2 tx_dropped
Outgoing packets dropped. Windows and Linux. Serialised as C<tx_dropped> --
spelled out, because deriving it from the Perl name would produce
C<TxDropped>.
=head2 endpoint_id
Endpoint ID. Not used on Linux.
This field is Windows-specific and omitted for Linux containers. Serialised
as C<endpoint_id> -- spelled out, because deriving it from the Perl name
would produce C<EndpointId>.
=head2 instance_id
Instance ID. Not used on Linux.
This field is Windows-specific and omitted for Linux containers. Serialised
as C<instance_id> -- spelled out, because deriving it from the Perl name
would produce C<InstanceId>.
lib/API/Docker/Type/ContainerSummary.pm view on Meta::CPAN
Serialised as C<ImageID> -- spelled out, because deriving it from the Perl
name would produce C<ImageId>.
=head2 image_manifest_descriptor
OCI descriptor of the platform-specific manifest of the image the container
was created from.
Note: Only available if the daemon provides a multi-platform image store.
This field is not populated in the C<GET /system/df> endpoint. See
L<API::Docker::Type::OCIDescriptor>.
=head2 command
Command to run when starting the container.
=head2 created
Date and time at which the container was created as a Unix timestamp (number
of seconds since EPOCH).