API-Docker

 view release on metacpan or  search on metacpan

.claude/agents/api-docker-engine-worker.md  view on Meta::CPAN

---
name: api-docker-engine-worker
description: "Docker Engine API specialist for API::Docker — use whenever the question is what the daemon does or expects: adding or correcting an endpoint, query-parameter and filter semantics, response shapes (204/304, NDJSON event streams, error...
model: inherit
allowed-tools: Read, Edit, Write, Bash, Glob, Grep
briefing:
  skills:
    - docker-engine-api
    - api-docker-core
    - getty-perl-core
    - getty-perl-moo
    - getty-perl-release-author-getty
    - getty-git-commit-style

.claude/agents/api-docker-engine-worker.md  view on Meta::CPAN

`curl --unix-socket <sock> http://localhost/v<ApiVersion>/...` shows the raw stream
including frame headers, which is the fastest way to confirm a wire format before writing
code against it. A finding names the engine and version it was taken on; the `/v1.XX/`
in your own URL is what you asked for, not what the engine is.

Changing a public return shape is a cross-repo change: `../p5-dist-zilla-plugin-docker-api`
consumes `images->build`, `->tag`, `->push` and `->inspect` — verify it, or file a ticket
on its board, before landing.

Podman is a reimplementation: anything beyond the documented surface — event payload
fields, healthcheck details, error message text — is unverified until you have measured
it there, and a difference from Docker is worth writing into the `Changes` entry.

New endpoint methods follow the existing shape: options normalised into `%params`,
`list`/`inspect` wrapped into entity objects, everything else returned raw, POD with an
`=item * C<name> - meaning` per accepted key. A behavior change gets a `Changes` entry
under `{{$NEXT}}` that states what was measured.

## Verification

`prove -lr t/` for the fixture suite. For live checks against the Podman socket, set

.claude/agents/api-docker-release-checker.md  view on Meta::CPAN

   is declared; `Carp` is loaded everywhere and undeclared. Say which of those are worth
   changing; do not leave them unmentioned.
2. **`$VERSION` consistency** — `grep -rn 'our \$VERSION' lib` must return the same
   literal for all 12 modules. A module carrying a stale version, or a new module with
   none, is a release blocker. The value is the *next* release; the previous one is the
   last git tag.
3. **`dist.ini`** — `[@Author::GETTY]`, `copyright_year` current.
4. **`Changes`** — a `{{$NEXT}}` section exists and covers the user-visible changes since
   the last tag (`git log --oneline $(git describe --tags --abbrev=0 2>/dev/null)..`).
   The house standard here is a measured claim, not a summary: existing entries name the
   engine's exact error string and the observed before/after. Flag an entry that asserts
   daemon behavior without evidence.
5. **POD in sync with the code.** Every public method has `=method`, every attribute
   `=attr`, and the option lists match what the method actually forwards — the drift most
   likely to ship is an option added to a `%params` block and never documented. Check
   `README.md` against `lib/API/Docker.pm`'s SYNOPSIS too.
6. **`dzil build`** — clean, no missing files, no warnings; then `dzil test` green,
   including the generated `xt/` author and release tests (pod-syntax,
   changes_has_content).
7. **`prove -lr t/`** green with no environment set — the suite must not require a
   daemon. If every file dies with exit 2 and no plan, report a missing build dependency,

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

constructor. Changing a public method signature, a return type, or what `_request` hands
back is a cross-repo change: check that repo builds and tests green, or file a ticket on
its board before landing the change here.

**`our $VERSION` is repeated in all 12 `.pm` files and must stay identical.** That is the
house shape here (`[@Git::VersionManager]` allows `^lib/.*\.pm$` to be dirty in the
version-bump commit) — a new module gets the same literal as the rest. The value is the
*next* release; `dzil release` bumps it, never you by hand.

**Behavior changes get a `Changes` entry under `{{$NEXT}}`, and the entry says what was
measured.** The existing entries name the exact engine error string and what a local
registry did before and after. Match that standard: a claim about the daemon's behavior
is worth writing down only if you observed it.

POD lives next to the code (`=attr`, `=method`, `=head1`), woven by `@Author::GETTY`.
Touch a public signature, touch its POD in the same change.

## Verification

`prove -lr t/` — recursive, so a subdirectory added under `t/` later is not silently
skipped. Fixture-driven, no daemon needed, and it must stay that way.

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

# API-Docker House Rules

Apply to every task in this distribution unless explicitly overridden. Bias: caution over
speed on non-trivial work; use judgment on trivial tasks. Loaded automatically at launch
(same priority as `CLAUDE.md`). Subagents get their discipline from the skills
force-loaded via `briefing.skills` — this file is for the orchestrating agent.

## Engineering discipline

1. **Think before coding** — state assumptions; when uncertain, ask rather than guess.
   Push back when a simpler approach exists.
2. **Simplicity first** — minimum code that solves the problem. Nothing speculative.
3. **Surgical changes** — touch only what you must. Match existing style.
4. **Goal-driven execution** — define success criteria, loop until verified.
5. **Surface conflicts, don't average them** — pick one (more recent / more tested), flag
   the other for cleanup. Don't blend.
6. **Read before you write** — `Role::HTTP` is the single seam every resource API and
   entity class hangs off. A change to `_request`'s options, return shape or error
   handling reaches every module in `lib/` and the mock harness at once.
7. **Tests verify intent, not just behavior** — a test that can't fail when the logic
   changes is wrong, and a helper that normalises its input before asserting is that
   test. Reproduce a bug before fixing it; leave the regression behind.
8. **Checkpoint after every significant step** — summarize: done / verified / left.
9. **Match conventions** — conformance > taste. Surface a harmful convention; don't fork
   silently.
10. **Fail loud** — "Done" is wrong if anything was skipped. "Tests pass" is wrong if any
    were skipped — and in this repo a skip is the default failure mode, see below.
11. **A red test is a claim before it is a failure** — before changing code to turn a

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


  The two workers split by *question*, not by file: "what does the engine answer here?"
  is the engine-worker's, "how is this distribution built?" is the plain worker's. Only
  the engine-worker carries the Engine API reference — the other one guessing at daemon
  behavior is how a wrong assumption gets cemented.

- **You cannot spawn subagents** (you ARE an `api-docker-*` agent): the delegation lock
  does not apply to you — implement, refactor, debug, and test per these rules.

Behavior-relevant = the HTTP transport and everything it returns, the resource API method
surface, entity wrappers, request/response encoding, error handling, `cpanfile`, and
tests. Pure prose docs and `Changes` notes are not.

## Parallel fan-out — isolate the working tree

Subagents share one working tree with the orchestrator and with each other. A global git
command in one reaches all of them, so:

- **A subagent never mutates git** — no `stash`, `reset`, `checkout -- <path>`, `clean`,
  `add` or `commit`. The orchestrator owns git and commits. Say so in every subagent
  prompt, but do not rely on the prompt alone: a subagent's `git stash`/`reset`/`checkout`
  has thrown away another agent's uncommitted work three times (k111) even when the prompt
  forbade it.
- **When two or more code-touching agents run at once, isolate them.** Launch each with
  `isolation: "worktree"` so a stray git command in one cannot reach another's tree, or run
  them sequentially in the shared tree. Never fan out parallel code-touching agents into the
  same working tree without isolation.
- **A worktree may branch from a stale base.** Integrate its result by the diff
  (`git diff <merge-base> <branch> -- <files>` piped to `git apply`, or a cherry-pick),
  never by `git checkout <branch> -- <file>` for a file the main tree has since changed —
  that reverts the main tree to the stale copy. Check the merge-base against what main
  touched first.
- **Commit a verified-green checkpoint before the next mutating fan-out.** A committed HEAD
  is immune to a later stray `stash`/`reset`; uncommitted work is not.

## Coordination — karr board (always in scope)

Ticket coordination is the orchestrating agent's job, so `karr` is always in scope —
don't invoke the `kanban-issues-karr-cli` skill first, just use it. Git-native kanban;

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

- **Which engine is there is a fact about the machine, not about this file.** Before
  any live run, check which sockets exist (`/var/run/docker.sock`,
  `$XDG_RUNTIME_DIR/podman/podman.sock`) and what each announces on `GET /version` --
  `Platform.Name`, `ApiVersion`, `MinAPIVersion`. `check_live_access` answers a missing
  socket with `skip_all`, so a live run pointed at a socket that is not there reports
  success while testing nothing: read the skip line. The `/v1.XX/` in a hand-written
  curl is what the request asked for, not what the engine is. A measurement names the
  engine and the version it was taken on, or it is not a measurement.
- **Live write tests mutate the real engine.** `API_DOCKER_TEST_WRITE=1` creates and
  removes actual containers, images, networks and volumes; cleanup runs in an `END`
  block, so an interrupted run leaves them behind. Run only when the task is about live
  behavior.
- **`prune` destroys, and `dangling => 0` destroys MORE, not less.** `POST
  /images/prune` with `filters => { dangling => ['false'] }` removes every
  unused *tagged* image on the engine, locally built ones included, and they
  are not recoverable. It reads like a narrowing filter and is the opposite.
  This has already cost a locally built image, during what its caller
  believed was a read-only probe. **No `prune` of any
  kind -- images, containers, networks, volumes, build cache -- and no
  `rm -a` or `system reset`, on either engine, ever, unless the user names
  the command.** Probing what an endpoint answers is not a reason: measure

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

  connection — an unbounded `events` or `stats` call without one of those
  callbacks never returns. Pass one of the three to consume the response as
  it arrives instead. Detail: `API::Docker::Role::HTTP`'s "Streaming a
  response as it arrives".
- **The buffered streaming return type is not stable.** `_request` first
  tries `decode_json` on the whole body and only falls back to line-by-line
  NDJSON parsing (returning an arrayref of events). A stream that carries
  exactly one JSON object comes back as that hashref, not as a one-element
  array. Callers check `ref` before iterating.
- **A failed build/pull/push is still HTTP 200.** `_request` croaks on status
  >= 400 only; `errorDetail` inside the event stream is the caller's job.
- **TLS is implemented, not stubbed.** `tls => 1` on a `tcp://` connection
  (`unix://` never encrypts, and refuses the combination outright) swaps in
  `IO::Socket::SSL` in place of the plain socket — same reader, same writer,
  same everything above it. `cert_path` names a directory in the `docker`
  CLI's own layout (`ca.pem` as the trust anchor, `cert.pem`+`key.pem` as
  this client's identity), defaulting from `$ENV{DOCKER_CERT_PATH}`;
  `tls_insecure => 1` turns verification off. `IO::Socket::SSL` is a
  recommended, not required, dependency, loaded only once a TLS connection is
  actually opened. Detail: `API::Docker::Role::HTTP`'s "TLS on a tcp://
  connection".

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

name: api-docker-type-model
description: "Use when working on API::Docker's typed object model — a class under API::Docker::Type::*, the API::Docker::Type DSL, maint/spec-to-type.pl, maint/spec-drift-check.pl, or anything under spec/. Also when a type class is wrong, out of d...
---

# API::Docker::Type — the generated model over the Docker Engine API

Every class under `API::Docker::Type::*` mirrors one `definitions:` entry in
Docker's swagger. They are **generated** by `maint/spec-to-type.pl` out of
`spec/`, and `maint/spec-drift-check.pl` is what keeps them honest.

## The two rules that override everything else

**Never hand-edit a file under `lib/API/Docker/Type/`.** Not one character,
not a typo, not a comma. `t/spec_to_type.t` runs the generator and asserts
that every class is byte-identical to what it emits — a hand edit turns the
suite red, and the edit is lost the moment anyone regenerates. Change the
generator or its data files instead; the next section says which.

**The generator only ever creates.** It writes a file that does not exist and
refuses to overwrite one that does. There is no `--force` and no bulk
refresh, and the refusal is enforced against relative paths, absolute paths,

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

translates what it knows and forwards the rest verbatim. This is not
theoretical: a real Podman `/info` answers with fields the swagger does not
have, and `ImageSummary` still serves `VirtualSize`, which Docker dropped from
the spec after v1.41.

**A null is where that stops, and only for a field we know.** A known field an
engine sends as `null` is read as unset: the attribute stays `undef` and
`TO_JSON` writes no key for it. That is not a leak, it is the daemon's own
resolution — measured 2026-08-28 against Podman 5.8.4 (API 1.44), where
`POST /containers/create` answers `{}`, `{"Image":null}` and `{"Image":""}`
with byte-identical errors, because Go's `encoding/json` unmarshals a null
into the type's zero value and an absent field leaves that same zero value.
It holds outbound too, which is why `/images/{id}/history` answers
`"Tags": null` instead of omitting the field. An *unknown* field keeps its
null, because with no declared type there is no zero value to read it as, and
so does a null under a key the caller chose. Three shapes, three outcomes, on
purpose; the reasoning lives in `API::Docker::Role::Type`'s POD and
`t/type_fixture_passthrough.t` holds all three against the fixtures.

