view release on metacpan or search on metacpan
.claude/skills/api-docker-core/SKILL.md view on Meta::CPAN
write code against them or treat their POD as current; the objects the
daemon answers with are the `API::Docker::Type::*` classes above.
- **The composed `client` on an entity is a `weak_ref`**, declared by
`API::Docker::Role::Entity` and composed into every wrapped
`API::Docker::Type::*` alongside the resource-specific
`API::Docker::Role::Entity::*` role. `API::Docker->new->images->list`
leaves every returned entity with `client => undef`, and the next
`$image->remove` dies on an undefined invocant. The client must stay in a
live variable â in library code, in examples, and in tests.
- **Query-string booleans are normalised to `1`/`0`** (`$opts{all} ? 1 : 0`);
**JSON-body booleans are `\1`/`\0`** (see `Exec::start`), because the engine
type-checks the body but not the query string.
- **A `params` value that is a hashref is JSON-encoded automatically**, but
`filters` specifically goes through `API::Docker::Role::Filters`, which
normalises it into the map-of-string-to-array-of-string shape the engine
wants (wraps a bare scalar in an array, stringifies numbers, turns a JSON
or `\1`/`\0` boolean into `'true'`/`'false'`) and croaks on anything else â
another ref, `undef`, an empty string. Pass
`filters => { dangling => ['true'] }` and let the role do the rest;
encoding it by hand double-encodes it.
- **Extra headers go through `headers =>`**, which strips CR/LF. Never
.claude/skills/api-docker-core/SKILL.md view on Meta::CPAN
=> $self->client)` â never `new`. A daemon response and a caller-built
object are different name spaces: `from_data` reads only the swagger's own
wire names, so a key it does not recognise keeps its own spelling in
`unknown_fields` instead of being misread as the Perl name of an unrelated
field it happens to collide with, and a value that disagrees with the
declared type costs that one field (recorded in `rejected_fields`) rather
than failing the whole response. `new` stays strict and croaks on both
cases â it is what a caller's own arguments go through. Detail:
`API::Docker::Role::Type`.
The convenience methods (`$container->start`, `$image->remove`, `logs`,
`is_running`, ...) are not on the generated classes. They live in
`API::Docker::Role::Entity::*` roles and are composed onto the generated
`API::Docker::Type::*` classes at load time
(`Moo::Role->apply_roles_to_package`), because the generated files must
match `maint/spec-to-type.pl`'s output byte for byte (`t/spec_to_type.t`
enforces it) â nothing hand-written can live in them. See
`API::Docker::Role::Entity` for why a role composed onto the class, and not
a wrapper class holding one.
Fields carry the swagger's own names in snake_case â `$container->state`,
.claude/skills/docker-engine-api/SKILL.md view on Meta::CPAN
`GET /version` answers `ApiVersion` (newest supported) and `MinAPIVersion`
(oldest). Negotiate by requesting `/version` unprefixed, then using
`ApiVersion` for everything else. Asking for a version above `ApiVersion` fails
with 400 `client version 1.99 is too new`; below `MinAPIVersion` fails the same
way. A feature added in a later version is simply absent â the daemon returns
404 or silently ignores the query parameter, so a client that assumes a
parameter took effect can be wrong without any error.
## Response shapes
- **204 No Content** is the success case for `start`, `stop`, `kill`, `pause`,
`remove` and friends. There is no body to decode.
- **304 Not Modified** means the container was already in the requested state â
starting a running container, stopping a stopped one. It is *not* an error,
and a client that only special-cases `>= 400` will hand back an empty result
here. Decide explicitly whether that is success.
- **Errors** carry `{"message": "..."}` as JSON with a 4xx/5xx status. The
message is human text; do not parse it for control flow.
- **`/build`, `/images/create` (pull) and `/images/{name}/push` stream
newline-delimited JSON** â one object per line: `{"stream":â¦}`,
`{"status":â¦,"progress":â¦}`, `{"aux":{"ID":â¦}}`, `{"errorDetail":{â¦}}`.
**A failed build, pull or push is still HTTP 200.** The failure arrives as an
`errorDetail` object inside the stream, after the daemon has already committed
to a successful status line. Any client that treats HTTP status as the verdict
reports a broken build as a success. Scan the events.
## The multiplexed stream â the one that looks like it works
`GET /containers/{id}/logs`, `/containers/{id}/attach` and
`POST /exec/{id}/start` return **frames, not text**, whenever the container was
created **without** a TTY:
```
[STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4][payload of SIZE bytes]
```
`STREAM_TYPE` is 0 stdin, 1 stdout, 2 stderr. `SIZE` is a big-endian uint32.
Frames repeat until the stream ends. Measured against a container running
`echo OUT; echo ERR 1>&2`:
.claude/skills/docker-engine-api/SKILL.md view on Meta::CPAN
```
**With `Tty: true` the stream is raw** â no headers, and newlines arrive as
`\r\n` because a PTY is involved. That is the trap: a developer testing by hand
reaches for an interactive container, sees clean text, and ships a client that
emits header bytes into the caller's log output for every non-TTY container â
which is every container a program actually runs. Demultiplex by reading eight
bytes, taking the length, reading that many payload bytes, repeating. Go clients
get this from `stdcopy.StdCopy`; everyone else writes it.
`attach` and `exec/start` additionally accept `Upgrade: tcp` +
`Connection: Upgrade`, to which the daemon answers **101 Switching Protocols**
and hands over a bidirectional connection. Without those headers it answers 200
and streams the same frames one-way.
## Filters are JSON, and the shape is specific
`filters` is a query parameter holding a JSON-encoded **map of string to array
of string** â the values are arrays of *strings*, even for booleans:
```
.claude/skills/docker-engine-api/SKILL.md view on Meta::CPAN
(`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
Podman serves this API on a compat socket â enable with
`systemctl --user enable --now podman.socket`, reach it at
`unix://$XDG_RUNTIME_DIR/podman/podman.sock`, and it announces API 1.41.
Multi-stage builds including `target` pass through unchanged, and the frame
format above is byte-identical. It is a reimplementation, not Docker: treat
anything beyond the documented surface â event payload details, healthcheck
fields, error message text â as unverified until measured against the engine
.claude/skills/getty-perl-core/SKILL.md view on Meta::CPAN
---
# Perl Core â Getty House Rules
These rules override defaults. They are non-negotiable in Getty projects.
Object-system specifics live in `getty-perl-moo` / `getty-perl-moose`.
## Module loading
- **`use Module;` at the top.** Always. Every dependency loads at compile time.
- **`require` is forbidden as a "lazy optimization".** Never use it to shave startup. `require Foo;` inside a method body â hoist it to a top-level `use`.
- **`require` only for true runtime plugin loading** â the class comes from config/DB at runtime (`Module::Runtime::use_module($class_from_db)`). Known at write-time â `use` it.
- **`require` + `->new` in a controller action** is a red flag. Hoist to `use`.
## strict and warnings
Both are active in **every** file, without exception. How they get there differs:
- **Scripts, tests, plain modules:** `use strict; use warnings;` explicitly.
- **Classes:** `Moo`, `Moose`, `Catalyst` and anything derived from them enable both on import â do not repeat them there.
.claude/skills/getty-perl-core/SKILL.md view on Meta::CPAN
Every distribution ships a `Changes` file with a `{{$NEXT}}` token at the top (Dist::Zilla's `[NextRelease]` fills it at release time).
- **Add a bullet under `{{$NEXT}}` in the SAME commit as any user-facing change** â new bindings, behaviour changes, bug fixes, deprecations. If a CPAN consumer would notice, it belongs there.
- **Match the existing style:** two-space indent, ` - ` bullets, wrap near 78 columns, present-tense imperative ("New binding X", "Fix Y on macOS").
- **One topic, one bullet, one to three lines** â touching an area again rewrites the bullet that is already there instead of adding a second. Wording and length: `getty-git-commit-style`.
- **Skip pure dev-tooling noise** â skill hardlinks, editor config, internal CI refactors. A CI fix that unbreaks the build for everyone IS worth a line.
- **Never hand-edit the version line or timestamp** â `[NextRelease]` owns those.
## Forbidden
â `require Foo` inside a method to "speed up startup" · â a Getty repo's `$VERSION` as a cpanfile requirement · â `'0'` or `'>= x'` as a version argument · â `default => sub {...}` for a non-trivial attribute default · â 4-space indent ...
## When in doubt
Grep hand-written Getty code for how the pattern is used there â the reference is an older repo with no AI commits in its history. Newer repos may show an agent's guess rather than the house rule.
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
```
Creates the board refs inside the current Git repository. With
`--claude-skill`, installs this skill to
`.claude/skills/kanban-issues-karr-cli/SKILL.md`.
Before it writes anything, init asks the remote whether this repository already
has a board there: `git clone` does not fetch `refs/karr/*`, so a fresh clone
looks exactly like a repository that never had one. A remote that advertises
`refs/karr/*` means the board exists and is one `karr sync` away, so init
refuses and says so rather than starting a second board beside it. Every other
answer -- no remote, an unreachable one, no answer inside the probe budget --
lets init through, because it has to work offline. Use `--new-board` only when
a clone is really meant to keep its own, independent board: the two will not
sync with each other, and the board-identity guard is what stops them.
### Create task
```bash
karr create "Title" [--status STATUS] [--priority PRIORITY] [--tags t1,t2] [--body TEXT]
karr create --title "Title" --assignee NAME --due 2026-03-15
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
**A fresh clone fetches the board by itself.** `git clone` does not carry
`refs/karr/*`, so a new checkout holds no board while the whole board sits on
its remote. The read commands (`board`, `list`, `show`, `log`, `context`,
`metrics`, `needs`, and `config show`/`config get`) do not pull as a rule â
only mutating commands do â but where there is nothing under `refs/karr/` at
all and the remote has a board, they fetch it once and answer, with one line
on STDERR (never STDOUT) saying where it came from. Where there is no remote,
or the remote has no board, they still refuse with exit 1 rather than
rendering an empty board: that is the only place `karr init` is the answer. In
a clone whose board is on the remote, `karr init` refuses as well and points
at `karr sync`, so it can no longer start a second, empty board beside the
real one; `karr init --new-board` is the documented way through when an
independent board there really is what you want.
`KARR_NO_AUTO_FETCH=1` switches the fetch off where karr must not touch the
network.
### File view (kanban-md interop)
```bash
karr materialize # refs -> tasks/ + config.yml on disk
karr materialize --force # overwrite git-tracked cards there
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
```bash
karr repair # report what would change
karr repair --yes # migrate
```
Boards written by karr 0.402 or earlier stored UTF-8 double-encoded. Such a
board is detected on read and repaired on the fly, so nothing is broken in the
meantime; this migrates the stored refs once so the workaround stops being
needed. A board created by a later version needs nothing here and says so.
The same command also raises a `started` stamp that precedes its own card's
`created` up to that `created` â karr wrote `started` as a bare date until
ticket #68, which reads as midnight and so lands before a card created later
the same day. A clamped card then asserts zero queue time and no longer
records that its stamp was ever day-granular, so the dry run tells you how
many cards that is before you apply it. It reports, but does not touch,
`completed` stamps with the same day-granular problem.
### Backup and restore
```bash
karr backup > karr-backup.yml
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
### Flow metrics
```bash
karr metrics # throughput, lead/cycle time, efficiency, aging
karr metrics --since 2026-01-01 # only count tasks completed after this date
karr metrics --compact # one line plus one per aging item
karr metrics --json # JSON output
```
Every figure comes from the `created`/`started`/`completed` stamps on the
cards, not from the activity log. Cards whose stamps cannot carry a
measurement â an unreadable date, a `started` that precedes the card's own
`created`, or a `completed` that precedes that `started` â are left out of the
averages that need them and counted in `unusable_timestamps` (cards, not
stamps), so a low sample count is visible rather than silent.
Lead time is the deliberate exception: a `completed` that precedes its own
`created` is still averaged in, negative and all, because every value it could
be clamped to would be an invention. Such samples are counted separately in
`negative_lead_samples` and named in the closing note, so the average is
qualified instead of cleaned â they are *in* the figure, not missing from it,
which is why they are not in `unusable_timestamps`. They come from boards
written before karr 0.403, which stamped `started`/`completed` as a bare
`YYYY-MM-DD` that reads as midnight; on such a board an average printed to the
hour is finer than the data underneath it.
### Agent name
```bash
NAME=$(karr agent-name) # mint once, reuse everywhere
karr pick --claim "$NAME" --move in-progress
karr handoff ID --claim "$NAME" --note "Implementation complete"
```
Revision history for API-Docker
0.004 2026-08-30 03:30:59Z
- The typed object model: `list` and `inspect` on every resource now
return generated `API::Docker::Type::*` objects with snake_case
accessors (`$c->id`, `$c->size_root_fs`) instead of hashrefs. An
unrecognised field is kept and re-sent unchanged rather than dropped,
and `since` is documentation only.
- The hand-written entity classes are gone. Their convenience methods
(`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 `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.
- New `API::Docker::API::Plugins` (`$docker->plugins`): `list`,
`privileges`, `install`, `inspect`, `remove`, `enable`, `disable`,
`upgrade`, `push` and `configure`. Needs a real Docker daemon; Podman
serves no `/plugins`.
- New `API::Docker::API::Secrets` and `API::Docker::API::Configs`: `list`,
key carrying CR/LF could open a header line of its own. Not
reachable from this distribution -- the one caller, `push`, passes
the literal `X-Registry-Auth` -- but the option is public. Names are
rejected rather than stripped, unlike values: a value can pick up a
stray newline honestly (`encode_base64` wraps its output by
default), and flattening it keeps what the caller meant, while a
name is a literal the programmer wrote and rewriting
"X-Foo\r\nX-Bar" into "X-FooX-Bar" would put a header on the wire
under a name nobody asked for. The check also catches spaces and
colons, which corrupt the request without injecting anything.
- `containers->logs` and `exec->start` now demultiplex the Docker
stream format and return an ArrayRef of frames, each a HashRef with
`stream` and `data`:
[ { stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" } ]
Both used to hand the caller the framed bytes, so the 8-byte frame
header of every frame landed inside the log text. Measured against
the rootless Podman socket (5.4.2, API 1.41) with a container
running `echo OUT; echo ERR 1>&2`: without a TTY the body is
`01 00 00 00 00 00 00 04 "OUT\n" 02 00 00 00 00 00 00 04 "ERR\n"`,
and the same exec produces byte-identical output. With a TTY there
is no framing at all -- the body is `"OUT\r\n" "ERR\r\n"` -- which
is why hand-testing interactively never showed the defect. TTY
output comes back as one frame with `stream => 'raw'`, so the shape
never varies and `$_->{stream} eq 'stderr'` is safe on any frame.
Callers wanting plain text use
`join '', map { $_->{data} } @$frames`.
Framing is decided from the response bytes, not from `Content-Type`.
Measured on Podman: `GET /containers/{id}/logs` sends no
`Content-Type` whatsoever, for either kind of container, and
`POST /exec/{id}/start` sends
`application/vnd.docker.raw-stream` for both -- including the
non-TTY exec whose body is in fact multiplexed. Trusting that header
would put frame headers back into the caller's output on that
engine. Instead the body is walked as frames and is only treated as
framed when the walk consumes it exactly; the one way to fool it,
and the `tty => 1` option that overrides it, are documented on
`API::Docker::Role::HTTP::stream_frames`.
`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 --
so `pull` can croak as well as report an error event.
you changed the files and the date of any change; and
b) cause the whole of any work that you distribute or publish, that
in whole or in part contains the Program or any part thereof, either
with or without modifications, to be licensed at no charge to all
third parties under the terms of this General Public License (except
that you may choose to grant warranty protection to some or all
third parties, at your option).
c) If the modified program normally reads commands interactively when
run, you must cause it, when started running for such interactive use
in the simplest and most usual way, to print or display an
announcement including an appropriate copyright notice and a notice
that there is no warranty (or else, saying that you provide a
warranty) and that users may redistribute the program under these
conditions, and telling the user how to view a copy of this General
Public License.
d) You may charge a fee for the physical act of transferring a
copy, and you may at your option offer warranty protection in
exchange for a fee.
END OF TERMS AND CONDITIONS
Appendix: How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to humanity, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.
To do so, attach the following notices to the program. It is safest to
attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) 19yy <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 1, or (at your option)
any later version.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) 19xx name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the
appropriate parts of the General Public License. Of course, the
commands you use may be called something other than `show w' and `show
c'; they could even be mouse-clicks or menu items--whatever suits your
lib/API/Docker/Type/PortBinding.pm
lib/API/Docker/Type/PortStatus.pm
lib/API/Docker/Type/ProcessConfig.pm
lib/API/Docker/Type/ProgressDetail.pm
lib/API/Docker/Type/PushImageInfo.pm
lib/API/Docker/Type/RegistryServiceConfig.pm
lib/API/Docker/Type/ResourceObject.pm
lib/API/Docker/Type/Resources.pm
lib/API/Docker/Type/Resources/BlkioWeightDevice.pm
lib/API/Docker/Type/Resources/Ulimit.pm
lib/API/Docker/Type/RestartPolicy.pm
lib/API/Docker/Type/Runtime.pm
lib/API/Docker/Type/Secret.pm
lib/API/Docker/Type/SecretSpec.pm
lib/API/Docker/Type/Service.pm
lib/API/Docker/Type/Service/Endpoint.pm
lib/API/Docker/Type/Service/Endpoint/VirtualIP.pm
lib/API/Docker/Type/Service/JobStatus.pm
lib/API/Docker/Type/Service/ServiceStatus.pm
lib/API/Docker/Type/Service/UpdateStatus.pm
lib/API/Docker/Type/ServiceCreateResponse.pm
lib/API/Docker/Type/TaskSpec/ContainerSpec/Secret.pm
lib/API/Docker/Type/TaskSpec/ContainerSpec/Secret/File.pm
lib/API/Docker/Type/TaskSpec/ContainerSpec/Ulimit.pm
lib/API/Docker/Type/TaskSpec/LogDriver.pm
lib/API/Docker/Type/TaskSpec/NetworkAttachmentSpec.pm
lib/API/Docker/Type/TaskSpec/Placement.pm
lib/API/Docker/Type/TaskSpec/Placement/Preference.pm
lib/API/Docker/Type/TaskSpec/Placement/Preference/Spread.pm
lib/API/Docker/Type/TaskSpec/PluginSpec.pm
lib/API/Docker/Type/TaskSpec/Resources.pm
lib/API/Docker/Type/TaskSpec/RestartPolicy.pm
lib/API/Docker/Type/TaskStatus.pm
lib/API/Docker/Type/ThrottleDevice.pm
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/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
t/fixtures/containers_logs_multiplexed.bin
t/fixtures/containers_logs_tty.bin
t/fixtures/containers_logs_tty_json.bin
t/fixtures/exec_start_multiplexed.bin
t/fixtures/images_build_error_stream.ndjson
t/fixtures/images_build_quiet_stream.ndjson
t/fixtures/images_build_stream.ndjson
t/fixtures/images_get.tar
t/fixtures/images_list.json
t/fixtures/images_load_stream.ndjson
t/fixtures/images_pull_stream.ndjson
t/fixtures/networks_list.json
t/fixtures/secrets_list.json
t/fixtures/system_events_stream.ndjson
"version" : "0.004"
},
"API::Docker::Type::Resources::BlkioWeightDevice" : {
"file" : "lib/API/Docker/Type/Resources/BlkioWeightDevice.pm",
"version" : "0.004"
},
"API::Docker::Type::Resources::Ulimit" : {
"file" : "lib/API/Docker/Type/Resources/Ulimit.pm",
"version" : "0.004"
},
"API::Docker::Type::RestartPolicy" : {
"file" : "lib/API/Docker/Type/RestartPolicy.pm",
"version" : "0.004"
},
"API::Docker::Type::Runtime" : {
"file" : "lib/API/Docker/Type/Runtime.pm",
"version" : "0.004"
},
"API::Docker::Type::Secret" : {
"file" : "lib/API/Docker/Type/Secret.pm",
"version" : "0.004"
},
"version" : "0.004"
},
"API::Docker::Type::TaskSpec::PluginSpec" : {
"file" : "lib/API/Docker/Type/TaskSpec/PluginSpec.pm",
"version" : "0.004"
},
"API::Docker::Type::TaskSpec::Resources" : {
"file" : "lib/API/Docker/Type/TaskSpec/Resources.pm",
"version" : "0.004"
},
"API::Docker::Type::TaskSpec::RestartPolicy" : {
"file" : "lib/API/Docker/Type/TaskSpec/RestartPolicy.pm",
"version" : "0.004"
},
"API::Docker::Type::TaskStatus" : {
"file" : "lib/API/Docker/Type/TaskStatus.pm",
"version" : "0.004"
},
"API::Docker::Type::ThrottleDevice" : {
"file" : "lib/API/Docker/Type/ThrottleDevice.pm",
"version" : "0.004"
},
version: '0.004'
API::Docker::Type::Resources:
file: lib/API/Docker/Type/Resources.pm
version: '0.004'
API::Docker::Type::Resources::BlkioWeightDevice:
file: lib/API/Docker/Type/Resources/BlkioWeightDevice.pm
version: '0.004'
API::Docker::Type::Resources::Ulimit:
file: lib/API/Docker/Type/Resources/Ulimit.pm
version: '0.004'
API::Docker::Type::RestartPolicy:
file: lib/API/Docker/Type/RestartPolicy.pm
version: '0.004'
API::Docker::Type::Runtime:
file: lib/API/Docker/Type/Runtime.pm
version: '0.004'
API::Docker::Type::Secret:
file: lib/API/Docker/Type/Secret.pm
version: '0.004'
API::Docker::Type::SecretSpec:
file: lib/API/Docker/Type/SecretSpec.pm
version: '0.004'
version: '0.004'
API::Docker::Type::TaskSpec::Placement::Preference::Spread:
file: lib/API/Docker/Type/TaskSpec/Placement/Preference/Spread.pm
version: '0.004'
API::Docker::Type::TaskSpec::PluginSpec:
file: lib/API/Docker/Type/TaskSpec/PluginSpec.pm
version: '0.004'
API::Docker::Type::TaskSpec::Resources:
file: lib/API/Docker/Type/TaskSpec/Resources.pm
version: '0.004'
API::Docker::Type::TaskSpec::RestartPolicy:
file: lib/API/Docker/Type/TaskSpec/RestartPolicy.pm
version: '0.004'
API::Docker::Type::TaskStatus:
file: lib/API/Docker/Type/TaskStatus.pm
version: '0.004'
API::Docker::Type::ThrottleDevice:
file: lib/API/Docker/Type/ThrottleDevice.pm
version: '0.004'
API::Docker::Type::Topology:
file: lib/API/Docker/Type/Topology.pm
version: '0.004'
lib/API/Docker.pm view on Meta::CPAN
my $containers = $docker->containers->list(all => 1);
for my $container (@$containers) {
say $container->id;
say $container->status;
}
my $result = $docker->containers->create(
Image => 'nginx:latest',
name => 'my-nginx',
);
$docker->containers->start($result->{Id});
my $inspected = $docker->containers->inspect($result->{Id});
say $inspected->state->running ? 'running' : 'not running';
# Image operations
$docker->images->pull(fromImage => 'nginx', tag => 'latest');
my $images = $docker->images->list;
# Network and volume management
my $networks = $docker->networks->list;
lib/API/Docker.pm view on Meta::CPAN
=head2 tls_insecure
Turn certificate verification off. Default C<0>. Only read when L</tls> is
set, and named for what it does.
C<< tls_insecure => 1 >> sets C<SSL_VERIFY_NONE> and drops the hostname check,
which leaves a connection encrypted against a passive listener and against
nothing else: whoever answers it chooses the certificate, so anyone able to
redirect the connection reads and rewrites everything on it -- registry
credentials, image contents, the commands containers are started with.
It exists for a self-signed daemon certificate whose CA is not to hand. The
better answer is nearly always L</cert_path>: a self-signed certificate is its
own CA and works as F<ca.pem> directly.
Setting it without L</tls> croaks, rather than being accepted and doing
nothing.
=head2 system
Returns L<API::Docker::API::System> instance for system operations like
C<info>, C<version>, C<ping>, and C<events>.
=head2 containers
Returns L<API::Docker::API::Containers> instance for container operations like
C<list>, C<create>, C<start>, C<stop>, and C<remove>.
=head2 images
Returns L<API::Docker::API::Images> instance for image operations like
C<list>, C<pull>, C<push>, and C<remove>.
=head2 networks
Returns L<API::Docker::API::Networks> instance for network operations like
C<list>, C<create>, C<connect>, and C<disconnect>.
lib/API/Docker.pm view on Meta::CPAN
default is turned off for a run of calls: what C<using> carries is read with
C<exists> rather than for truth, so a C<0> reaches the transport instead of
vanishing into "no opinion".
Three things they do not do:
=over
=item * B<C<read_timeout> 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. So it bounds a daemon that goes quiet -- it does
not bound a long transfer, and it does not bound a stream that keeps sending
without saying anything, which is what C<< containers->stats >> degrades into
on Docker after the container exits.
=item * B<Neither of them bounds writing the request.> Sending the bytes out
is unbounded on every transport. In practice that matters for one thing: a
large C</build> context or C<< images->load >> archive being written to a
daemon that has stopped reading.
lib/API/Docker/API/Containers.pm view on Meta::CPAN
sub inspect {
my ($self, $id) = @_;
croak "Container ID required" unless $id;
my $result = $self->client->get("/containers/$id/json",
%{ $self->_request_options },
);
return $self->_wrap('API::Docker::Type::ContainerInspectResponse', $result);
}
sub start {
my ($self, $id) = @_;
croak "Container ID required" unless $id;
return $self->_state_change("/containers/$id/start",
%{ $self->_request_options },
);
}
sub stop {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{t} = $opts{timeout} if defined $opts{timeout};
$params{signal} = $opts{signal} if defined $opts{signal};
return $self->_state_change("/containers/$id/stop",
params => \%params,
%{ $self->_request_options },
);
}
sub restart {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{t} = $opts{timeout} if defined $opts{timeout};
return $self->_state_change("/containers/$id/restart",
params => \%params,
%{ $self->_request_options },
);
}
sub kill {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
lib/API/Docker/API/Containers.pm view on Meta::CPAN
my ($self, $id, $name) = @_;
croak "Container ID required" unless $id;
croak "New name required" unless $name;
return $self->client->post("/containers/$id/rename", undef,
params => { name => $name },
%{ $self->_request_options },
);
}
# The update body is Resources + RestartPolicy; the booleans are the two
# Resources flags. Normalised on the way out, as for create.
my @UPDATE_BOOLS = qw( Init OomKillDisable );
sub update {
my ($self, $id, %config) = @_;
croak "Container ID required" unless $id;
$self->_json_bools(\%config, @UPDATE_BOOLS);
return $self->client->post("/containers/$id/update", \%config);
}
lib/API/Docker/API/Containers.pm view on Meta::CPAN
my $docker = API::Docker->new;
# List containers
my $containers = $docker->containers->list(all => 1);
for my $container (@$containers) {
say $container->id;
say $container->status;
}
# Create and start a container
my $result = $docker->containers->create(
Image => 'nginx:latest',
name => 'my-nginx',
ExposedPorts => { '80/tcp' => {} },
);
$docker->containers->start($result->{Id});
# Inspect container details
my $container = $docker->containers->inspect($result->{Id});
say $container->name;
# Stop and remove
$docker->containers->stop($result->{Id}, timeout => 10);
$docker->containers->remove($result->{Id});
# View logs (ArrayRef of { stream => 'stdout'|'stderr'|'raw', data => ... })
lib/API/Docker/API/Containers.pm view on Meta::CPAN
my $attached = $docker->containers->attach($result->{Id});
# Copy a file out, and a tar archive in (what docker cp is built on)
my $tar = $docker->containers->get_archive($result->{Id},
path => '/etc/hostname');
$docker->containers->put_archive($result->{Id}, $tar, path => '/tmp');
=head1 DESCRIPTION
This module provides methods for managing Docker containers including creation,
lifecycle operations (start, stop, restart), inspection, logs, and more.
C<list> and C<inspect> return generated L<API::Docker::Type> objects carrying
the convenience methods of L<API::Docker::Role::Entity::Container>, so
C<< $container->start >> and C<< $container->logs >> work on either. Which
class each returns, and where the two disagree, is below.
Accessed via C<< $docker->containers >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->containers->using(read_timeout => 5) >>.
=head2 The two container shapes
The daemon describes a container two ways and the swagger has two
definitions for it, so this class returns two classes:
lib/API/Docker/API/Containers.pm view on Meta::CPAN
=item * C<< ->labels >> and C<< ->ports >> are top-level on a summary only.
An inspect carries the labels under C<< ->config->labels >> and the port
bindings under C<< ->network_settings->ports >>, which is a map of container
port to host bindings rather than the summary's ArrayRef of
L<API::Docker::Type::Port>.
=item * C<< ->names >> (an ArrayRef, each with a leading C</>) is the
summary's; C<< ->name >> (one string, also with the C</>) is the inspect's.
=item * C<< ->config >>, C<< ->restart_count >>, C<< ->driver >>,
C<< ->platform >>, C<< ->graph_driver >>, C<< ->exec_ids >> and the
C<*_path> fields come from an inspect only.
=item * C<< ->host_config >> and C<< ->network_settings >> exist on both and
are B<different classes>: the summary's are
L<API::Docker::Type::ContainerSummary::HostConfig> (C<NetworkMode> and
C<Annotations>, nothing else) and
L<API::Docker::Type::ContainerSummary::NetworkSettings> (C<Networks> alone),
against the full L<API::Docker::Type::HostConfig> and
L<API::Docker::Type::NetworkSettings> on an inspect.
lib/API/Docker/API/Containers.pm view on Meta::CPAN
C<OomKillDisable>).
=head2 inspect
my $container = $containers->inspect($id);
Get detailed information about a container. Returns an
L<API::Docker::Type::ContainerInspectResponse> -- see L</"The two container
shapes">.
=head2 start
$containers->start($id);
say 'was already running' unless $containers->start($id);
Start a container. Returns 1 when the container was started and 0 when it was
already running: the engine answers a state change with 204 and a no-op with
B<304 Not Modified>, and both carry an empty body, so until now both came back
as C<undef>.
The no-op keeps the falsy value this method always returned -- 0 where it used
to be C<undef> -- so a caller that ignores the return or tests it for falseness
is unaffected; only a caller testing C<defined> sees a difference. A failure is
still a croak, never a 0.
=head2 stop
$containers->stop($id, timeout => 10);
say 'was already stopped' unless $containers->stop($id);
Stop a container. Returns 1 when the container was stopped and 0 when it was
already stopped -- the engine answers the no-op with B<304 Not Modified>. See
L</start> for what that 0 replaces.
Options:
=over
=item * C<timeout> - Seconds to wait before killing (default 10)
=item * C<signal> - Signal to send (default SIGTERM)
=back
=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>.
Returns nothing -- unlike L</start>, L</stop>, L</restart>, L</pause> and
L</unpause>, which report 1/0 through their shared C<_state_change> path.
Those methods have two outcomes worth telling apart: a change (204) and a
no-op (304, where the engine sends one). C<kill> has only one, because
C<_request> croaks on any C<< status >= 400 >>, so the B<409> a non-running
container gets back never reaches this method's C<return>. A boolean with a
single possible value is not worth adding.
More importantly, B<204 does not mean the container stopped.> Measured on
B<both> engines -- Docker 29.7.2 (API 1.55) and rootless Podman 5.4.2 (API
1.41), same machine, identical behavior: sending a signal the container traps
lib/API/Docker/API/Containers.pm view on Meta::CPAN
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
=item * A container that was B<created and never started> answers immediately
with an invented one: Docker C<< StatusCode => 0 >>, Podman
C<< StatusCode => -1 >>. There is no exit status to report and the two
engines make up different ones, so a C<0> from this call is not proof that
anything ran
=item * C<< condition => 'next-exit' >> and C<< condition => 'removed' >>
against an exited container B<block> on both engines: the awaited event is in
the future and may never happen
=item * An unrecognised condition croaks B<400> on both (Docker C<invalid
lib/API/Docker/API/Containers.pm view on Meta::CPAN
default), C<next-exit> or C<removed>. Sent only when given
=back
=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>.
=head2 rename
$containers->rename($id, 'new-name');
Rename a container.
=head2 update
lib/API/Docker/API/Exec.pm view on Meta::CPAN
sub create {
my ($self, $container_id, %config) = @_;
croak "Container ID required" unless $container_id;
croak "Cmd required" unless $config{Cmd};
$self->_json_bools(\%config, @EXEC_CONFIG_BOOLS);
return $self->client->post("/containers/$container_id/exec", \%config);
}
sub start {
my ($self, $exec_id, %opts) = @_;
croak "Exec ID required" unless $exec_id;
my $body = {
Detach => $opts{Detach} ? \1 : \0,
Tty => $opts{Tty} ? \1 : \0,
};
# exists, not truth: an unset callback is a caller bug, and falling back to
# the buffered path for it would answer a long-running command by waiting
# for it in silence. Handed over as it is, the transport says so instead.
return $self->client->stream_frames('POST', "/exec/$exec_id/start",
body => $body,
$opts{Tty} ? ( tty => 1 ) : (),
%{ $self->_request_options },
exists $opts{on_frame} ? ( on_frame => $opts{on_frame} ) : (),
);
}
sub resize {
my ($self, $exec_id, %opts) = @_;
lib/API/Docker/API/Exec.pm view on Meta::CPAN
my $docker = API::Docker->new;
# Create an exec instance
my $exec = $docker->exec->create($container_id,
Cmd => ['/bin/sh', '-c', 'echo hello'],
AttachStdout => 1,
AttachStderr => 1,
);
# Start the exec -- ArrayRef of { stream => ..., data => ... } frames
my $frames = $docker->exec->start($exec->{Id});
my $output = join '', map { $_->{data} } @$frames;
# The exit status comes from a separate call
my $exit = $docker->exec->inspect($exec->{Id})->{ExitCode};
# Inspect exec instance
my $info = $docker->exec->inspect($exec->{Id});
=head1 DESCRIPTION
lib/API/Docker/API/Exec.pm view on Meta::CPAN
Common config keys: C<AttachStdin>, C<AttachStdout>, C<AttachStderr>, C<Tty>,
C<Env>, C<User>, C<WorkingDir>.
The boolean flags (C<AttachStdin>, C<AttachStdout>, C<AttachStderr>, C<Tty>,
C<Privileged>) may be given as a Perl C<1>/C<0> or as a JSON boolean; either
goes out as a real JSON C<true>/C<false>, which the engine's body type-check
requires. Passing C<1> where the daemon wants a boolean would otherwise be
rejected.
=head2 start
my $frames = $exec->start($exec_id, Detach => 0);
my $output = join '', map { $_->{data} } @$frames;
Start an exec instance. Returns an ArrayRef of frames in the same shape as
L<API::Docker::API::Containers/logs>:
[ { stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" } ]
An exec instance created without a TTY multiplexes stdout and stderr into one
framed stream, which this method demultiplexes. One created with a TTY has no
frame headers and its output arrives as a single C<< stream => 'raw' >> frame.
A detached start produces no output, so it returns an empty ArrayRef.
The exit status is B<not> part of this response. It comes from a separate call
once the exec has finished:
my $exit = $exec->inspect($exec_id)->{ExitCode};
Options:
=over
lib/API/Docker/API/Exec.pm view on Meta::CPAN
=back
=head2 Watching the output as it is produced
Without a callback this returns when the command has finished and the daemon
has closed the stream -- a command that runs for a minute is a minute of
silence, and one that never finishes never returns. Pass C<on_frame> and the
frames are handed over as they arrive:
my $summary = $exec->start($exec_id,
on_frame => sub {
my ($frame, $stop) = @_;
print $frame->{data};
$stop->() if $frame->{data} =~ /ready/;
},
);
$summary; # { delivered => 9, stopped => 1 }
With a callback the return value is that summary HashRef, not the frames:
C<delivered> is how many went to the callback, C<stopped> is 1 when the
callback ended the stream and 0 when the daemon did. Nothing is accumulated,
so joining the output is the callback's job. See
L<API::Docker::Role::HTTP/"Streaming a response as it arrives">.
A detached start produces no output, so its summary is
C<< { delivered => 0, stopped => 0 } >> where the buffered call returns an
empty ArrayRef.
C<Tty> means something stronger on this path. The buffered path decides
framing by walking the whole body, which is exactly what a streamed one does
not have; so with C<on_frame> it is a promise about the exec instance rather
than a hint, and an undeclared stream that turns out not to be framed croaks
instead of being handed back raw. Pass the same C<Tty> that went to L</create>
-- the engine expects them to agree in any case.
lib/API/Docker/Container.pm view on Meta::CPAN
# anything, and xt/release/changes_has_content.t, which only reads Changes.
my $REFUSED =
__PACKAGE__ . ' was removed in API::Docker 0.004 and this file is a stub'
. ' with nothing in it: it ships only so that installing this'
. ' release overwrites the working copy an earlier one left on'
. ' disk. You have not hit a fault in the distribution. The'
. ' containers the daemon answers with are'
. ' API::Docker::Type::ContainerSummary (containers->list) and'
. ' API::Docker::Type::ContainerInspectResponse'
. ' (containers->inspect), with the field names the swagger\'s'
. ' own in snake_case, and start, stop, logs, is_running and the'
. ' rest are unchanged on them, composed in from'
. ' API::Docker::Role::Entity::Container. This stub refuses';
# The croak below is what a caller normally hits. AUTOLOAD is for the one who
# swallowed it -- eval { require API::Docker::Container } and then called a
# method anyway; the answer has to be the same one, not a bare "Can't locate
# object method". DESTROY is defined so it does not reach AUTOLOAD.
sub AUTOLOAD { croak $REFUSED }
sub DESTROY { }
lib/API/Docker/Container.pm view on Meta::CPAN
the file is still in the distribution.
What to reach for instead:
=over
=item * L<API::Docker::Type::ContainerSummary> -- what C<< containers->list >> returns
=item * L<API::Docker::Type::ContainerInspectResponse> -- what C<< containers->inspect >> returns
=item * L<API::Docker::Role::Entity::Container> -- start, stop, logs, is_running and the rest,
unchanged, composed into the above at load time
=back
Where this class mirrored the daemon's CamelCase verbatim, the generated
classes carry the swagger's own names in snake_case.
L<API::Docker::API::Containers> documents the shape each method returns.
=head1 SEE ALSO
lib/API/Docker/Error/HTTP.pm view on Meta::CPAN
package API::Docker::Error::HTTP;
# ABSTRACT: Error status returned by the Docker Engine on the status line
our $VERSION = '0.004';
use Moo;
# namespace::clean has to come BEFORE "use overload" here, not after it as
# everywhere else in this distribution -- same reason as in
# API::Docker::Error::Stream. It sweeps the symbols `overload` installs --
# the `("" ` slot among them -- so with the two lines in the house order the
# class ends up not overloaded at all and stringifies as
# API::Docker::Error::HTTP=HASH(0x...). Nothing dies when that happens: every
# caller that only inspects $@ as a string silently starts seeing a reference
# address instead of the reason, and this is the exception every resource
# method in the distribution can raise. Measured, not assumed:
# overload::Overloaded($err) is false with the lines swapped.
use namespace::clean;
use overload
'""' => sub { $_[0]->as_string },
'bool' => sub { 1 },
fallback => 1;
lib/API/Docker/Error/HTTP.pm view on Meta::CPAN
the POD of the streaming methods still says to inspect C<$@> as a string.
Two exception classes reach a caller and which one it is depends on the
engine: a failure the daemon decides before it commits to a status arrives
here, while one it decides after arrives as an L<API::Docker::Error::Stream>
inside a stream that was already answered with HTTP 200. C<< ->status >> is
the extra for a caller that has already established it is holding one of
these, not the new recommended way to detect failure.
The C<response> out-parameter of L<API::Docker::Role::HTTP/get> is untouched
by this class and is not superseded by it: it is the only way to the status of
a request that did B<not> fail -- a C<304 Not Modified> from starting an
already-running container, or the C<X-Docker-Container-Path-Stat> header a
successful C<HEAD> carries its whole payload in.
=head2 message
The reason on its own, without the location suffix: the same
C<Docker API error (STATUS): REASON> text the transport croaked before this
class existed, where C<REASON> is the engine's C<message> field, its
C<errorDetail.message>, its flat C<error> key or the raw body, in that order
of preference.
lib/API/Docker/Error/Stream.pm view on Meta::CPAN
package API::Docker::Error::Stream;
# ABSTRACT: Failure reported inside a Docker Engine progress stream
our $VERSION = '0.004';
use Moo;
# namespace::clean has to come BEFORE "use overload" here, not after it as
# everywhere else in this distribution. It sweeps the symbols `overload`
# installs -- the `("" ` slot among them -- so with the two lines in the
# house order the class ends up not overloaded at all and stringifies as
# API::Docker::Error::Stream=HASH(0x...). Nothing dies when that happens:
# every caller that only inspects $@ as a string silently starts seeing a
# reference address instead of the reason. Measured, not assumed:
# overload::Overloaded($err) is false with the lines swapped.
use namespace::clean;
use overload
'""' => sub { $_[0]->as_string },
'bool' => sub { 1 },
fallback => 1;
has message => (
lib/API/Docker/Error/Timeout.pm view on Meta::CPAN
package API::Docker::Error::Timeout;
# ABSTRACT: Read timeout while waiting for the Docker Engine
our $VERSION = '0.004';
use Moo;
# namespace::clean has to come BEFORE "use overload" here, not after it as
# everywhere else in this distribution -- same reason as in
# API::Docker::Error::Stream and API::Docker::Error::HTTP. It sweeps the
# symbols `overload` installs -- the `("" ` slot among them -- so with the two
# lines in the house order the class ends up not overloaded at all and
# stringifies as API::Docker::Error::Timeout=HASH(0x...). Nothing dies when
# that happens: every caller that only inspects $@ as a string silently starts
# seeing a reference address instead of the reason. Measured, not assumed:
# overload::Overloaded($err) is false with the lines swapped.
use namespace::clean;
use overload
'""' => sub { $_[0]->as_string },
'bool' => sub { 1 },
fallback => 1;
has message => (
lib/API/Docker/Error/Truncated.pm view on Meta::CPAN
# ABSTRACT: The daemon closed before the response it announced was complete
our $VERSION = '0.004';
use Moo;
# namespace::clean has to come BEFORE "use overload" here, not after it as
# everywhere else in this distribution -- same reason as in
# API::Docker::Error::Stream, API::Docker::Error::HTTP and
# API::Docker::Error::Timeout. It sweeps the symbols `overload` installs --
# the `("" ` slot among them -- so with the two lines in the house order the
# class ends up not overloaded at all and stringifies as
# API::Docker::Error::Truncated=HASH(0x...). Nothing dies when that happens:
# every caller that only inspects $@ as a string silently starts seeing a
# reference address instead of the reason. Measured, not assumed:
# overload::Overloaded($err) is false with the lines swapped.
use namespace::clean;
use overload
'""' => sub { $_[0]->as_string },
'bool' => sub { 1 },
fallback => 1;
has message => (
lib/API/Docker/Error/Truncated.pm view on Meta::CPAN
It is a structural check, not a heuristic, and it asks one of two questions
depending on how the piece is delimited. Where the response announced a length
it compares what arrived against it. Where the framing is by terminator
instead -- the head, and the chunk headers -- it asks whether the terminator
came before the stream ended, which needs nothing to compare and is just as
decidable. Neither is a guess about content: a header block that never closed
is not a short one, it is an unfinished one.
A body delimited by nothing but the close -- C<attach>,
C<< logs(follow => 1) >>, C</exec/{id}/start>, the whole
C<application/vnd.docker.raw-stream> family -- announces no end and has no
terminator either, so there an EOF B<is> the end and this is never raised.
Its B<head> is framed like any other, and is checked like any other.
=head2 Why it is fatal
For the same reason L<API::Docker::Error::Timeout> is, and the two are the
same defect reached by different routes: a short body satisfies every return
shape this role promises and is indistinguishable from a complete one.
C<ndjson> promises an ArrayRef of events and gets a shorter one; C<raw>
lib/API/Docker/Error/Truncated.pm view on Meta::CPAN
=item * C<'chunk-data'> - the stream ended inside a chunk, short of the size
that chunk's own header announced
=item * C<'chunk-terminator'> - a chunk's data arrived in full and the CRLF
that ends it did not
=back
Informational rather than something to branch on: every value means the same
thing to a caller, which is that the response is incomplete. It is here
because "which of the four" is the first question when a real engine starts
raising this, and reading it off the object beats parsing L</message>.
=head2 expected
The byte count the framing announced for the piece that was cut short: the
C<Content-Length> for C<'content-length'>, the chunk's own size for
C<'chunk-data'>. C<undef> for the four phases with no announcement to fall
short of, which are the ones framed by a terminator instead.
=head2 received
lib/API/Docker/Role/Entity.pm view on Meta::CPAN
=head1 SYNOPSIS
package API::Docker::Role::Entity::Container;
use Moo::Role;
with 'API::Docker::Role::Entity';
requires 'id';
use API::Docker::Type::ContainerSummary;
use namespace::clean;
sub start {
my ($self) = @_;
return $self->client->containers->start($self->id);
}
# at the bottom of the same file: the methods land on the generated class
Moo::Role->apply_roles_to_package(
'API::Docker::Type::ContainerSummary', __PACKAGE__);
=head1 DESCRIPTION
An entity is a generated L<API::Docker::Type> class that has been given the
convenience methods of its resource -- C<< $container->start >>,
C<< $container->logs >>, C<< $image->remove >>. The methods live in a role
that is applied to the generated class at load time; they are never written
into the generated file.
=head2 Why the methods are not in the generated class
They cannot be. C<maint/spec-to-type.pl --verify> renders every class under
C<lib/API/Docker/Type/> out of C<spec/v1.51.yaml> and requires the result to
match what is shipped B<byte for byte> (F<t/spec_to_type.t>), and the
generator refuses to overwrite a file that exists. A hand-added C<with> line
lib/API/Docker/Role/Entity/Container.pm view on Meta::CPAN
with 'API::Docker::Role::Entity';
requires 'id';
use API::Docker::Type::ContainerInspectResponse;
use API::Docker::Type::ContainerSummary;
use Carp qw( croak );
use Package::Stash;
use Scalar::Util qw( blessed );
use namespace::clean;
sub start {
my ($self) = @_;
return $self->client->containers->start($self->id);
}
sub stop {
my ($self, %opts) = @_;
return $self->client->containers->stop($self->id, %opts);
}
sub restart {
my ($self, %opts) = @_;
return $self->client->containers->restart($self->id, %opts);
}
sub kill {
my ($self, %opts) = @_;
return $self->client->containers->kill($self->id, %opts);
}
sub remove {
lib/API/Docker/Role/Entity/Container.pm view on Meta::CPAN
return $state->running ? 1 : 0 if blessed $state;
return lc($state) eq 'running' ? 1 : 0;
}
# --- composition -----------------------------------------------------------
#
# Here rather than in API::Docker::API::Containers, which is the other
# candidate: loading this role is then what puts the methods on the classes,
# and there is no program in which an API::Docker::Type::ContainerSummary has
# ->start and another in which it does not, depending on which module was
# loaded first.
#
# The clash check is not decoration. Moo composes a role into a class the
# class-wins way, so a generated accessor of the same name as a method here
# would silently keep its place and the method would be missing -- and the
# generated classes are written from a specification that grows fields
# without asking. None of the 19 names collides today; a future one says so
# on the first `use`.
{
my @provided = Package::Stash->new(__PACKAGE__)->list_all_symbols('CODE');
lib/API/Docker/Role/Entity/Container.pm view on Meta::CPAN
my $docker = API::Docker->new;
# from list: an API::Docker::Type::ContainerSummary
my ($container) = @{ $docker->containers->list };
say $container->id;
say $container->status; # "Up 2 hours"
say $container->state; # "running"
$container->start;
$container->stop(timeout => 10);
my $logs = $container->logs(tail => 100);
$container->remove(force => 1);
# from inspect: an API::Docker::Type::ContainerInspectResponse, where
# the same methods work and `state` is an object
my $full = $docker->containers->inspect($container->id);
say $full->state->status;
say $full->state->exit_code;
lib/API/Docker/Role/Entity/Container.pm view on Meta::CPAN
place to correct when the engine's are found to be something else.
The fields differ between the two classes -- see
L<API::Docker::API::Containers/"The two container shapes"> for the
differences that have bitten. L</is_running> is the one method that reads
both shapes.
Why the methods are a role applied to generated classes rather than a class
of their own: L<API::Docker::Role::Entity/DESCRIPTION>.
=head2 start
$container->start;
say 'was already running' unless $container->start;
Start the container. Returns 1 when it was started and 0 when it was already
running. Delegates to L<API::Docker::API::Containers/start>, which documents
what that 0 replaces.
=head2 stop
$container->stop(timeout => 10);
Stop the container. Returns 1 when it was stopped and 0 when it was already
stopped. Delegates to L<API::Docker::API::Containers/stop>.
=head2 restart
$container->restart;
Restart the container. Returns 1/0 as L<API::Docker::API::Containers/restart>
does; no engine measured here answers a restart with 304, so it is 1.
=head2 kill
$container->kill(signal => 'SIGTERM');
Send a signal to the container.
=head2 remove
$container->remove(force => 1);
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# path character set -- unreserved, the sub-delims, and : @ % / -- and
# rejected, not sanitised, for the reason a header name is (see
# _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;
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
# -- there is no supported way to take them back out; ungetc is layer-
# dependent, seek does not work on a socket, and select/MSG_PEEK see the
# kernel's buffer rather than PerlIO's. So switching only the body reads to
# sysread would have silently dropped the start of every body. Either all read
# sites move together or none do.
#
# The buffer lives on the handle rather than on the client or in the context:
# it is the unconsumed bytes of *that* handle, its lifetime is the handle's,
# and a client that opens a socket per request therefore has nothing to reset.
# ${*$sock}{...} is the IO::Socket idiom for exactly this and was measured to
# work on a real socket, a lexical filehandle, a bareword glob and a tied
# handle alike.
my $RBUF = __PACKAGE__ . '/rbuf';
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
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# 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 };
}
return $body;
}
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# exactly the close-delimited branch of _read_body, where an EOF is the
# legitimate end and nothing looks wrong. Measured over a socketpair whose
# peer writes "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nX-Half"
# and closes: status 200, one header, an empty body, no complaint. The cuts
# that did get caught were caught by accident one level lower, because the
# half-arrived header happened to be one of those two.
#
# Nothing legitimate ends a head without the blank line, which was measured
# rather than taken from the RFC, on both engines and including the two heads
# an engine writes by hand instead of through its HTTP server: attach and
# /exec/{id}/start answer with "HTTP/1.1 200 OK", one Content-Type line and
# the blank line, on Docker 29.7.2 and on rootless Podman 5.8.4 alike. So do
# 204, 304, HEAD, chunked and every other shape either of them produces.
sub _assert_status_line {
my ($self, $ctx, $line) = @_;
# Only the unterminated half: a status line that never started at all is the
# croak above, which says something better than this could.
$self->_croak_truncated($ctx, phase => 'status-line',
detail => 'the stream ended inside the status line, after '
. length($line) . ' byte' . (length($line) == 1 ? '' : 's') . ' of one')
unless $line =~ /\n\z/;
# Terminated, and now: is it an HTTP status line at all? RFC 9112 section 4:
# HTTP-version SP status-code SP [ reason-phrase ], with status-code exactly
# three digits. A line that arrived whole but is not this shape -- a proxy's
# plain-text banner, an ICY greeting, an HTML error page -- would otherwise
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# it is a loop over the same primitive as the other two branches now, which
# is what karr k60 needed and what the timeout wanted anyway -- with $/ undef
# a whole body and a truncated one are both just bytes, so the slurp's own
# result could never say which it was. This is the path karr k52's hang is
# on, an attach whose buffered frames arrive and whose socket then never
# closes.
#
# And the one shape with no completeness check to make: the response
# announced no end, so the close IS the end (karr k64). Treating an EOF here
# as truncation would make every attach, every logs(follow) and every
# exec/start fail on the daemon hanging up, which is how all three finish.
my $body = '';
# What a timeout hands over instead of dropping; see the content-length
# branch above.
local $ctx->{partial} = \$body;
while (1) {
my ($n, $buf) = $self->_read_bytes($sock, $READ_SIZE, $ctx);
last unless $n;
$body .= $buf;
}
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# 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;
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
that -- which is the intended outcome, not a silent downgrade. Point
C<cert_path> at the directory holding its F<ca.pem> and it verifies.
=head3 Turning verification off
C<< tls_insecure => 1 >>, and the name is the whole of the warning. It sets
C<SSL_VERIFY_NONE> and switches the hostname check off, which leaves a
connection that is encrypted against a passive listener and against nothing
else: whoever answers chooses the certificate, so anyone able to redirect the
connection reads and rewrites everything on it -- registry credentials,
image contents, the commands containers are started with.
It exists for a self-signed daemon certificate whose CA is genuinely not to
hand. The better answer to that is nearly always F<ca.pem>: a self-signed
certificate is its own CA and can be used as the anchor directly.
=head3 The dependency
L<IO::Socket::SSL> is a B<recommended>, not a required, dependency, and it is
loaded at the moment the first TLS connection is opened. It brings in
L<Net::SSLeay>, which is XS compiled against libssl, and the C<unix://>
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
going (karr k59).
L</read_timeout> bounds that:
# Give up after two seconds of silence rather than waiting forever.
my $frames = $docker->containers->using(read_timeout => 2)->attach($id);
=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
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.
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
applies after a C<< $stop->() >>, which leaves a partial unit in the buffer by
construction.
=head2 Reading the status line and the response headers
The return value is the decoded body and nothing else, which leaves two things
the engine said unreachable: the status code, and the response headers. Pass a
HashRef as C<response> to get them:
my %res;
my $data = $client->post("/containers/$id/start", undef,
response => \%res);
$res{status}; # 204
$res{reason}; # 'No Content'
$res{headers}{'api-version'}; # header names are lowercased
The hash is overwritten on every call and filled B<before> the C<< >= 400 >>
croak, so a caller that wraps the request in C<eval> can still read the status
of a failed one. The return value is unaffected, so passing C<response> never
changes what a method hands back.
Two things need it. The engine answers a state change that did nothing with
B<304 Not Modified> -- starting a running container, stopping a stopped one --
which carries no body, exactly like the 204 of a change that did happen; see
L<API::Docker::API::Containers/start>. And C<< HEAD /containers/{id}/archive >>
carries its whole payload in the C<X-Docker-Container-Path-Stat> header, with
no body to return at all.
=head2 Failure on the status line
A status of 400 or above croaks with an L<API::Docker::Error::HTTP>. The
message is the engine's C<message> field, its C<errorDetail.message>, its flat
C<error> key or the raw body, in that order of preference, wrapped as
C<Docker API error (STATUS): REASON> -- the same text this croak has always
carried, and the object stringifies to it byte for byte, Carp's location
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
a value.
The one thing here that is B<not> raised as an object: a connection that
closed without a single byte of a status line still croaks with the plain
C<No response from Docker daemon> string it always has. Nothing about it was
ever silent, and it is a message callers may be matching on.
=head3 Where an end of stream is still the end
A body delimited by nothing but the close. C<attach>,
C<< logs(follow => 1) >>, C</exec/{id}/start> -- the whole
C<application/vnd.docker.raw-stream> family -- carry neither a
C<Content-Length> nor chunked encoding, so the response announces no end and
there is nothing for a short one to be short of. That is how every one of them
finishes, and treating it as truncation would break all of them.
Their B<heads> are another matter and are checked like every other head. An
engine writes those two by hand rather than through its HTTP server, so it is
worth saying that they are well-formed: both answer with C<HTTP/1.1 200 OK>, a
single C<Content-Type> line and the blank line, measured on Docker 29.7.2 and
on rootless Podman 5.8.4. So does every other shape either of them produces --
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
=head2 A request path is rejected, not sanitised
The C<$path> given to L</get>, L</post>, L</put>, L</delete_request>, L</head>
and C<_request> is spliced straight into the request line as
C<< $method /v$version$path HTTP/1.1 >>, and it carries caller data: the
resource methods build it by interpolation -- C<< "/containers/$id/json" >>,
C<< "/images/$name/push" >> -- so a container name or an image reference the
user typed ends up in the request line unescaped. A byte the line's own
grammar reads therefore rewrites the request rather than naming a resource: a
CR or LF ends the line and opens a header of its own, a space starts the
HTTP-version field, and a C<?> or C<#> opens the query string or fragment.
So the path is checked against the RFC 3986 origin-form character set --
unreserved, the sub-delims, and C<:> C<@> C<%> C<< / >>, which is the set an
image reference lives in -- and a path outside it is refused with a croak
before anything reaches the wire, the same treatment and for the same reason a
header name gets. Sanitising is not on the table here: percent-encoding the
path at this layer cannot tell a separator from data, so it would either
mangle every C<< / >> and C<:> or leave the injection open. Query parameters
belong in C<params>, which is assembled separately and runs each element
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
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:
my $text = join '', map { $_->{data} } @$frames;
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
A container created without a TTY produces the Docker stream format -- an
8-byte header per frame (byte 0 the stream type, bytes 4-7 a big-endian uint32
payload length) followed by that many payload bytes. With a TTY there is no
header and the payload is raw pty output.
The engine is supposed to distinguish the two with the response C<Content-Type>
(C<application/vnd.docker.multiplexed-stream> against
C<application/vnd.docker.raw-stream>), but that signal is not dependable.
Measured against Podman 5.4.2 (API 1.41): C<< GET /containers/{id}/logs >>
sends no C<Content-Type> at all, for either kind of container, and
C<< POST /exec/{id}/start >> sends C<application/vnd.docker.raw-stream> for
both -- including the non-TTY exec whose body is in fact multiplexed. Trusting
the header would therefore hand frame headers to the caller on that engine.
The framing is decided from the bytes instead. The body is walked as frames:
each header must have a stream type of 0, 1 or 2, three zero bytes after it,
and a payload length that leaves at least that many bytes in the buffer. The
body is treated as framed only when the walk consumes it exactly and yields at
least one frame; anything else is returned as a single C<raw> frame.
This can be fooled in one direction only. Raw TTY output is misread as framed
lib/API/Docker/Role/JSONBody.pm view on Meta::CPAN
C<boolean> must arrive as a JSON C<true>/C<false>, and a number in its place is
rejected outright. Measured non-mutating against Docker 29.7.2 (API 1.55),
C<< POST /containers/no-such/exec >> with C<< {"Cmd":["true"],"AttachStdout":1}
>> answers C<400> C<json: cannot unmarshal number into Go struct field ... of
type bool>; Podman 5.8.4 (compat API 1.44) answers C<500> with the same Go
message. The query string is not type-checked, which is why C<1>/C<0> is right
there and wrong here.
Every resource API that forwards a caller HashRef as a JSON body therefore
normalises its own boolean keys on the way out, the same C<\1>/C<\0> encoding
L<API::Docker::API::Exec/start> already used. Which keys are boolean is the
swagger's answer and belongs to each method (the sets are declared beside the
call); this role carries only the mechanical coercion they share.
=head2 _json_bools
$self->_json_bools(\%body, qw( Tty OpenStdin AttachStdout ));
Coerce the named keys of C<$hash> in place to JSON booleans and return the same
HashRef. A key that is absent is left alone (so an unset option sends nothing),
and a value that is already a reference -- a C<\1>/C<\0> or a
lib/API/Docker/Role/Type.pm view on Meta::CPAN
sub from_data {
my ($class, $data, %extra) = @_;
$class = ref($class) if ref($class);
croak __PACKAGE__ . '->from_data needs a HashRef'
unless ref $data eq 'HASH';
my $reg = $class->_docker_attr_registry;
my $wire = $class->_docker_wire_index;
# Lifted out of the loop rather than handled in it, so that what the loop
# files never depends on the order the keys came out in -- the same reason
# the BUILDARGS above deletes them before it starts.
my %args;
my %unknown = %{ $data->{unknown_fields} || {} };
my %rejected = %{ $data->{rejected_fields} || {} };
# In effect for the coercions _fits runs below as well as for the
# constructor at the end: a nested hashref is part of the same response
# and has to be read as one.
local $RESPONSE = 1;
for my $key (keys %$data) {
next if $key eq 'unknown_fields' || $key eq 'rejected_fields';
my $attr = $wire->{$key};
lib/API/Docker/Role/Type.pm view on Meta::CPAN
fields with a lowercase first letter, so a lowercase key off an engine is
ordinary rather than exotic, and reading C<id> as the Perl name of C<Id> both
loses the field it really was and rewrites one we did know (karr k85).
The same split decides what happens to a value that does not fit its declared
type. L</from_data> keeps it -- unset attribute, raw value in
L</unknown_fields>, name in L</rejected_fields> -- because one divergent field
must not make the rest of a usable response unreachable. L</new> croaks,
because there the value is the caller's and a mistake worth stopping on.
A nested hashref follows whichever entry point started the construction, so
one object graph is read one way throughout.
=head2 unknown_fields
A HashRef of everything that reached this object under a name the model could
not translate, kept under the name it arrived with and handed back out by
L</TO_JSON> unchanged. Two things land here: a name the registry does not know
at all, and -- on the response path only -- a known wire name whose value did
not fit the type the swagger declares for it. L</rejected_fields> is what
tells the two apart.
lib/API/Docker/Type.pm view on Meta::CPAN
# the second and later DSL calls are a cheap flag check.
sub _ensure_role {
my ($target) = @_;
return if $CLASS_SUGAR{$target}{role_composed};
$CLASS_SUGAR{$target}{role_composed} = 1;
Moo::Role->apply_roles_to_package($target, 'API::Docker::Role::Type');
return;
}
# The one place a short class name becomes a full one. 'Mount' is
# API::Docker::Type::Mount; a name that already starts with the prefix is
# left alone; a leading + means "this is the full name, take it as it is".
# Docker's definitions are flat -- there are no groups to map, which is why
# there is no prefix table here and only this one rule.
sub _expand_class {
my ($short) = @_;
return substr($short, 1) if $short =~ /\A\+/;
return $short if $short =~ /\AAPI::Docker::Type::/;
return 'API::Docker::Type::' . $short;
}
lib/API/Docker/Type.pm view on Meta::CPAN
$value = $$value;
croak __PACKAGE__ . ': a Bool scalar ref dereferenced to another reference ('
. ref($value) . '), not a boolean' if ref $value;
}
return undef unless defined $value;
return 0 if lc($value) eq 'false';
return $value ? 1 : 0;
}
# A hashref handed to an object-typed field is inflated the way the entry
# point that started the construction reads keys: through from_data while an
# engine response is being inflated, through new otherwise. So a nested
# literal in a request a caller assembled takes both spellings, and a nested
# object in a daemon response resolves wire names only -- the same
# distinction the two entry points draw at the top level, carried one level
# down. The class is loaded on first use rather than at declaration time: the
# registry is the only place its name appears, and loading it while the
# declaring class is still compiling would close a cycle the moment two
# definitions reference each other.
sub _coerce_for {
my ($d) = @_;
lib/API/Docker/Type/ClusterVolumeSpec/AccessMode.pm view on Meta::CPAN
=over 4
=item * C<active> The volume is fully available for scheduling on the
cluster
=item * C<pause> No new workloads should use the volume, but existing
workloads are not stopped.
=item * C<drain> All workloads using this volume should be stopped and
rescheduled, and no new ones should be started.
=back
The daemon defaults it to active.
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
lib/API/Docker/Type/ContainerConfig.pm view on Meta::CPAN
The hostname to use for the container, as a valid RFC 1123 hostname.
=head2 domainname
The domain name to use for the container.
=head2 user
Commands run as this user inside the container. If omitted, commands run as
the user specified in the image the container was started from.
Can be either user-name or UID, and optional group-name or GID, separated by
a colon (C<< <user-name|UID>[<:group-name|GID>] >>).
=head2 attach_stdin
Whether to attach to C<stdin>. The daemon defaults it to false.
=head2 attach_stdout
lib/API/Docker/Type/ContainerInspectResponse.pm view on Meta::CPAN
docker hosts_path => Str, since => '1.51';
docker log_path => Str, since => '1.51';
docker name => Str, since => '1.51';
docker restart_count => Int, since => '1.51';
docker driver => Str, since => '1.51';
docker platform => Str, since => '1.51';
docker image_manifest_descriptor => 'OCIDescriptor', since => '1.51';
lib/API/Docker/Type/ContainerInspectResponse.pm view on Meta::CPAN
This file is managed through the docker daemon, and should not be accessed
or modified by other tools.
=head2 name
The name associated with this container.
For historic reasons, the name may be prefixed with a forward-slash (C</>).
=head2 restart_count
Number of times the container was restarted since it was created, or since
daemon was started.
=head2 driver
The storage-driver used for the container's filesystem (graph-driver or
snapshotter).
=head2 platform
The platform (operating system) for which the container was created.
lib/API/Docker/Type/ContainerState.pm view on Meta::CPAN
package API::Docker::Type::ContainerState;
# ABSTRACT: Container's running state
our $VERSION = '0.004';
use API::Docker::Type;
use API::Docker::Type::Health;
use namespace::clean;
docker status => Str,
enum => [qw( created running paused restarting removing exited dead )];
docker running => Bool;
docker paused => Bool;
docker restarting => Bool;
docker oom_killed => Bool, wire => 'OOMKilled';
docker dead => Bool;
docker pid => Int;
docker exit_code => Int;
docker error => Str;
docker started_at => Str;
docker finished_at => Str;
docker health => 'Health';
1;
lib/API/Docker/Type/ContainerState.pm view on Meta::CPAN
=head1 DESCRIPTION
Generated from the C<ContainerState> definition of C<spec/v1.51.yaml>.
It's part of ContainerJSONBase and will be returned by the "inspect"
command.
=head2 status
String representation of the container state. Can be one of "created",
"running", "paused", "restarting", "removing", "exited", or "dead".
=head2 running
Whether this container is running.
Note that a running container can be I<paused>. The C<Running> and C<Paused>
booleans are not mutually exclusive:
When pausing a container (on Linux), the freezer cgroup is used to suspend
all processes in the container. Freezing the process requires the process to
be running. As a result, paused containers are both C<Running> I<and>
C<Paused>.
Use the C<Status> field instead to determine if a container's state is
"running".
=head2 paused
Whether this container is paused.
=head2 restarting
Whether this container is restarting.
=head2 oom_killed
Whether a process within this container has been killed because it ran out
of memory since the container was last started. Serialised as C<OOMKilled>
-- spelled out, because deriving it from the Perl name would produce
C<OomKilled>.
=head2 dead
Undocumented upstream. The boolean beside L</status>'s C<dead> value, as
L</running>, L</paused> and L</restarting> stand beside theirs. The
swagger's example is C<false>.
=head2 pid
The process ID of this container.
=head2 exit_code
The last exit code of this container.
=head2 error
Undocumented upstream.
=head2 started_at
The time when this container was last started.
=head2 finished_at
The time when this container last exited.
=head2 health
Health stores information about the container's healthcheck results. See
L<API::Docker::Type::Health>.
lib/API/Docker/Type/ContainerSummary.pm view on Meta::CPAN
docker size_rw => Int;
docker size_root_fs => Int;
docker labels => { Str, Str };
docker state => Str,
enum => [qw( created running paused restarting exited removing dead )];
docker status => Str;
docker host_config => 'ContainerSummary::HostConfig';
docker network_settings => 'ContainerSummary::NetworkSettings';
lib/API/Docker/Type/ContainerSummary.pm view on Meta::CPAN
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).
=head2 ports
Port-mappings for the container. See L<API::Docker::Type::Port>.
lib/API/Docker/Type/ContainerSummary.pm view on Meta::CPAN
API request.
=head2 labels
User-defined key/value metadata. B<The keys are the caller's data> and are
never translated.
=head2 state
The state of this container. The swagger enumerates C<created>, C<running>,
C<paused>, C<restarting>, C<exited>, C<removing> and C<dead>.
=head2 status
Additional human-readable status of this container (e.g. C<Exit 0>).
=head2 host_config
Summary of host-specific runtime information of the container. This is a
reduced set of information in the container's "HostConfig" as available in
the container "inspect" response. See
lib/API/Docker/Type/Health.pm view on Meta::CPAN
package API::Docker::Type::Health;
# ABSTRACT: Information about the container's healthcheck results
our $VERSION = '0.004';
use API::Docker::Type;
use API::Docker::Type::HealthcheckResult;
use namespace::clean;
docker status => Str, enum => [qw( none starting healthy unhealthy )];
docker failing_streak => Int;
docker log => [ 'HealthcheckResult' ];
1;
lib/API/Docker/Type/Health.pm view on Meta::CPAN
=head1 VERSION
version 0.004
=head1 DESCRIPTION
Generated from the C<Health> definition of C<spec/v1.51.yaml>.
=head2 status
Status is one of C<none>, C<starting>, C<healthy> or C<unhealthy>
=over 4
=item * "none" Indicates there is no healthcheck
=item * "starting" Starting indicates that the container is not yet ready
=item * "healthy" Healthy indicates that the container is running correctly
=item * "unhealthy" Unhealthy indicates that the container has a problem
=back
=head2 failing_streak
FailingStreak is the number of consecutive failures.
lib/API/Docker/Type/HealthConfig.pm view on Meta::CPAN
docker interval => Int;
docker timeout => Int;
docker retries => Int;
docker start_period => Int;
docker start_interval => Int, since => '1.44';
1;
__END__
=pod
=encoding UTF-8
lib/API/Docker/Type/HealthConfig.pm view on Meta::CPAN
If the health check command does not complete within this timeout, the check
is considered failed and the health check process is forcibly terminated
without a graceful shutdown.
=head2 retries
The number of consecutive failures needed to consider a container as
unhealthy. 0 means inherit.
=head2 start_period
Start period for the container to initialize before starting health-retries
countdown in nanoseconds. It should be 0 or at least 1000000 (1 ms). 0 means
inherit.
=head2 start_interval
The time to wait between checks in nanoseconds during the start period. It
should be 0 or at least 1000000 (1 ms). 0 means inherit.
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/p5-api-docker/issues>.
=head1 CONTRIBUTING