**`since` is documentation, never a check.** It records which API version
introduced a field, derived by diffing the specs in `spec/` against each

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

Every path is prefixed `/v1.NN` (`/v1.47/containers/json`). Unversioned paths
work and mean "whatever the daemon defaults to" — fine for `/version` and
`/_ping`, wrong for anything a client should pin.

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

```
Tty=0:  01 00 00 00 00 00 00 04  "OUT\n"   02 00 00 00 00 00 00 04  "ERR\n"
Tty=1:  "OUT\r\n"  "ERR\r\n"
```

**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

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

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

```
?filters={"dangling":["true"],"label":["stage=build"]}     correct
?filters={"dangling":true}                                 matches nothing
```

Wrong-shaped filters do not error. The daemon accepts them and returns an
unfiltered or empty list, so the bug surfaces as "my prune deleted too much" or
"my list is empty", never as a 400.

Query-string booleans elsewhere are strings: `?all=1` / `?all=true`. Booleans
in a **JSON request body** must be real JSON booleans — a language that encodes
`1` where the daemon expects `true` gets a type error from the API.

## Registry auth

`X-Registry-Auth` carries **base64url of a JSON object**, and the padding is
required — the daemon decodes with Go's `base64.URLEncoding`, not
`RawURLEncoding`. Stripping `=` produces
`failed to parse "X-Registry-Auth" header ... unexpected EOF`.

The header is mandatory on **every** push, anonymous included; the anonymous
form is the encoding of `{}`, which is `e30=` — three characters and one pad,

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

`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
you actually target.

Clients differ in how they *find* the daemon: `DOCKER_HOST` is the one
mechanism all of them honour. Docker contexts
(`~/.docker/config.json` `currentContext` plus
`~/.docker/contexts/meta/*/meta.json`) are resolved by the `docker` CLI and
docker-java but not by most library clients, so "works in the terminal, fails
in my program" usually means a context the program never read.

## Probing by hand

.claude/skills/getty-perl-core/SKILL.md  view on Meta::CPAN

---
name: getty-perl-core
description: "Load on any Perl edit in a Getty project — module loading, attributes, errors, strings, control flow, cpanfile, Changes, and the house choices that differ from Perl defaults."
---

# 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

.claude/skills/getty-perl-core/SKILL.md  view on Meta::CPAN

- **A builder that ignores `$self` fits on one line:** `sub _build_readonly { 0 }`.
- **`_` prefix marks private** subs and attributes. Builders for private attributes double up: `sub _build__mp`.

### Methods, not bare subs

- **In a class, every helper is a method on `$self`** — not `sub _foo {...}` invoked as `_foo($self->config, $x)`.
- **Per-process caches go on the singleton as an attribute** (`has _cache => ( is => 'ro', default => sub { {} } )`), not a `my %CACHE` package variable.
- **No package-level state** unless it is a true constant (an `%ENGINE_CLASS` lookup table counts; a per-call cache does not).
- Bare subs are fine in **non-class utility modules** imported as functions. Once a file says `use Moose`/`use Moo`, every `sub` is a method.

Why: bare subs hide what the call needs, can't be overridden or mocked, and force every caller to thread state by hand.

## Errors

- **`croak`, never `die`.** Errors report the caller's line, not ours.
- **Import it:** `use Carp qw( croak );` and call `croak(...)` bare.
- **Name the origin in the message:** `croak __PACKAGE__."->state too many args"` — or whatever identifies the operation in that module's DSL.

## Strings

- **Concatenate, do not interpolate:** `'Adding '.$f.' with '.$length.' bytes'`. Interpolate only where concatenation would be unreadable.

.claude/skills/getty-perl-moo/SKILL.md  view on Meta::CPAN


- **`with '...'` goes directly under `use Moo;`**, before the remaining `use` lines. Role composition is a runtime action, not an import — placing it with the class declaration says so. Same in Moose.
- **`is => 'lazy'` + a separate `sub _build_x`** for anything non-trivial; never move construction into `default => sub {...}`.
- **A builder that ignores `$self` is one line:** `sub _build_readonly { 0 }`.
- **`is => 'ro'` is the default**; `rw` needs a reason.
- **Type attributes.** `Types::Standard` is the house choice and typed attributes are wanted — reach for a type before leaving one off. See Type Constraints below.
- **`init_arg => undef`** for attributes the constructor must not set; `init_arg => 'other'` to rename or free up a method name.

---

## Pattern 1 – `extends` + Attribute Override

```perl
package App::Base;
use Moo;
has prefix => (is => 'ro', default => sub { 'Hello' });
sub greet { $_[0]->prefix . ", " . $_[0]->name }

package App::Friendly;
use Moo;
extends 'App::Base';
has '+prefix' => (default => sub { 'Hi' });   # override via +attr
```

**Rules:** Multiple `extends` calls REPLACE (don't add). Reference defaults always as coderefs (`sub { [] }`, never `[]`).

---

## Pattern 2 – Role with `requires`

```perl
package App::Role::UppercaseName;

.claude/skills/getty-perl-moo/SKILL.md  view on Meta::CPAN


sub FOREIGNBUILDARGS {          # maps args to non-Moo parent's constructor
  my ($class, $args) = @_;
  return ($args->{source});
}

sub BUILD {                     # runs AFTER all attributes are set; parent→child order
  my ($self, $args) = @_;
  die "invalid" unless length $args->{source};
}
# DEMOLISH: child→parent order. Never override DESTROY directly.
```

Do NOT call `SUPER::BUILD` manually – Moo handles the chain.

---

## Pattern 10 – Strict Constructor

```perl
package StrictThing;

.claude/skills/getty-perl-moo/SKILL.md  view on Meta::CPAN


## Common Pitfalls

- `default => []` → **shared state bug**. Always `default => sub { [] }`.
- `extends 'A'; extends 'B'` → replaces, does NOT add B to A. Use `extends 'A', 'B'`.
- Imports after `use Moo::Role` are **composed into consumers** as methods.
- `namespace::autoclean` < 0.16 inflates Moo classes to Moose unexpectedly.
- `trigger` does NOT receive old value (unlike Moose).
- `Sub::HandlesVia` must be loaded *after* `use Moo`.
- `BUILD` chain is automatic; calling `SUPER::BUILD` manually breaks it.
- Never override `DESTROY`; use `DEMOLISH`.

.claude/skills/getty-perl-release-author-getty/SKILL.md  view on Meta::CPAN

- `adoptme` - Add x_adoptme metadata
- `no_github` - Skip GithubMeta and GitHub::CreateRelease, use Repository instead. Auto-set to 1 when `.git/config` has no github.com remote; set `no_github = 0` to force GitHub plugins on anyway
- `no_github_release` - Skip only GitHub::CreateRelease. Same auto-detection; when active, `dzil release` creates a GitHub Release and attaches the tarball, which needs `~/.github-identity` (login + token)
- `gitea` - Treat the remote host as Gitea/Forgejo (repository/bugtracker/homepage via GiteaMeta). Only needed for self-hosted instances — codeberg.org and the author's own are auto-detected. No effect when a GitHub remote exists
- `include_readme` - Ship README.md (excluded from the tarball by default)
- `no_install` - Resulting distribution can't be installed
- `generate_license` - Go back to a generated LICENSE: `@Basic` keeps its License plugin, no LicenseFile check is added. Default 0 — the bundle expects a committed LICENSE (see above)

### Identity & Metadata
- `author` - CPAN author name used for the authority
- `authority` - Override the authority, e.g. `authority = ETHER` when uploading modules owned by another author (default: the `author` value)

### XS with Alien
- `xs_alien = Alien::Foo` - Auto-configures MakeMaker::Awesome for XS+Alien
- `xs_object = Name` - Override XS object name (default: derived from Alien name)

### Versioning
- `task = 1` - TaskWeaver + AutoVersion
- `manual_version = x.x` - Manual version
- `major_version = 2` - Major version for AutoVersion
- `version_finder` - multi-value; forwarded as the `finder` option of RewriteVersion::Transitional + BumpVersionAfterRelease (default path) and PkgVersion (task/manual_version path). Defaults to `:MainModule` when `no_cpan` is set, otherwise unset.

### Build & Release
- `weaver_config` - PodWeaver `config_plugin` to use (default: the bundle's own)
- `installrelease_command` - Command used to install after release, instead of cpanm

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

Git-native kanban board for multi-agent workflows. Canonical board state lives in
`refs/karr/*`, not in a checked-in `karr/` directory. Commands materialize a
temporary task/config view only while they run.

`--json` is available on every command with an alternate rendering. `--compact`
is not -- exactly nine render one: `board`, `config`, `context`, `dashboard`,
`list`, `log`, `metrics`, `pick`, `show`. Anywhere else it answers
`Unknown option: compact` with the usage and exit 2, rather than accepting the
flag and ignoring it.

## Referring to a karr id

In prose -- commit subjects, card bodies, anywhere the text travels -- a karr id is
written `k12`, never a bare `#12`: GitHub, Gitea and GitLab all resolve `#12` against
*their own* issue 12, which is a different thing or nothing at all. This is karr's
numbering only and says nothing about how the repository's public issue tracker is
referenced -- that is separate, with its own notation.

## Commands

### Initialize

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

`--sort priority -n 5` is the five most urgent open cards rather than five
arbitrary ones put in order -- that is the "what next" call, instead of pulling
the whole board and cutting it locally.

`--unclaimed` is "what is free right now" -- `claimed_by` unset or empty, or a
claim older than the board's `claim_timeout`. It is the question `karr pick`
answers by *taking* the card, so this is how to see the free work without
touching it, and it uses the very test `pick` uses. It is not the opposite of
`--claimed-by NAME`: that one is an exact match on the field and matches an
expired claim too, so the two overlap on "cards NAME no longer holds" and
passing both is a usage error. Since it asks about the claim and nothing else,
a blocked card nobody holds is still listed -- `--blocked --unclaimed` is a
real triage query.

### Show task

```bash
karr show ID
karr show                  # most recently updated task
karr show --last 5         # the 5 most recent
karr show --me             # the task you most recently acted on (re-orient)

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

karr edit ID --remove-needs other-repo#7     # absent references are a no-op
karr edit ID --body "New description"
karr edit ID -a "Appended note"              # append to body
karr edit ID --claim agent-1                 # claim
karr edit ID --release                       # release claim
karr edit ID --block "Waiting on API"        # mark blocked
karr edit ID --unblock                       # clear blocked
```

An unknown or non-numeric id given to `--depends-on`/`--add-depends-on` rejects
the whole invocation before anything is written (usage error, exit 2); a
self-reference (`karr edit 5 --add-depends-on 5`) fails only that id, the rest
of the batch proceeds, and the command exits 1. Taking up a card whose
dependencies are unfinished warns on move/pick but is never blocked.

### Delete task

```bash
karr delete ID                               # asks first
karr delete ID --yes                         # skip confirmation
karr delete ID,ID,ID --yes                   # a batch

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

karr config show --compact                   # key=value per line, no padding
```

Writable keys: `board.name`, `board.description`, `defaults.status`, `defaults.priority`, `defaults.class`, `claim_timeout`, `lock_timeout`, `foundation.enabled`, `foundation.reason`.

`show` and `get` read this board and refuse with exit 1 when there is none —
they never fall back to the built-in defaults, which is how a fresh clone used
to answer `board.name: Kanban Board` for a board that has a name. Ask for those
defaults explicitly with `--defaults`: it reads no board (and needs no
repository), so `diff <(karr config show) <(karr config show --defaults)` is
exactly what this board overrides.

### Disable / enable automated agent runs

```bash
karr disable                                 # no automated agent runs here
karr disable --reason "abandoned driver, backlog parked"
karr enable                                  # allow them again
karr disable --json                          # {"foundation":{"enabled":0,"reason":"…"}}
```

Board-level opt-out from `karr-foundation`. Unlike the per-machine `.karr` file
the flag is board state (`foundation.enabled` in `refs/karr/config`), so it syncs
with the board and every foundation instance on every machine honours it. A
disabled board is skipped whole: no drain, no auto-block, no agent run — the
flag wins over `karr-foundation --command`, the config's `default_command`, the
`.karr` `command` and `claude: true`, and `--force` does not override it. Nothing
else changes: the board stays fully usable by hand (`karr list`, `karr pick`,
`karr move`, …). Use it for a repository whose backlog is parked rather than
abandoned.

`karr disable` without `--reason` clears any previously stored reason. The same
state is readable and writable through `karr config`:

```bash
karr config get foundation.enabled           # -> 0 or 1
karr config set foundation.enabled false     # true/false, yes/no, on/off, 1/0

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

karr materialize                             # refs -> tasks/ + config.yml on disk
karr materialize --force                     # overwrite git-tracked cards there
karr import --yes                            # tasks/ on disk -> refs
```

The board lives in `refs/karr/*`. `materialize` writes a file view of it for
grepping or for kanban-md to read; `import` reads such a directory back in.
The `tasks/` directory is always gitignored and is never the source of truth —
losing it costs nothing, editing it costs nothing until you `import`.
`materialize` refuses to write over paths the project itself tracks in git,
which is what `--force` overrides.

### Repair an old board

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

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

claim_timeout: 1h
defaults:
  status: backlog
  priority: medium
  class: standard
foundation:
  enabled: false
  reason: abandoned driver, backlog parked
```

That YAML lives in `refs/karr/config` as sparse overrides. The next numeric id
is kept separately in `refs/karr/meta/next-id`.

## Decision tree: which command?

1. **Need a board?** → `karr init`
2. **New work item?** → `karr create "Title" --priority high`
3. **What's on the board?** → `karr board` or `karr list`
4. **Starting work?** → `karr pick --claim NAME --move in-progress`
5. **Done with task, hand to review?** → `karr handoff ID --claim NAME --note "reason"`
6. **Done with task, close it?** → `karr edit ID --release && karr move ID done`

.claude/skills/perl-release-dist-ini/SKILL.md  view on Meta::CPAN

```

## Common dist.ini Sections

| Section | Purpose |
|---------|---------|
| `name` | Distribution name |
| `author` | CPAN author |
| `license` | License type |
| `copyright_holder` | Copyright owner |
| `copyright_year` | Override year |
| `[@Bundle]` | Plugin bundle |
| `[Plugin]` | Individual plugin |

## Plugin Loading Order

1. `[GatherDir]` - Collects files
2. `[PruneCruft]` - Removes unwanted files
3. `[Prereqs]` / `cpanfile` - Dependencies
4. `[Version plugins]` - PkgVersion, AutoVersion, etc.
5. `[Meta plugins]` - MetaJSON, MetaYAML

.gitignore  view on Meta::CPAN

API-Docker-*
.build

# Claude Code — commit: skills/, agents/, hooks/, settings.json
# Ignore: local overrides, credentials, session data
.claude/*.local.*
.claude/local/
.claude/.credentials.json
.claude/statsig/
.claude/todos/
.claude/projects/

# karr materialized task view -- never commit
tasks/
config.yml

Changes  view on Meta::CPAN

    `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`,
    `create`, `inspect`, `update` and `remove`. `Data` is base64-encoded for
    the caller; `update` takes the current `Version.Index` as a mandatory
    concurrency token.
  - New `API::Docker::API::System::auth` (POST /auth): check registry

Changes  view on Meta::CPAN

  - Swarm (`/swarm`, `/nodes`, `/services`, `/tasks`) is documented as a
    permanent scope decision, not a gap: Podman implements none of it and no
    consumer needs it. `secrets` and `configs` stand on their own and stay
    covered.

0.003     2026-08-27 03:37:00Z
  - t/containers.t: the registered cleanup tolerates the container the
    happy path already removed, so a live run no longer warns "Cleanup
    failed: ... no such container" on every pass. The safety net still
    warns on any other failure.
  - The `>= 400` croak now falls back to `errorDetail.message` and then to
    the flat `error` key when the JSON error body carries no `message`.
    Docker answers `{"message":...}`; Podman answers a failed push with
    the stream shape instead -- `{"errorDetail":{"message":...},"error":
    ...}` and no `message` at all -- so the whole JSON object used to be
    the croak text and the reason had to be dug out of it by eye. A body
    that is not an object still surfaces verbatim.
  - `images->build`, `->pull` and `->push` now croak when the engine
    reports a failure inside the event stream, instead of returning the
    stream and leaving the check to the caller. A failed build, pull or
    push is answered with HTTP 200 -- the status line is committed
    before the operation is attempted -- and the failure arrives as an
    `errorDetail` object among the progress events, so nothing about the
    response says the operation broke. Measured against the rootless
    Podman socket (5.4.2, API 1.41): a Dockerfile of `FROM alpine:3` /
    `RUN exit 7` answers `200 OK` and ends the stream with
    `{"errorDetail":{"message":"building at STEP \"RUN exit 7\": while
    running runtime: exit status 7\n"},"error":"..."}`, the flat `error`
    key carrying the same text. Callers that never scanned for it -- the
    documented shape until now -- reported a broken build as a success.
    The exception is an `API::Docker::Error::Stream`, a new class whose
    whole purpose is that the progress output is not lost with the
    return value: `$err->events` is the complete event list, error event
    included. It overloads stringification and produces exactly what the
    plain croak it replaces produced, reason plus Carp's ` at FILE line
    N.` suffix, so existing eval-and-inspect-$@ code needs no change;
    `$@ =~ s/...//` on it yields a plain string as it would for any
    overloaded object. The trigger is the `errorDetail` key alone, never
    the word "error" in payload text.
    Which of the three actually takes that route depends on the engine,
    and Podman is not Docker here -- measured on the same socket, all
    three cases: only `build` answers 200 with the failure in the
    stream. A pull of a missing repository answers `403 Forbidden` with
    `{"message":"denied: requested access to the resource is denied"}`,
    a missing tag answers `404 Not Found` with `{"message":"manifest
    unknown: manifest unknown"}`, and a push to an unreachable registry
    answers `500 Internal Server Error` with an `errorDetail` body and
    no `message` key at all. The first two never reach the stream; the
    third has its whole JSON body used as the croak text, because the
    >= 400 path looks for `message`. So on Podman the new check fires
    for `build` and the pre-existing status check catches the other two.
    All three are loud either way, but catching
    `API::Docker::Error::Stream` specifically is not a reliable way to
    catch a failed pull or push -- inspect $@ as a string, which both
    routes satisfy. The POD on each method says which engine does what.
    `system->events` is explicitly exempt and never croaks on stream
    content: it is a feed, so an object in it records something that
    happened on the engine rather than the outcome of this call. The
    check is on by default for the transport's `ndjson` option and
    exempting an endpoint is deliberate (`croak_on_error => 0`), because
    the operation-shaped streaming endpoints are open-ended while the
    feed-shaped ones are `/events` and nothing else.
  - `tls => 1` now croaks with "not implemented" instead of being
    accepted and ignored. `tls` and `cert_path` were attributes no code
    read: `API::Docker::Role::HTTP` builds a plain IO::Socket::INET and
    speaks HTTP over it, so a `tcp://` daemon was always addressed in
    cleartext and a caller who asked for TLS got an unencrypted
    connection with no indication of it -- anyone passing the option was
    by definition sending credentials in the clear while believing
    otherwise. TLS is still not implemented; the croak names the reason

Changes  view on Meta::CPAN

    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`

Changes  view on Meta::CPAN

    `/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.
  - Bring the cpanfile in line with what the code loads. `URI` was
    required and is used nowhere in `lib/` or `t/`, so every consumer
    installed it for nothing; it is gone. `Carp` (loaded by eight of the
    twelve modules) and `IO::Socket::INET` (loaded by
    API::Docker::Role::HTTP beside its already-declared `IO::Socket::UNIX`
    sibling) were undeclared and are now required, as is `Exporter`
    under `on test` for the mock helper. Nothing else in the tree loads
    an undeclared module: `SOCK_STREAM` in the HTTP role comes from
    IO::Socket, which IO::Socket::UNIX and IO::Socket::INET both
    re-export, and `Path::Tiny` appears in `lib/` only inside the

LICENSE  view on Meta::CPAN

                            Preamble

  The license agreements of most software companies try to keep users
at the mercy of those companies.  By contrast, our General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  The
General Public License applies to the Free Software Foundation's
software and to any other program whose authors commit to using it.
You can use it for your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Specifically, the General Public License is designed to make
sure that you have the freedom to give away or sell copies of free
software, that you receive source code or can get it if you want it,
that you can change the software or use pieces of it in new free
programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

LICENSE  view on Meta::CPAN

Program or a portion of it, either verbatim or with modifications.  Each
licensee is addressed as "you".

  1. You may copy and distribute verbatim copies of the Program's source
code as you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and
disclaimer of warranty; keep intact all the notices that refer to this
General Public License and to the absence of any warranty; and give any
other recipients of the Program a copy of this General Public License
along with the Program.  You may charge a fee for the physical act of
transferring a copy.

  2. You may modify your copy or copies of the Program or any portion of
it, and copy and distribute such modifications under the terms of Paragraph
1 above, provided that you also do the following:

    a) cause the modified files to carry prominent notices stating that
    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

LICENSE  view on Meta::CPAN


    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.

Mere aggregation of another independent work with the Program (or its
derivative) on a volume of a storage or distribution medium does not bring
the other work under the scope of these terms.

  3. You may copy and distribute the Program (or a portion or derivative of
it, under Paragraph 2) in object code or executable form under the terms of
Paragraphs 1 and 2 above provided that you also do one of the following:

LICENSE  view on Meta::CPAN

    years, to give any third party free (except for a nominal charge
    for the cost of distribution) a complete machine-readable copy of the
    corresponding source code, to be distributed under the terms of
    Paragraphs 1 and 2 above; or,

    c) accompany it with the information you received as to where the
    corresponding source code may be obtained.  (This alternative is
    allowed only for noncommercial distribution and only if you
    received the program in object code or executable form alone.)

Source code for a work means the preferred form of the work for making
modifications to it.  For an executable file, complete source code means
all the source code for all modules it contains; but, as a special
exception, it need not include source code for modules which are standard
libraries that accompany the operating system on which the executable
file runs, or for standard header files or definitions files that
accompany that operating system.

  4. You may not copy, modify, sublicense, distribute or transfer the
Program except as expressly provided under this General Public License.
Any attempt otherwise to copy, modify, sublicense, distribute or transfer

MANIFEST  view on Meta::CPAN

lib/API/Docker/Type/Volume.pm
lib/API/Docker/Type/Volume/UsageData.pm
lib/API/Docker/Type/VolumeCreateOptions.pm
lib/API/Docker/Type/VolumeListResponse.pm
lib/API/Docker/Volume.pm
t/author-pod-syntax.t
t/basic.t
t/connect_timeout.t
t/containers.t
t/containers_endpoints.t
t/containers_stats_error.t
t/dist_source.t
t/distribution.t
t/entity_container.t
t/entity_roles.t
t/exec.t
t/filters.t
t/fixtures/container_inspect.json
t/fixtures/containers_archive.tar
t/fixtures/containers_list.json
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
t/fixtures/system_info.json

MANIFEST  view on Meta::CPAN

t/lib/Test/API/Docker/Mock.pm
t/mock_harness.t
t/networks.t
t/plugins.t
t/read_timeout.t
t/registry_auth.t
t/release-changes_has_content.t
t/role_http.t
t/secrets_configs.t
t/spec_to_type.t
t/stream_error.t
t/stream_frames.t
t/stream_incremental.t
t/streaming_callback.t
t/streaming_methods.t
t/streaming_shape.t
t/system.t
t/system_auth.t
t/timeout_forwarding.t
t/tls.t
t/tls_read.t

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

on> -- C<DOCKER_TLS_VERIFY=0> included, and so are C<false>, C<no> and C<off>.
Only unset, or the empty string, is off. That is deliberately not Perl
truthiness: C<'0'> is the value most likely to be typed for "off" and is
precisely where the two rules would part company. An explicit C<< tls => ... >>
passed to the constructor outranks the variable in both directions.

The variable is B<ignored on a socket host>, as the CLI ignores it -- a
C<unix://>, C<npipe://> or C<fd://> connection carries nothing to encrypt.
Without that exception a shell exporting C<DOCKER_TLS_VERIFY> would make a bare
C<< API::Docker->new >> croak on every machine talking to a local socket, since
C<< tls => 1 >> on a non-C<tcp://> host is a construction error (below).

C<DOCKER_TLS_VERIFY> with no L</cert_path> and no C<DOCKER_CERT_PATH> beside it
is TLS against the system trust store, not an error; the CLI asks for no
certificates either, and non-empty there means encrypt B<and> verify.

With C<< tls => 1 >> the transport opens an L<IO::Socket::SSL> connection
instead of an L<IO::Socket::INET> one and nothing above the socket changes.
The daemon's certificate is B<verified>, and so is its hostname; L</cert_path>
supplies the trust anchor and this client's own certificate.

With no certificates at all it still means encrypt and verify, against the
system trust store -- see
L<API::Docker::Role::HTTP/"TLS with no certificates at all">
for why that rather than an error. To switch verification off, and to read
what that gives away, see L</tls_insecure>.

C<< tls => 1 >> on a C<unix://> host croaks at construction. A Unix socket is
a file, not a wire; there is nothing on it to encrypt, and accepting the
option would mean answering a request for an encrypted transport with an
unencrypted one -- which is the failure this attribute previously had.

L<IO::Socket::SSL> is a recommended rather than a required dependency, loaded
when the first TLS connection is opened; C<< tls => 1 >> without it installed
croaks naming it. See

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

The Engine API groups C</configs> with Swarm. A Docker daemon that is not a
swarm manager answers B<503> C<"This node is not a swarm manager."> to all of
these endpoints, which this client turns into a croak. That is documented
engine behaviour, not a fault at this end -- the daemon needs
C<docker swarm init>, or a manager to talk to, and a single-node install that
has never run it is the ordinary case, not an edge one.

B<Podman does not serve C</configs>,> though what it answers for "not served"
differs by path. Measured against the rootless socket on Podman 5.8.4 (API
1.44): C<GET /configs> -- the collection listing -- still answers B<404> with
the plain-text body C<Not Found>, not a JSON error, so the croak from this
client reads C<Docker API error (404): Not Found>. Every other path under it
-- C<GET /configs/{id}>, C<POST /configs/create>, C<DELETE /configs/{id}> and
C<POST /configs/{id}/update> -- answers B<503> instead, with a JSON body
naming the route it refuses, e.g. C<< {"cause":"Podman does not support
service: /v1.44/configs/xyz","message":"...","response":503} >>.

An earlier pass measured every path here as a flat 404 against Podman 5.4.2
(API 1.41). That measurement is not reproducible on this machine any more --
5.4.2 is gone from it -- so whether 5.8.4 actually changed this or the
original pass only ever exercised the collection endpoint is not something
this distribution can decide from here; it is recorded as what 5.8.4 answers,

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

}


# The booleans of the container create body, from spec/v1.51.yaml: the
# ContainerConfig flags at the top level, and the HostConfig flags in the
# nested `HostConfig` object (its own plus the ones it inherits from
# Resources). The engine rejects a number for any of them, so 1/0 is
# normalised to a JSON boolean on the way out; a caller may still pass 1/0 or a
# JSON boolean and it goes out correctly either way.
my @CONTAINER_CONFIG_BOOLS = qw(
  ArgsEscaped AttachStderr AttachStdin AttachStdout NetworkDisabled
  OpenStdin StdinOnce Tty
);
my @HOST_CONFIG_BOOLS = qw(
  AutoRemove Init OomKillDisable Privileged PublishAllPorts ReadonlyRootfs
);

sub create {
  my ($self, %config) = @_;
  my %params;
  $params{name} = delete $config{name} if defined $config{name};

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

  );
}


sub logs {
  my ($self, $id, %opts) = @_;
  croak "Container ID required" unless $id;
  my %params;
  $params{follow}     = $opts{follow} ? 1 : 0 if defined $opts{follow};
  $params{stdout}     = defined $opts{stdout} ? ($opts{stdout} ? 1 : 0) : 1;
  $params{stderr}     = defined $opts{stderr} ? ($opts{stderr} ? 1 : 0) : 1;
  $params{since}      = $opts{since}      if defined $opts{since};
  $params{until}      = $opts{until}      if defined $opts{until};
  $params{timestamps} = $opts{timestamps} ? 1 : 0 if defined $opts{timestamps};
  $params{tail}       = $opts{tail}       if defined $opts{tail};
  # exists, not truth: an unset callback is a caller bug, and quietly falling
  # back to the buffered path for it would answer a follow with a hang.
  return $self->client->stream_frames('GET', "/containers/$id/logs",
    params => \%params,
    defined $opts{tty} ? ( tty => $opts{tty} ) : (),
    %{ $self->_request_options },

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

# not be the thing that breaks a working call.
sub _assert_container_running {
  my ($self, $id) = @_;

  # A State the model could not use is one more shape the check does not
  # recognise, and it arrives as one: the generated classes type their fields
  # from the swagger, and a State that is not the object
  # ContainerInspectResponse declares -- the bare status string of the list
  # shape, say -- leaves ->state unset and keeps the raw value in
  # unknown_fields rather than taking the response down with it. So there is
  # nothing to catch here; an error that does reach this line, the daemon's
  # own 404 included, is the caller's and goes up.
  my $inspected = $self->inspect($id);

  # An API::Docker::Type::ContainerState, or undef where the daemon sent no
  # State at all -- which is the "does not recognise" case above, not a stopped
  # container.
  my $state = $inspected->state;
  return unless blessed($state) && defined $state->running;
  return if $state->running;

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

  # The pre-flight is a request the caller never wrote, and one that hangs is
  # exactly what a bound was set to prevent -- so it carries the same one. It
  # does that by itself here: the check runs on $self, which is the clone
  # ->using returned when there was one (karr k74).
  $self->_assert_container_running($id) if $require_running;

  my %params;
  $params{stream} = $opts{stream} ? 1 : 0;
  $params{logs}   = defined $opts{logs}   ? ($opts{logs}   ? 1 : 0) : 1;
  $params{stdout} = defined $opts{stdout} ? ($opts{stdout} ? 1 : 0) : 1;
  $params{stderr} = defined $opts{stderr} ? ($opts{stderr} ? 1 : 0) : 1;
  $params{stdin}  = $opts{stdin} ? 1 : 0 if defined $opts{stdin};
  return $self->client->stream_frames('POST', "/containers/$id/attach",
    params => \%params,
    defined $opts{tty} ? ( tty => $opts{tty} ) : (),
    %{ $self->_request_options },
    exists $opts{on_frame} ? ( on_frame => $opts{on_frame} ) : (),
  );
}


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

  my %params;
  $params{ps_args} = $opts{ps_args} if defined $opts{ps_args};
  return $self->client->get("/containers/$id/top",
    params => \%params,
    %{ $self->_request_options },
  );
}


# The Podman compatibility path, and deliberately only that. Podman answers
# GET /containers/{id}/stats for a container that is not running with an error
# object inside a response it has already committed to 200 --
# {"cause":"container is stopped","message":"container is stopped",
# "response":500}, chunked, for the one-shot call and for stream => 1 alike
# (measured on Podman 5.4.2, API 1.41). Neither guard in the transport sees
# it: the >= 400 croak reads the status line, which says 200, and the stream
# check triggers on errorDetail, which this object does not carry. Docker
# 29.7.2 (API 1.55) answers the same call with a real, zero-filled reading and
# never produces this shape at all -- so this belongs here, next to the one
# endpoint and the one engine it was measured on, and not in Role::HTTP, where
# it would be a heuristic on daemon prose sitting under all twelve modules.
#
# All four clauses have to hold. The narrowness is the point, not an accident:
#
#   1. the status was 2xx. True by construction on both paths this guards:
#      _request croaks before returning for >= 400, and
#      _read_streaming_response reads such a body whole rather than handing it
#      to a callback, so nothing that failed the status line reaches here
#   2. the decoded value is a HashRef
#   3. it carries all three of cause, message and response, exactly
#      lower-cased. Never case-insensitively, and this is the counter-example
#      that fixes it: POST /containers/{id}/wait answers its SUCCESS case with
#      a top-level `Error` key -- Podman sends "Error":null on every wait --
#      so a rule matching /error/i would turn every successful wait into a
#      failure. Measured over fifteen read endpoints per engine and every
#      fixture in t/fixtures: no 2xx body on either engine carries even one of
#      these three lower-cased at the top level
#   4. `response` is a non-ref scalar reading as an integer >= 400. That is
#      what makes the rule self-evidencing rather than a guess about prose:
#      the object is an error because Podman says so inside it. Known miss:
#      Podman's GET /plugins answers {"cause":"","message":"Path ... is not
#      supported","response":0}, which clause 4 rejects -- but it arrives with
#      404 on the status line and the transport croaks it long before this
#      runs, so the miss goes in the conservative direction and costs nothing
#
# A bare {message => ...} deliberately does not trigger: that is the ordinary
# Docker error body, and treating one inside a 2xx as a failure would be a
# guess about prose rather than a reading of what the engine said.
sub _podman_error_object {
  my ($self, $value) = @_;

  return unless ref $value eq 'HASH';
  return unless exists $value->{cause}
    && exists $value->{message}
    && exists $value->{response};

  my $response = $value->{response};
  return if ref $response;
  return unless defined $response && $response =~ /\A[0-9]+\z/;
  return unless $response >= 400;

  return $value;
}

# API::Docker::Error::HTTP rather than ::Stream: the one-shot call is not a
# stream at all, so "Docker API stream error" would be the wrong sentence for
# it and ->events would be a fabricated list. What the caller wants instead is
# exactly what this class carries -- ->status for the code Podman named, and
# ->data for the object, whose `cause` key that attribute's own POD already
# points at. Two of its attributes are left at their defaults on this path, on
# purpose: ->reason, because the status line's reason phrase was "OK" and
# putting that on a 500 would mislead, and ->body, because the bytes were
# decoded by the transport before this check ever saw them.
sub _assert_no_podman_error {
  my ($self, $endpoint, $value) = @_;

  my $error = $self->_podman_error_object($value) or return $value;

  my $reason = $error->{message};
  $reason = $error->{cause}    unless defined $reason && length $reason;
  $reason = 'no message given' unless defined $reason && length $reason;
  # Carp appends no location to a message that already ends in a newline.
  $reason =~ s/\s+\z//;

  # The object goes into a variable first: `croak CLASS->new(...)` is indirect
  # object syntax and parses as CLASS->croak(new(...)). Carp hands a reference
  # straight back rather than decorating it, so the location is captured by
  # hand, naming the frame a croak of a plain string would have named.
  my $err = API::Docker::Error::HTTP->new(
    message  => 'Docker API error (' . $error->{response} . '): ' . $reason
      . ' -- reported inside a 200 response to ' . $endpoint,
    location => shortmess(''),
    status   => $error->{response},
    data     => $error,
  );
  croak $err;
}

sub stats {
  my ($self, $id, %opts) = @_;
  croak "Container ID required" unless $id;
  my $stream = $opts{stream} ? 1 : 0;
  my %params = ( stream => $stream );
  # one-shot asks the engine not to wait for a second sampling cycle, which
  # only means anything to a single reading. It is sent for the one-shot call
  # alone, the way it always was, and never beside stream => 1.
  $params{'one-shot'} = 1 unless $stream;

  my $endpoint = 'GET /containers/' . $id . '/stats';

  # The guard has to sit on both sides of the callback split, because the same
  # body arrives either way: buffered it is the return value, streamed it goes
  # to the callback and is never returned at all. Wrapping puts the check in
  # front of the caller's callback, so no caller is handed the error object as
  # though it were a reading. Only a CodeRef is wrapped -- anything else is
  # passed through untouched, so the transport still raises its own "on_event
  # option must be a CodeRef" instead of this method dying on a closure it
  # built around a non-callback.
  my $on_event = $opts{on_event};
  if (exists $opts{on_event} && ref $on_event eq 'CODE') {
    my $cb = $on_event;
    $on_event = sub {
      my ($reading, $stop) = @_;
      $self->_assert_no_podman_error($endpoint, $reading);
      return $cb->($reading, $stop);
    };
  }

  my $result = $self->client->get("/containers/$id/stats",
    params => \%params,
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $on_event )
      : $stream            ? ( ndjson   => 1 )
      : (),
  );

  # A HashRef for the one-shot call and an ArrayRef of readings for
  # stream => 1 without a callback -- both are the buffered body and both can
  # be that error object. With a callback the return value is the summary
  # HashRef, which carries none of the three keys and passes untouched.
  $self->_assert_no_podman_error($endpoint, $_)
    for ref $result eq 'ARRAY' ? @$result : ($result);

  return $result;
}


sub changes {
  my ($self, $id) = @_;
  croak "Container ID required" unless $id;
  my $result = $self->client->get("/containers/$id/changes",

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

    $docker->containers->start($result->{Id});

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

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

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

    # Attach one-way: replays the same frames and returns (stream => 0 by
    # default -- stream => 1 on a stopped container never returns). On Podman,
    # attaching to a container that has ALREADY EXITED destroys its exit
    # status; use logs() for that case, see attach()
    my $attached = $docker->containers->attach($result->{Id});

    # Copy a file out, and a tar archive in (what docker cp is built on)

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

Create a new container. Returns hashref with C<Id> and C<Warnings>.

The C<name> parameter is extracted and passed as query parameter. All other
parameters are Docker container configuration (see Docker API documentation).

Common config keys: C<Image>, C<Cmd>, C<Env>, C<ExposedPorts>, C<HostConfig>.

Boolean flags 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. This applies to the top-level flags (C<Tty>, C<OpenStdin>,
C<AttachStdin>, C<AttachStdout>, C<AttachStderr>, C<StdinOnce>,
C<NetworkDisabled>, C<ArgsEscaped>) and to the C<HostConfig> flags
(C<Privileged>, C<PublishAllPorts>, C<ReadonlyRootfs>, C<AutoRemove>, C<Init>,
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

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

L<API::Docker::Error::HTTP/status> and not on the message. For one and the
same stopped container:

=over

=item * Docker -- C<cannot kill container: E<lt>nameE<gt>: container E<lt>idE<gt> is
not running>

=item * Podman -- C<can only kill running containers. E<lt>idE<gt> is in state
exited: container state improper>. C<container state improper> is Podman's
separate C<cause> field, reachable as C<< $err->data->{cause} >>, not a
phrase Docker uses anywhere

=back

The 404 differs too, and on Docker it differs I<per endpoint>: C<kill>
against a missing ID answers C<cannot kill container: E<lt>nameE<gt>: No such
container: E<lt>nameE<gt>> where L</inspect> answers the bare C<No such container:
E<lt>nameE<gt>>. Podman sends one sentence for both.

Options:

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

=item * C<volumes> - Remove associated volumes

=item * C<link> - Remove specified link

=back

=head2 logs

    my $frames = $containers->logs($id, tail => 100, timestamps => 1);

    # stdout and stderr, in the order the engine emitted them
    my $text = join '', map { $_->{data} } @$frames;

    # stderr only
    my @errors = grep { $_->{stream} eq 'stderr' } @$frames;

Get container logs. Returns an ArrayRef of frames, each a HashRef with
C<stream> and C<data>:

    [ { stream => 'stdout', data => "OUT\n" },
      { stream => 'stderr', data => "ERR\n" } ]

A container created without a TTY multiplexes stdout and stderr into a single
framed stream, and this method demultiplexes it -- without that, the 8-byte
frame headers end up in the caller's log text. A container created B<with> a
TTY writes to one pty and the engine sends no frame headers, so its whole
output arrives as a single frame with C<< stream => 'raw' >>: with a TTY there
is no stdout/stderr distinction left to report. C<stream> is always a plain
string, so C<< $_->{stream} eq 'stderr' >> is safe on any frame.

Framing is detected from the response bytes, because the engine's
C<Content-Type> cannot be trusted for it -- see
L<API::Docker::Role::HTTP/"Detecting a framed stream"> for the rule and its one
failure mode.

Options:

=over

=item * C<follow> - Keep the connection open and send new output as the
container writes it. Only usable with C<on_frame>; see below

=item * C<stdout> - Include stdout (default 1)

=item * C<stderr> - Include stderr (default 1)

=item * C<since> - Show logs since timestamp

=item * C<until> - Show logs before timestamp

=item * C<timestamps> - Include timestamps

=item * C<tail> - Number of lines from end (e.g., C<100> or C<all>)

=item * C<tty> - Set to 1 when the container was created with a TTY and its

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

=head2 attach

    my $frames = $containers->attach($id);

    my $text = join '', map { $_->{data} } @$frames;

Attach to a container's streams and return everything they produced, as an
ArrayRef of frames in the same shape L</logs> returns:

    [ { stream => 'stdout', data => "OUT\n" },
      { stream => 'stderr', data => "ERR\n" } ]

A container created without a TTY multiplexes its output into one framed
stream, which this method demultiplexes; one created with a TTY arrives as a
single C<< stream => 'raw' >> frame. See L</logs> and
L<API::Docker::Role::HTTP/"Detecting a framed stream">.

B<The container must be running.> Attaching to one that has already exited
destroys its exit status on Podman, so this method checks first and croaks
rather than attaching -- read the output of a finished container with
L</logs>. Both halves of that are worth knowing before the call: see

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

connection -- the response carries no C<Content-Length> and no chunked
terminator -- so HTTP framing cannot signal the end either. The transport
reads until EOF, there is no EOF, and the call hangs. C<on_frame> does not
help: nothing will ever call C<< $stop->() >>.

Measured on Podman 5.4.2 (API 1.41), all four against one and the same
container:

=over

=item * C<?logs=1&stdout=1&stderr=1&stream=0>, exited container -- 200, the
frames, connection closed after 13 ms

=item * C<?logs=1&stdout=1&stderr=1&stream=1>, exited container -- 200, the
same frames, then hangs

=item * the same with C<Upgrade: tcp> -- 101 UPGRADED, the same frames, still
hangs

=item * C<?stream=1> while the container is still B<running> and exits three
seconds later -- closes cleanly after 3 s

=back

B<Docker does exactly the same, and that is measured now too.> Against Docker
29.7.2 (API 1.55): C<?logs=1&stdout=1&stderr=1&stream=1> on an exited
container was still open when a 10 s probe gave up, and
C<?logs=1&stdout=1&stderr=1&stream=0> answered 200 with byte-identical frames
and closed in half a millisecond. So the hang is not a Podman quirk to be
worked around -- it is what both engines do with a subscription whose only
terminator is already in the past, on an endpoint whose reference promises a
close in neither direction. It is unspecified behavior on both, which is the
case for the C<< stream => 0 >> default rather than an argument against it.

One more measured difference: Podman refuses C<< stream => 0 >> together with
C<< logs => 0 >> outright, with B<400> C<at least one of Logs or Stream must
be set>, rather than answering an empty 200.

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

L</"The defaults follow the engine">

=item * C<logs> - Replay what the container has already written. Default
B<1>, so the call returns something without subscribing; combined with
C<< stream => 1 >> the replay comes first and then transitions seamlessly
into the live output. C<< logs => 0 >> without C<< stream => 1 >> is the
combination the engine refuses (400 on Podman)

=item * C<stdout> - Attach stdout. Default 1 (engine default: false)

=item * C<stderr> - Attach stderr. Default 1 (engine default: false)

=item * C<stdin> - Attach stdin. Sent as asked, but nothing can be written to
it here; see above

=item * C<tty> - Set to 1 when the container was created with a TTY and its
output is binary, to skip demultiplexing. Same meaning as in L</logs>, and
with C<on_frame> the same promise

=item * C<on_frame> - CodeRef called with each frame as it arrives, instead of
the ArrayRef being collected and returned. Same contract as in L</logs>

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

callback ended the stream and 0 when the daemon did. Nothing is accumulated.
See L<API::Docker::Role::HTTP/"Streaming a response as it arrives">.

B<Without a callback, C<< stream => 1 >> blocks> until the container stops or
the daemon closes the connection: the whole response is read before anything
is parsed. It then returns an ArrayRef of readings rather than the single
HashRef the one-shot call returns, which is the other reason to pass a
callback instead.

Unlike L<API::Docker::API::System/events>, this does not turn the stream's
error check off. C</events> is a feed of engine records, where an
C<errorDetail> object would still be data; a stats stream is one container's
readings, and the transport's default is to croak on a failure reported inside
a 200 body (L<API::Docker::Role::HTTP/"Failure inside a 200 response">).

The default is kept on a measurement rather than on that analogy. Against
Podman 5.4.2 (API 1.41) every object a running container's stream carries is
a complete reading -- C<read>, C<cpu_stats>, C<memory_stats>, C<networks> and
the rest -- and killing the container and then removing it while the stream
was open ended the stream on a whole reading, with nothing appended after it.
No C<errorDetail> was sent in either case, and the Engine API reference names
that key for C</build>, C</images/create> and C</images/{name}/push> alone.
So the check has no legitimate reading here it could turn into a croak, and
that -- not an unexamined default -- is why it stays on.

Options:

=over

=item * C<stream> - Ask for a reading per sampling cycle instead of one.
Defaults off and is always sent, so a call with no options is the single
reading it has always been

=item * C<on_event> - CodeRef called with each reading as it arrives, instead
of them being collected and returned; see above

=back

=head2 A container that is not running: a croak on Podman, zeros on Docker

Neither engine answers this with an HTTP error, and they agree on nothing
else. Measured on Podman 5.4.2 (API 1.41) and Docker 29.7.2 (API 1.55), same
machine, one container that had exited.

B<Podman sends an error object inside the 200> -- chunked, for the one-shot
call and for C<< stream => 1 >> alike:

    { cause    => 'container is stopped',
      message  => 'container is stopped',
      response => 500 }

B<This method now croaks on it>, with an L<API::Docker::Error::HTTP> carrying
C<< ->status == 500 >> -- the code Podman named in C<response> -- and the
object itself as C<< ->data >>, so C<< $err->data->{cause} >> stays readable.
That is a B<change>: up to and including the previous release the one-shot
call returned this HashRef where a reading was expected, and an C<on_event>
callback was handed it as though it were one. The check runs on the buffered
return value and in front of the callback alike, and only on that exact
shape -- a HashRef inside a 2xx carrying all three of C<cause>, C<message>
and C<response> lower-cased, with C<response> reading as an integer >= 400.
Nothing else in this distribution, in its fixtures, or in a sweep of fifteen
read endpoints per engine carries even one of those keys lower-cased at the
top level. The rule is deliberately B<not> case-insensitive: a I<successful>
L</wait> answers with a top-level C<Error> key, and a looser rule would
report every one of those as a failure.

B<Docker sends a structurally valid reading with everything zeroed> -- 200,
about 825 bytes, no error anywhere in it:

    { id => '...', name => '/...', os_type => 'linux',
      read      => '0001-01-01T00:00:00Z',
      cpu_stats => { cpu_usage => { total_usage => 0, ... }, ... },
      memory_stats => {}, pids_stats => {}, num_procs => 0, ... }

So the advice this section used to give -- test for C<read> or C<cpu_stats>
before using what comes back -- is B<wrong on Docker>: both keys are present,
both look plausible, the test passes, and the caller uses zeros as though
they were a measurement. The markers in that body are Go's zero time in

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

cheap Docker-specific second opinion, not as the test.

=head2 C<< stream => 1 >> on a container that is not running

The same call in follow mode fails in B<opposite> directions on the two
engines, and the standing advice for a streaming endpoint -- bound the
window -- does not help, because there is no window to bound:

=over

=item * B<Podman> sends the one error object above and closes at once. Without
a callback that arrives as a one-element ArrayRef; either way this method
croaks it

=item * B<Docker> streams one zero-filled reading B<per second, forever>.
Measured: 5 objects in a 5 s probe, 12 in a 12 s probe, the connection never
closed by the engine. No container exit will ever end it -- the container had
already exited when the call was made

=back

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

condition: "...">, Podman C<failed to parse query parameter 'condition' ...>),
and an unknown container ID croaks B<404>

=back

Podman also answers C<< StatusCode => -1 >> for a container whose exit status
L</attach> has destroyed -- see
L</"On Podman this destroys a stopped container's exit status">. The value is
that engine's sentinel for "no status", not an exit code.

This endpoint is also the reason the error check on L</stats> matches
C<cause>, C<message> and C<response> case-sensitively: a I<successful> wait
is a 2xx body with a top-level C<Error> key in it, and a rule matching
C<error> case-insensitively would turn every one of them into a failure.

Options:

=over

=item * C<condition> - What to wait for: C<not-running> (the engine's own
default), C<next-exit> or C<removed>. Sent only when given

=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/Containers.pm  view on Meta::CPAN

=back

=head2 stat_archive

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

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

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

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

=over

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


has client => (
  is       => 'ro',
  required => 1,
  weak_ref => 1,
);


# Only when the caller asked for it, like the plugin family and unlike
# /images/{name}/push. The distribution router decodes the header and
# discards the error -- the same "ignore invalid AuthConfig" pattern the
# plugin routes use -- so a public image is looked up anonymously, with no
# header at all. Not verifiable on this machine: Podman serves no route here.
sub _auth_headers {
  my ($self, $opts) = @_;
  return () unless defined $opts->{auth};
  return (headers => { 'X-Registry-Auth' => $self->_registry_auth_header($opts->{auth}) });
}

sub inspect {
  my ($self, $name, %opts) = @_;

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


  # A caller's own response HashRef is reused rather than shadowed, so
  # passing one through this method still fills it.
  my $res = ref $opts{response} eq 'HASH' ? $opts{response} : {};
  my $ok = eval {
    $self->inspect($name, %opts, response => $res);
    1;
  };
  return 1 if $ok;

  my $err = $@;
  die $err unless ($res->{status} // 0) == 404;
  croak __PACKAGE__ . '->exists cannot ask this engine: ' . $err
    if $err =~ $NO_SUCH_ROUTE;
  return 0;
}



1;

__END__

=pod

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

the engine parses, and percent-encoding them breaks the reference.

=head2 A 404 means two different things

The endpoint answers 404 both when the registry does not have the reference
and when the engine has no such route, and the two want opposite handling.
The split here is:

=over

=item * L</inspect> is the endpoint, and croaks on B<any> error status, 404
included, the way every other method in this distribution does.

=item * L</exists> is the question, and answers it: true, false, or a croak
when the engine could not ask the registry at all.

=back

L</exists> exists because answering "no" to everything is the failure this
class was added to remove -- see the Podman note below -- and a predicate
that cannot fail loudly would have reintroduced it one layer up.

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

    my $descriptor = $distribution->inspect('private/app:1.0', auth => $auth);

Ask the registry for the manifest descriptor of an image reference. The
daemon performs the lookup; nothing is pulled and no local image is touched.

Returns a HashRef with C<Descriptor> -- C<MediaType>, C<digest>, C<size>,
C<URLs> -- and C<Platforms>, the list of C<{ Architecture, OS, ... }> the
reference resolves to.

B<A missing reference croaks.> This method is the endpoint, so it inherits
the transport's rule that any status at or above 400 is an error, and the
registry's "no such reference" is a 404 like any other. Use L</exists> for
the predicate, or eval and read the status:

    my %res;
    my $d = eval { $distribution->inspect($ref, response => \%res) };
    # $res{status} == 404 here means the registry said no *or* the engine
    # has no such route -- see L</exists>, which separates the two.

Options:

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


Whether the registry has that image reference. Returns a true value when the
lookup succeeded, a false one when the registry answered 404, and B<croaks>
otherwise -- including when the engine has no C</distribution> route, so that
an engine which cannot answer the question never answers it with "no".

Takes the same options as L</inspect>. Callable without an C<eval>: every
outcome it returns is an answer from the registry, and everything else is
loud.

The distinction rests on the engine's error message, which is the only thing
that separates the two 404s -- C<is not supported> from Podman,
C<page not found> from Docker's own router. A wording neither recognises is
read as the registry's answer, i.e. as false, which is the behaviour a plain
"404 means no" would have had anyway.

=head1 SEE ALSO

=over

=item * L<API::Docker> - Main Docker client

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

  is       => 'ro',
  required => 1,
  weak_ref => 1,
);


# The ExecConfig booleans of spec/v1.51.yaml. The engine rejects a number for
# any of them, so 1/0 is normalised to a JSON boolean on the way out; a caller
# may still pass 1/0 (or a JSON boolean) and it goes out correctly either way.
my @EXEC_CONFIG_BOOLS = qw(
  AttachStdin AttachStdout AttachStderr Tty Privileged
);

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

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

version 0.004

=head1 SYNOPSIS

    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

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


=head2 client

Reference to L<API::Docker> client. Weak reference to avoid circular dependencies.

=head2 create

    my $exec = $exec->create($container_id,
        Cmd          => ['/bin/sh', '-c', 'echo hello'],
        AttachStdout => 1,
        AttachStderr => 1,
        Tty          => 0,
    );

Create an exec instance. Returns hashref with C<Id>.

Required config: C<Cmd> (ArrayRef of command and arguments).

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:

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

Returns an ArrayRef of build events, one per object in the engine's
newline-delimited JSON stream, even when the stream carried a single object
(C<< q => 1 >> produces exactly one). A successful build returns; a failed one
croaks.

    my $events = $images->build(context => $tar, t => 'myapp:latest');
    my ($aux) = grep { $_->{aux} } @$events;
    my $image_id = $aux->{aux}{ID};

The engine answers a failed build with HTTP 200 and reports the failure as an
C<errorDetail> object inside the stream, so nothing about the response status
says the build broke. This method used to return that stream like any other
and leave the scan to the caller, which meant a caller who did not know to
scan reported a broken build as a success. It now croaks with an
L<API::Docker::Error::Stream> instead:

    my $events = eval { $images->build(context => $tar, t => 'myapp:latest') };
    if (my $err = $@) {
        warn "$err";               # the reason, with Carp's location suffix
        for my $event (@{ $err->events }) {   # the build output up to the failure
            print $event->{stream} if defined $event->{stream};
        }
    }

The exception stringifies to what a plain C<croak> would have produced, so
existing C<eval>-and-inspect-C<$@> code needs no change.

Options:

=over

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

callback ended the stream and 0 when the daemon did. Nothing is accumulated,
so a caller that wants the C<aux> event with the image id in it must keep that
event itself as it goes by. See
L<API::Docker::Role::HTTP/"Streaming a response as it arrives">.

The same section applies to L</pull>, L</push> and L</load>, which take
C<on_event> on the same terms.

=head3 A failed build still croaks, one event earlier

The C<errorDetail> check runs either way, so a failed build croaks with an
L<API::Docker::Error::Stream> on both paths. What differs is when, and what
the exception carries:

=over

=item * Buffered, the stream is scanned once it is complete, and
C<< $err->events >> is the B<whole> event list -- all the build output that
led up to the failure.

=item * Streamed, the check runs per event, so the croak happens at the event
that reports the failure rather than when the daemon eventually closes. The
exception then carries B<that one event> alone: a callback stream keeps no
history, having handed every earlier event to the callback already. The
failing event itself is not delivered.

=back

So a caller that reads the progress out of C<< $err->events >> must, on this
path, collect it in the callback instead:

    my @output;
    my $summary = eval {
        $images->build(context => $tar, t => 'myapp:latest',
            on_event => sub { push @output, $_[0] });
    };
    if (my $err = $@) {
        warn "$err";               # the reason, as before
        # $err->events is the failing event; @output is what preceded it
    }

=head2 pull

    my $events = $images->pull(fromImage => 'nginx', tag => 'latest');
    my $events = $images->pull(fromImage => 'nginx:1.25');   # tag rides in the name
    my $events = $images->pull(fromImage => 'alpine@sha256:...');  # by digest

Pull an image from a registry.

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


Returns an ArrayRef of progress events, one per object in the engine's
newline-delimited JSON stream, even when the stream carried a single object.

A failed pull croaks either way, but which way depends on the engine, so do
not write code that expects one of them:

=over

=item * Docker reports it in the stream. The response is HTTP 200 and the
failure is an C<errorDetail> object among the progress events; this method
croaks with an L<API::Docker::Error::Stream>, whose C<< ->events >> holds the
progress that preceded the failure.

=item * Podman reports it in the status line. Measured against the rootless
socket (5.4.2, API 1.41): pulling a repository that does not exist answers
C<403 Forbidden> with C<< {"message":"denied: requested access to the resource
is denied"} >>, and an existing repository with a missing tag answers
C<404 Not Found> with C<< {"message":"manifest unknown: manifest unknown"} >>.
Neither reaches the stream at all -- the transport's own status handling
croaks with an L<API::Docker::Error::HTTP> -- which is that same string to

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


Push an image to a registry. Optionally specify C<tag>.

Returns an ArrayRef of progress events, one per object in the engine's
newline-delimited JSON stream, even when the stream carried a single object.

A failed push croaks, by one of two routes depending on the engine -- an
unauthorised push to a private registry is the common case, and it is exactly
the one that must not be reported as a success.

Docker reports it inside a 200 stream as an C<errorDetail> object, which
croaks with an L<API::Docker::Error::Stream> carrying the progress events.
Podman puts an C<errorDetail> body behind a real error status instead:
measured against the rootless socket (5.4.2, API 1.41), a push to an
unreachable registry answers C<500 Internal Server Error> with
C<< {"errorDetail":{"message":"... connection refused"},"error":"..."} >>, so
the transport's status handling croaks with an L<API::Docker::Error::HTTP> --
which is that same string to anything inspecting C<$@> as text -- before the
stream is ever decoded. That body carries no C<message> key, so the whole
JSON object ends up as the croak text.

Either way the failure is loud. Inspect C<$@> as a string rather than testing
for the exception class, which only the first route produces.

The Docker Engine requires an C<X-Registry-Auth> header on every push,
even for anonymous attempts; the header is always sent. Pass C<auth> as

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

body stays newline-delimited JSON and the return stays an ArrayRef either way.
B<Podman ignores it entirely> -- measured against 5.4.2 (API 1.41), C<quiet>
unset, C<0> and C<1> all produce the identical single
C<< {"stream":"Loaded image: ..."} >> object. Should an engine answer a quiet
load with a body of no bytes at all, the transport still returns C<[]>, not
C<undef> -- the C<ndjson> branch in L<API::Docker::Role::HTTP/_request> runs
before the empty-body check that would return C<undef>, so a caller that
iterates the result unconditionally needs no guard for this case.

A failed load croaks, but by which route depends on the engine, the same split
L</pull> and L</push> have. Docker reports it as an C<errorDetail> object
inside a 200 stream, which croaks with an L<API::Docker::Error::Stream>
carrying the events. Podman reports it in the status line instead: measured
against 5.4.2, a body that is not an image archive answers C<500 Internal
Server Error> with C<< {"message":"failed to load image: payload does not
match any of the supported image formats: ..."} >>, and the transport's status
handling croaks with an L<API::Docker::Error::HTTP> -- which is that same
string to anything inspecting C<$@> as text -- before any stream is decoded.
Inspect C<$@> as a string rather than testing for the exception class.

The archive is sent as one buffered request body, so loading a large image

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

=head2 commit

    my $result = $images->commit(
        container => $container_id,
        repo      => 'myapp',
        tag       => 'snapshot',
        comment   => 'after the migration ran',
    );
    my $image_id = $result->{Id};

    # With a config override and Dockerfile instructions
    $images->commit(
        container => $container_id,
        repo      => 'myapp',
        tag       => 'v2',
        config    => { Cmd => [ '/bin/sh' ], Labels => { built => 'here' } },
        changes   => [ 'EXPOSE 8080', 'LABEL stage=release' ],
    );

Create an image from a container's current filesystem. This is the one
image-producing path that does not go through a build context, and it is how a

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

=item * C<comment> - Commit message stored in the image history

=item * C<author> - Author, e.g. C<< Jane <jane@example.com> >>

=item * C<pause> - Pause the container while committing (engine default is true)

=item * C<changes> - Dockerfile instructions to apply to the new image, as a
single string or an ArrayRef of them; an ArrayRef is joined with newlines,
which is what the engine's parser expects

=item * C<config> - HashRef of container configuration to override on the new
image (C<Cmd>, C<Env>, C<Labels>, C<ExposedPorts>, ...), sent as the request
body. Measured against Podman 5.4.2: C<Cmd> replaces the container's, C<Env>
is merged onto the environment the container inherited, and a C<Labels> here
lands alongside a C<LABEL> given in C<changes> -- the two are applied
together, not one instead of the other

=back

=head2 build_prune

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

interchangeable: L</prune> deletes unused I<images>, this deletes the
intermediate I<build cache> that L</build> writes. Neither touches the other's
storage, and on a machine that builds often the build cache is usually the
larger of the two.

Returns the raw daemon response, a HashRef with C<CachesDeleted> and
C<SpaceReclaimed>.

B<Podman does not implement this endpoint.> Measured against 5.4.2 (API 1.41):
C<POST /build/prune> answers C<404 Not Found> with a C<text/plain> body of
C<Not Found> -- not the JSON C<< {"message":...} >> shape its other errors use
-- at every version prefix tried, and there is no C<libpod> equivalent either.
The transport croaks with C<Docker API error (404): Not Found>, the plain body
verbatim, because it is not JSON to unwrap. A caller that must work on both
engines has to treat that 404 as "no build cache to clear here" rather than as
a transport fault.

Options:

=over

=item * C<keep_storage> - Bytes of cache to keep. Sent as the engine's
C<keep-storage>, which is also accepted as the option name; the underscore

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

  my ($self, $class, $data) = @_;
  return $class->from_data($data, client => $self->client);
}

sub _wrap_list {
  my ($self, $class, $list) = @_;
  return [ map { $self->_wrap($class, $_) } @$list ];
}

# The plugin router calls registry.DecodeAuthConfig on the header and
# discards the error -- "Ignore invalid AuthConfig to increase compatibility
# with the existing API" -- so unlike /images/{name}/push, which rejects a
# missing header, an anonymous plugin operation needs no header at all. It is
# sent only when the caller asked for it. The encoding is
# API::Docker::Role::RegistryAuth; what stays here is the policy.
sub _auth_headers {
  my ($self, $opts) = @_;
  return () unless defined $opts->{auth};
  return (headers => { 'X-Registry-Auth' => $self->_registry_auth_header($opts->{auth}) });
}

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


L</privileges> is the first call, L</install> the second:

    my $privileges = $docker->plugins->privileges('vieux/sshfs:latest');
    # inspect $privileges here -- it is an ArrayRef of
    #   { Name => 'network', Description => '...', Value => ['host'] }
    $docker->plugins->install('vieux/sshfs:latest', privileges => $privileges);

C<install> B<requires> C<privileges> and croaks without it, which is stricter
than the engine: the daemon's own body parser treats a missing body as an
empty privilege list rather than an error, so a blind install of a plugin
that happens to demand nothing would quietly succeed and one that demands
C<network: host> would fail with an error naming neither. Passing
C<< accept_privileges => 1 >> makes C<install> perform the first call itself
and hand the answer straight back -- a blanket grant, spelled out at the call
site so it is greppable.

The same applies to L</upgrade>, which takes the same body.

=head2 Not available on Podman

Measured against the rootless Podman socket (5.4.2, API 1.41): B<none> of the
C</plugins> endpoints exist there. C<< GET /v1.41/plugins >> answers
C<404 Not Found> with
C<< {"cause":"","message":"Path /v1.41/plugins is not supported","response":0} >>
(the C<1.41> there is this client's negotiated API version, echoed back from
the request path -- it moves with negotiation, not a fixed string in the
daemon's error text),
and every other path in this family -- C</plugins/privileges>,
C</plugins/pull>, C</plugins/{name}/json>, C</plugins/{name}/enable> and the
rest -- answers a bare C<404 Not Found> as C<text/plain>, meaning the compat
layer has no route registered for them at all. Managed plugins are a Docker
feature; Podman's own plugin model is not served here. Everything in this
class therefore needs a real Docker daemon.

=head2 What this class returns

L</list> and L</inspect> return L<API::Docker::Type::Plugin> objects carrying

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

        },
    );

    $summary;   # { delivered => 18, stopped => 0 }

With a callback the return value is that summary HashRef, not the events:
C<delivered> is how many went to the callback, C<stopped> is 1 when the
callback ended the stream and 0 when the daemon did. Nothing is accumulated.
See L<API::Docker::Role::HTTP/"Streaming a response as it arrives">.

The C<errorDetail> check runs on this path too, per event rather than over the
finished list, so a failure inside the 200 stream still croaks with an
L<API::Docker::Error::Stream> -- at the event that reports it, and carrying
that one event alone rather than the whole stream. It is the difference
L<API::Docker::API::Images/"A failed build still croaks, one event earlier">
describes, and it applies here identically. A caller that wants the progress
that preceded a failure must collect it in the callback.

L</upgrade> and L</push> take C<on_event> on the same terms.

A failed install croaks by one of two routes, exactly as
L<API::Docker::API::Images/pull> does, because the daemon commits to HTTP 200
the moment it flushes the first progress object. A failure before that point
arrives as a real error status -- C<incorrect privileges> is reported this
way, since it is decided before anything is pulled -- and one after it
arrives as an C<errorDetail> object inside the 200 stream, which croaks with
an L<API::Docker::Error::Stream>. C<eval> and inspect C<$@> as a string
rather than testing for the exception class.

=head2 inspect

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

Get detailed information about an installed plugin. Returns an

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


=item * C<timeout> - Seconds to wait for the plugin to come up, C<0> for no
timeout (the default)

=back

C<timeout> is B<always> sent, whether or not the caller passes it. The Engine
API reference gives it a default of C<0>, but the daemon has none: it reads
the raw query value and parses it with Go's C<strconv.Atoi>, so an absent
parameter is parsed as the empty string and the request fails with
C<strconv.Atoi: parsing "": invalid syntax> as an invalid-parameter error.
This is the one endpoint in the family where omitting an optional parameter
is fatal.

=head2 disable

    $plugins->disable('vieux/sshfs:latest');
    $plugins->disable('vieux/sshfs:latest', force => 1);

Disable an enabled plugin. Returns C<undef>.

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

}


sub events {
  my ($self, %opts) = @_;
  my %params;
  $params{since}   = $opts{since}   if defined $opts{since};
  $params{until}   = $opts{until}   if defined $opts{until};
  $params{filters} = $self->_normalise_filters($opts{filters})
    if defined $opts{filters};
  # croak_on_error => 0: /events is a feed, not the progress of one
  # operation. An object in it describes something that happened on the
  # engine, so it is data even if it ever carries an errorDetail key -- this
  # call must never croak on ordinary event traffic. It holds for the
  # callback path too, where the check would otherwise run per event.
  #
  # exists, not truth: `on_event => $cb` with an unset $cb is a caller bug,
  # and falling back to the buffered path for it would answer an unbounded
  # feed by hanging. Handed over as it is, the transport says so instead.
  return $self->client->get('/events',
    params         => \%params,
    croak_on_error => 0,
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $opts{on_event} ) : ( ndjson => 1 ),
  );
}


sub df {
  my ($self) = @_;
  return $self->client->get('/system/df',
    %{ $self->_request_options },

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

        until   => 1234567900,
        filters => { type => ['container'] },
    );

Get events from the Docker daemon. Returns an ArrayRef of events, one per
object in the engine's newline-delimited JSON stream, even when the stream
carried a single object.

Unlike C<< $docker->images->build >>, C<pull> and C<push>, this method never
croaks on the content of the stream. Those report the outcome of one
operation, so an C<errorDetail> object in their stream means that operation
failed; C</events> is a feed, and an object in it is a record of something
that happened on the engine, never a failure of this call. Only transport and
HTTP errors croak here.

B<Bound the window with C<until>, or pass C<on_event>.> Without a callback the
transport buffers the whole response before parsing, so an unbounded call
blocks until the daemon closes the connection, which for a live event stream
is never.

Options:

=over

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

callback ended the feed and 0 when the daemon did. Nothing is accumulated --
a feed that runs for a day must not cost memory in proportion to how long it
ran, and the callback has been handed every event already. See
L<API::Docker::Role::HTTP/"Streaming a response as it arrives">.

Measured against the rootless Podman socket (5.4.2, API 1.41): with C<since>
and no C<until>, this returned in 0.3 seconds as soon as the callback said
stop, where the same call without one was still running when it was killed
after 22 seconds.

The callback never croaks on the content of the feed either: C<croak_on_error>
is off here on both paths, for the reason above.

=head2 df

    my $usage = $system->df;

Get data usage information (disk usage by images, containers, and volumes).

Returns hashref with C<LayersSize>, C<Images>, C<Containers>, and C<Volumes> arrays.

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

    );

    # Or hand over the same auth argument images->push takes
    $system->auth(auth => $auth);

Check a set of registry credentials against the registry, without pulling or
pushing anything. Returns the decoded C<< POST /auth >> response, a HashRef
with C<Status> (C<Login Succeeded>) and, where the registry issues one,
C<IdentityToken>.

B<Bad credentials croak.> The engine answers a failed check with an error
status, and the transport croaks on any status at or above 400, so a
successful return I<is> the answer -- there is no false value to test. That
is what makes this useful as a pre-flight check: call it before building and
tagging an image, and a stale credential fails the run where it is cheap
rather than halfway through a push.

To tell one failure from another, eval and read the status:

    my %res;
    eval { $docker->system->auth(auth => $auth, response => \%res); 1 }

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

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;


has message => (
  is       => 'ro',
  required => 1,

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


API::Docker::Error::HTTP - Error status returned by the Docker Engine on the status line

=head1 VERSION

version 0.004

=head1 SYNOPSIS

    eval { $docker->containers->kill($id) };
    if (my $err = $@) {
        # Behaves exactly like the string it replaces ...
        warn "kill failed: $err";

        # ... and carries the status code, so 404 and 409 are told apart
        # without matching on prose the engine is free to change.
        if (ref $err && $err->isa('API::Docker::Error::HTTP')) {
            return    if $err->status == 404;   # already gone
            sleep 1   if $err->status == 409;   # wrong state, retry
        }
    }

=head1 DESCRIPTION

L<API::Docker::Role::HTTP> croaks with an object of this class whenever the
engine answers a request with a status of 400 or above.

The reason it exists is that the croak B<text> is not an interface. What the
engine puts in the error body is engine-specific prose: a kill against a
stopped container answers 409 with C<can only kill running containers. E<lt>idE<gt> is
in state stopped: container state improper> on rootless Podman 5.4.2, while
Docker's own example for the same case is C<Container E<lt>idE<gt> is not running> --
a different body shape and entirely different wording. A caller that has to
tell "no such container" from "wrong state" apart had no choice but to match
that prose. L</status> is the same distinction as a number the engine
documents.

=head2 It is still the string it replaces

Everything this class replaces was a plain C<croak> of a string, and callers
rely on that. It overloads stringification (with C<< fallback => 1 >>, so
comparison, concatenation, C<sprintf> and matching all work through it) and
produces byte for byte what C<croak> died with before: the same
C<Docker API error (STATUS): REASON> text, followed by Carp's own
C< at FILE line N.> location suffix, naming the same frame. Code written
against the old behaviour keeps working unchanged:

    eval { $docker->containers->inspect($id) };
    if ($@) {
        (my $reason = $@) =~ s/\s+at\s+\S+\s+line\s+\d+\.?//g;   # still works
        die "no good: $@";                                        # still works
        warn $@ if $@ =~ /404/;                                   # still works
    }

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


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.

=head2 location

Carp's location suffix (C< at FILE line N.\n>), captured at the point the
error was raised so it names the same frame a plain C<croak> would have named.
Kept apart from L</message> so a caller can have the reason without it.

=head2 status

The HTTP status code: C<404>, C<409>, C<500>. This is the whole point of the
class -- the one part of an engine error that is documented per endpoint and
identical across engines.

It arrives off the status line as a string of digits, exactly as
C<< $res{status} >> from L<API::Docker::Role::HTTP>'s C<response> option does,
so compare it numerically (C<< $err->status == 404 >>) rather than relying on
a type.

=head2 reason

The status line's reason phrase as the engine sent it (C<Not Found>,
C<Conflict>). Informational: it comes off the wire, not from a table, so it is
no more of a stable interface than the error body's prose. Branch on
L</status>.

=head2 body

The response body verbatim, before any decoding -- the bytes the engine sent.
Empty string when it sent none.

=head2 data

The decoded body, or C<undef> when there was nothing to decode or decoding
failed. Usually the HashRef the engine's C<{"message":...}> shape decodes to,
which is where an engine-specific extra such as Podman's C<cause> key can be
read; an array-shaped body decodes to an ArrayRef, so check the C<ref> before
subscripting it.

=head2 as_string

    my $text = $err->as_string;   # same as "$err"

The message and the location suffix, concatenated. This is what the
stringification overload returns.

=head1 SEE ALSO

=over

=item * L<API::Docker::Role::HTTP> - Raises this error; see its C<response>
option for the status of a request that did not fail

=item * L<API::Docker::Error::Stream> - Raised instead for a failure reported
inside a stream the daemon already answered with HTTP 200

=back

=head1 SUPPORT

=head2 Issues

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

# 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 => (
  is       => 'ro',
  required => 1,

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


API::Docker::Error::Stream - Failure reported inside a Docker Engine progress stream

=head1 VERSION

version 0.004

=head1 SYNOPSIS

    my $events = eval { $docker->images->build(context => $tar, t => 'app:v1') };
    if (my $err = $@) {
        # Behaves exactly like the string it replaces ...
        warn "build failed: $err";

        # ... and carries the whole stream that led up to the failure.
        if (ref $err && $err->isa('API::Docker::Error::Stream')) {
            for my $event (@{ $err->events }) {
                print $event->{stream} if defined $event->{stream};
            }
        }
    }

=head1 DESCRIPTION

The engine's streaming endpoints -- C</build>, C</images/create> (pull) and
C</images/{name}/push> -- report a failed operation as an C<errorDetail> object
B<inside> a stream that was already answered with HTTP 200. A client that only
treats status >= 400 as an error hands a broken build back to its caller as a
success.

L<API::Docker::Role::HTTP> therefore croaks with an object of this class as
soon as an C<errorDetail> event appears in such a stream. The object exists
purely so the progress output is not lost with the failure: the complete event
list, error event included, is available through L</events>.

=head2 It is still the string it replaces

Everything else in this distribution croaks plain strings, and callers rely on
that. This class overloads stringification (with C<< fallback => 1 >>, so
comparison, concatenation, C<sprintf> and matching all work through it) and
produces exactly what C<croak> would have died with: the reason, followed by
Carp's own C< at FILE line N.> location suffix. Code written against the old
behaviour keeps working unchanged:

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


Note that a substitution B<on> C<$@> replaces the object in that scalar with a
plain string, as it would with any overloaded object, so take a copy first if
L</events> is still wanted afterwards.

The boolean overload is explicit rather than derived from the string, so an
engine message of C<0> cannot make a live exception test false.

=head2 message

The reason on its own, without the location suffix: the C<errorDetail.message>
the engine sent, prefixed with the request it belongs to. Trailing whitespace
is stripped -- engine messages usually end in a newline, and Carp appends no
location to a message that already ends in one.

=head2 events

ArrayRef of every event decoded from the stream, in order, the C<errorDetail>
event included. This is the progress output the caller would otherwise lose
by never receiving a return value.

=head2 location

Carp's location suffix (C< at FILE line N.\n>), captured at the point the
error was raised so it names the same frame a plain C<croak> would have named.
Kept apart from L</message> so a caller can have the reason without it.

=head2 as_string

    my $text = $err->as_string;   # same as "$err"

The message and the location suffix, concatenated. This is what the
stringification overload returns.

=head1 SEE ALSO

=over

=item * L<API::Docker::Role::HTTP> - Raises this error; see its C<ndjson> option

=item * L<API::Docker::API::Images> - C<build>, C<pull> and C<push>

=back

=head1 SUPPORT

=head2 Issues

Please report bugs and feature requests on GitHub at

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

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 => (
  is       => 'ro',
  required => 1,

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

    # Stop waiting after two seconds of silence instead of hanging forever.
    my $out = '';
    eval {
        $docker->containers->attach($id,
            stream       => 1,
            stdout       => 1,
            read_timeout => 2,
            on_frame     => sub { $out .= $_[0]{data} },
        );
    };
    if (my $err = $@) {
        die $err unless ref $err
            && $err->isa('API::Docker::Error::Timeout');
        # Every complete frame reached the callback before the timeout; the
        # summary says how many.
        warn 'stopped after ' . $err->summary->{delivered} . ' frames';
    }

=head1 DESCRIPTION

L<API::Docker::Role::HTTP> croaks with an object of this class when a request
was given a L<API::Docker::Role::HTTP/read_timeout> and the daemon then went
quiet for longer than it -- and, with L</phase> set to C<'connect'>, when a
request was given a L<API::Docker::Role::HTTP/connect_timeout> and the socket
never came up within it.

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

The reason on its own, without the location suffix: the request it belongs to,
the timeout that expired and how much had arrived before it did.

The request is named without its query string, for the same reason the
C<< >= 400 >> croak names it that way -- C</build> carries its C<buildargs>
there, which can hold credentials and have no business in an exception.

=head2 location

Carp's location suffix (C< at FILE line N.\n>), captured at the point the
error was raised so it names the same frame a plain C<croak> would have named.
Kept apart from L</message> so a caller can have the reason without it.

=head2 endpoint

The request that timed out, as C<"GET /v1.47/containers/json"> -- method and
path, no query string.

=head2 phase

Which of the two bounds fired: C<'read'> for

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

the content may stop mid-value. They are here so a caller who wants them can
have them rather than because the transport thinks they are usable.

Always the empty string for a streamed request, which keeps no body by design.
Nothing is lost there either: every byte that arrived went through the same
decoding as every other byte, so the units it completed reached the callback
and are counted in L</summary> before this is raised.

That holds by construction rather than by rescue, which is worth knowing if
you are reading the transport. The read that expires carries nothing --
C<sysread> returns what it received and leaves C<errno> alone, and fails with
C<EAGAIN> only when it received nothing at all. It used to be otherwise:
PerlIO's C<read()> could come back with part of what it was asked for B<and>
C<EAGAIN> together, so the bytes of the expiring read had to be fed to the
callback before the exception went up or a caller would have been handed
nothing even though the whole response had arrived.

Also empty when L</phase> is C<'connect'>: there was no response to have part
of.

=head2 summary

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

For a request streaming through C<on_event>, C<on_frame> or C<on_chunk>: the
same C<< { delivered => N, stopped => 0 } >> HashRef the call would have
returned, describing what reached the callback before the timeout. C<undef> for
a buffered request.

Every unit it counts was complete and was delivered; a unit still arriving when
the clock ran out was not, and is not counted and not delivered.

=head2 as_string

    my $text = $err->as_string;   # same as "$err"

The message and the location suffix, concatenated. This is what the
stringification overload returns.

=head1 SEE ALSO

=over

=item * L<API::Docker::Role::HTTP> - Raises this error; see its C<read_timeout>
attribute and option

=item * L<API::Docker::Error::HTTP> - Raised instead when the daemon answered,
with a status of 400 or above

=item * L<API::Docker::Error::Stream> - Raised instead for a failure reported
inside a stream the daemon already answered with HTTP 200

=back

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

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 => (
  is       => 'ro',
  required => 1,

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

API::Docker::Error::Truncated - The daemon closed before the response it announced was complete

=head1 VERSION

version 0.004

=head1 SYNOPSIS

    # A tar the daemon stopped sending halfway is not a tar.
    my $tar = eval { $docker->images->get_tar('busybox') };
    if (my $err = $@) {
        die $err unless ref $err
            && $err->isa('API::Docker::Error::Truncated');
        warn 'got ' . length($err->partial) . ' of '
            . $err->expected . ' bytes; retrying';
        $tar = $docker->images->get_tar('busybox');
    }

=head1 DESCRIPTION

L<API::Docker::Role::HTTP> croaks with an object of this class when the daemon
closed the connection in the middle of a response -- a status line with no
terminator, a header block with no blank line to close it, a body shorter than
its C<Content-Length>, a chunk shorter than its own header, a chunk header cut
in half, or a chunked body with no terminating zero chunk. It is raised in one

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

Not a status. Where a status line arrived intact it said 200 and the response
after it did not follow; where L</phase> is C<'status-line'> there was no
usable status to begin with. An engine that reports a failure the normal way
raises L<API::Docker::Error::HTTP>, and one that reports it inside an HTTP 200
stream raises L<API::Docker::Error::Stream>. This is the third thing: no
report at all, because the connection went away mid-sentence.

Nor is it the daemon answering nothing whatsoever. A connection that closed
before a single byte of the status line is still the plain
C<No response from Docker daemon> croak it has always been -- there is no
half-sent response to describe, and that string predates every error class
here.

A response with a status of 400 or above raises this rather than
L<API::Docker::Error::HTTP> when B<its> body is the one cut short, which is
the same rule the timeout follows: the transport cannot tell a caller what the
engine said when it did not finish saying it. Read L</partial> for the part of
the error body that did arrive.

=head2 It is still the string it replaces

Like the other three error classes here, this one overloads stringification
(with C<< fallback => 1 >>, so comparison, concatenation, C<sprintf> and
matching all work through it) and produces what a plain C<croak> would have
died with: the reason, followed by Carp's own C< at FILE line N.> location
suffix, naming the same frame.

Unlike the other three it replaces no string, because there was nothing here
to replace -- a truncated response used to be returned rather than raised.
That makes it the one exception in this distribution that existing code cannot
have been catching, which is why it is a documented behaviour change and not a
bug fix in passing.

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

The reason on its own, without the location suffix: the request it belongs to,
where in the response framing the stream ended, and how much had arrived.

The request is named without its query string, for the same reason the
C<< >= 400 >> croak names it that way -- C</build> carries its C<buildargs>
there, which can hold credentials and have no business in an exception.

=head2 location

Carp's location suffix (C< at FILE line N.\n>), captured at the point the
error was raised so it names the same frame a plain C<croak> would have named.
Kept apart from L</message> so a caller can have the reason without it.

=head2 endpoint

The request that was cut short, as C<"GET /v1.47/images/get"> -- method and
path, no query string. The empty string for a reader driven directly with no
request context, which is how the transport's own tests drive them.

=head2 phase

Which piece of the response framing the stream ended inside. One of:

=over

=item * C<'status-line'> - the stream ended inside the status line, before the
CRLF that terminates it. A status line with nothing after it parses perfectly
well -- C<'HTTP/1.1 200 OK'> yields 200 and C<OK> -- so the missing terminator
is the only thing that says the daemon never finished writing it. Also a line
that arrived in full but is not an HTTP status line at all -- a proxy's
plain-text banner, an HTML error page -- which is no cut response, but is
refused here for the reason the non-hexadecimal chunk size below is: its second
word would otherwise be split out and read as the status

=item * C<'header-block'> - the stream ended inside a header line, or where
one belongs with the blank line that ends the field section never sent. The
second covers a head with no fields at all: RFC 9112 section 2.1 requires the
empty line whether there are twenty fields or none

=item * C<'content-length'> - fewer bytes arrived than the C<Content-Length>
header announced, or the header arrived in full but its value is not a number.

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

same C<< { delivered => N, stopped => 0 } >> HashRef the call would have
returned, describing what reached the callback before the stream was cut off.
C<undef> for a buffered request.

C<stopped> is always 0 here. A stream the caller ended with C<< $stop->() >>
leaves the rest of the response unread on purpose and is never truncation --
the check is skipped entirely once the callback has said stop.

=head2 as_string

    my $text = $err->as_string;   # same as "$err"

The message and the location suffix, concatenated. This is what the
stringification overload returns.

=head1 SEE ALSO

=over

=item * L<API::Docker::Role::HTTP> - Raises this error; see
L<API::Docker::Role::HTTP/"Failure in the middle of a response">

=item * L<API::Docker::Error::Timeout> - Raised instead when the daemon went
quiet for longer than a C<read_timeout>, rather than closing

=item * L<API::Docker::Error::HTTP> - Raised instead when the daemon answered,
completely, with a status of 400 or above

=item * L<API::Docker::Error::Stream> - Raised instead for a failure reported
inside a stream the daemon already answered with HTTP 200

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

Get fresh container information. Returns an
L<API::Docker::Type::ContainerInspectResponse> whatever the invocant was, so
this is also how a C<list> entry is turned into the full shape.

=head2 pause

    $container->pause;

Pause all processes in the container. Returns 1/0 as
L<API::Docker::API::Containers/pause> does; an already-paused container is an
error there, not a 0.

=head2 unpause

    $container->unpause;

Unpause the container. Returns 1/0 as
L<API::Docker::API::Containers/unpause> does.

=head2 top

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

=head2 put_archive

    $container->put_archive($tar, path => '/opt/app');

Unpack a tar archive into a directory in the container.

=head2 stat_archive

    my $stat = $container->stat_archive(path => '/etc/hostname');

Stat a path in the container without transferring it.

=head2 is_running

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

True when the container is running. Reads whichever shape it is on: the
status string C<< $summary->state >> from C<list>, and
C<< $inspected->state->running >> from C<inspect>.

=head1 SEE ALSO

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

reason an empty string croaks here rather than travelling on.

=head2 What it deliberately does not do

It does not check filter B<names>. Doing so would need one accepted-name
table per endpoint, and the daemon already has them: measured against Podman
5.x (API 1.41), an unknown name is refused with HTTP 500 by
C</containers/json> (C<bogusname is an invalid filter>), C</images/json>
(C<invalid image filter "danglin">), C</volumes>, C</networks> and
C</events>, and Docker validates C</plugins> the same way -- which is how the
Engine API reference's documented C<enable> turns out to be a hard error
where the daemon wants C<enabled> (see
L<API::Docker::API::Plugins/list>). A client-side table would duplicate that
check, and the first time it lagged the daemon it would refuse a filter the
daemon accepts. That is a worse failure than the one it prevents.

It also does not check that a value makes sense for its filter. C<'yes'> for
C<dangling> is a well-formed filter that the daemon rejects
(C<strconv.ParseBool: parsing "yes">), and C<< { label => ['nope'] } >> is a
well-formed filter that simply matches nothing. Both are the caller's
question to get right.

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

                                type string
    {"dangling":[1]}        500 json: cannot unmarshal number into Go value
                                of type string
    {"dangling":[null]}     500 non-boolean value for filter:
                                strconv.ParseBool: parsing ""
    {"dangling":[""]}       500 the same -- Go reads a JSON null into a
                                string as ""
    {"dangling":["1"]}      200, and so do "0", "true" and "false"

So a wrong shape is not silent on this engine -- it is a 500 carrying a Go
type error, one round trip later, naming neither the option nor the key the
caller got wrong. What this role changes is where that is said: at the call,
in terms of the argument, and for the recoverable shapes not at all, because
they are repaired instead.

=head1 METHODS

C<_normalise_filters($filters)> is private and composed into the resource
classes. It takes what the caller passed as C<filters> and returns the
HashRef to hand to the transport as a query parameter; it croaks rather than
sending a shape the daemon will refuse.



( run in 2.506 seconds using v1.01-cache-2.11-cpan-8dfa8b56332 )