API-Docker

 view release on metacpan or  search on metacpan

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


Document the surface as it exists. If the code and the documentation disagree, the code
wins and the disagreement is a finding you report — you do not change behavior to match
prose. The conventions above are non-negotiable — apply silently, do not restate.

## What this distribution's POD looks like

POD is interleaved with the code, each `=attr`/`=method` block directly after the
`has`/`sub` it documents, and every class ends with `=seealso`. Option lists are `=over`
blocks with one `=item * C<name> - meaning` per accepted key — mirror the method's own
`%params`/`%opts` handling, including the defaults it applies (`rm` defaults to true in
`build`, `tag` to `latest` in `pull`).

The two places where accuracy matters most, because a reader cannot discover the truth
from the signature:

- **What a method returns.** `list`/`inspect` hand back entity objects; everything else
  hands back the raw daemon response, and the streaming endpoints hand back an arrayref
  of newline-delimited JSON events — except when the stream held exactly one object.
- **What the client deliberately does not do.** The `CONTAINER ENGINES` section in
  `API::Docker` documents that socket discovery reads `DOCKER_HOST` and the default
  socket and consults no Docker contexts, and contrasts that with other clients. That
  section is a promise about behavior; keep it true or flag it.

Say what is true, do not describe intent as capability — and do not trust a claim of
incapability written down here either. This paragraph used to assert that `tls` and
`cert_path` were unimplemented. They are implemented: `API::Docker::Role::HTTP` carries
a full `IO::Socket::SSL` path, including `tls_insecure` and the `docker` CLI's cert
layout. An agent following that sentence would have written a falsehood into
shipped POD. Check the code for the feature you are about to describe, every time.

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

---
name: api-docker-test-writer
description: "Write API::Docker tests with Test::More and the Test::API::Docker::Mock route table. The default suite never touches a Docker daemon or the network; live paths stay gated on is_live()/can_write(). Use for test additions, regression scaf...
model: sonnet
allowed-tools: Read, Edit, Write, Bash, Glob, Grep
briefing:
  skills:
    - api-docker-core
    - docker-engine-api
    - getty-perl-core
    - getty-perl-moo
    - kanban-issues-karr-cli
---

You are the api-docker-test-writer for **API::Docker**.

Division of labor: the dispatching agent owns test **intent** — which behaviors matter
and whether coverage is sufficient. You own the **mechanics** — translating that intent
into correct, intent-faithful setups and assertions. Don't invent coverage decisions; if
the intent is unclear or the briefed behavior seems wrong, stop and ask.

Hard rules:

- **The default run reaches no daemon and no network.** `prove -lr t/` with no
  environment set must pass on a machine with no Docker installed. Anything live sits
  behind `is_live()`, mutations behind `can_write()` / `skip_unless_write()`, with
  `register_cleanup` for every resource created.
- **Never point a live test at a daemon the suite does not own.** Write tests create and
  destroy real containers, images, networks and volumes.

## Mechanics that decide whether a test is real

- **Pick the right level.** `test_docker` replaces `_request` wholesale, so anything
  below it — request line assembly, header sanitising, chunked reading, status handling,

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


## What makes this lane different

You write from a **specification**, not from a running daemon. That is a discipline, not
a shortcut:

- The spec is the source for a field's name, type and description. Take the description
  and make it read well; do not invent one the spec does not have.
- Where a measurement contradicts the spec — and this distribution has several, recorded
  in POD and `Changes` — the POD says both, and names the engine and version the
  measurement came from. The spec does not win by default and neither does the
  measurement.
- You are allowed to probe a daemon to settle a question, but a class is never justified
  by "the daemon answered this once". 132 definitions cannot be verified that way, which
  is exactly why the drift checker exists.

## The two failures that matter

**Translating a key that is the caller's data.** `Labels`, `ExposedPorts`,
`PortBindings`, `Volumes`, `Sysctls` and their kin are keyed by what the user wrote. A
label named `com.example.Some-Label` must arrive at the daemon spelled exactly that way.

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

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
    test green, say what the test asserts and whether your fix keeps that claim or
    replaces it. If the claim is wrong, fix the claim and say so.

## Delegation

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

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

  | Task | Agent |
  |---|---|
  | Anything turning on what the daemon does or expects — endpoints, wire formats, filters, registry auth, version gating | `api-docker-engine-worker` |
  | The Perl side — Moo, transport internals, entity classes, refactoring, cpanfile | `api-docker-worker` (default) |
  | Write/extend tests, add fixtures | `api-docker-test-writer` |
  | The generated type model, the `API::Docker::Type` DSL, the drift checker, `spec/` | `api-docker-type-writer` |
  | Pre-release audit | `api-docker-release-checker` |
  | POD and README | `api-docker-doc-writer` |

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

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

  This has already cost a locally built image, during what its caller
  believed was a read-only probe. **No `prune` of any
  kind -- images, containers, networks, volumes, build cache -- and no
  `rm -a` or `system reset`, on either engine, ever, unless the user names
  the command.** Probing what an endpoint answers is not a reason: measure
  it against something you created yourself.
- **`images->push` publishes.** With credentials it writes to a real registry under the
  maintainer's account. Never run it — nor any test that does — without explicit
  instruction.
- **Streaming endpoints block until the daemon closes, unless given a callback.**
  `_request` still buffers a whole response by default, so `system->events` or
  `containers->stats` without a bound and without `on_event`/`on_frame`/`on_chunk` never
  returns. Bound the window, pass a callback, or wrap a manual probe in `timeout` — a
  callback still needs `$stop->()` called from somewhere, or it runs until the daemon
  closes the connection on its own.
- **`../p5-dist-zilla-plugin-docker-api` consumes this API.** A public signature or
  return-shape change is a cross-repo change: verify that repo, or file a ticket on its
  board before landing.
- **`[@Author::GETTY]` gathers through `Git::GatherDir`, which sees only tracked
  files.** A new `.pm`, test file or fixture is invisible to `dzil build`/`dzil test`
  until it is `git add`-ed — while `prove -lr t/` stays green the whole time, because it

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

decodes it with Go's `base64.URLEncoding`, which **requires the padding**.
Stripping the `=` made every push fail with
`failed to parse "X-Registry-Auth" header ... unexpected EOF` — including the
anonymous case, whose payload `{}` encodes to `e30=`, three characters and one
pad. `_registry_auth_header` produces padded base64url (`tr{+/}{-_}`, no `=`
removal); a bare base64-looking string passed as `auth` is forwarded
untouched.

## Transport behavior that's easy to get wrong

- **Buffers the whole response by default; streaming needs a callback.**
  Every request sends `Connection: close`. With none of `on_event`,
  `on_frame`, `on_chunk` given, `_request` reads the whole response before
  parsing, so `/build`, `/images/create`, `/push`, `/events`,
  `/containers/*/stats` and `logs(follow)` block until the daemon closes the
  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".
- **No connection reuse.** Each `_request` calls `_reconnect` and closes
  afterwards, streamed or not.

## Tests — `Test::API::Docker::Mock`

`test_docker('GET /images/json' => $fixture_or_coderef, ...)` returns a client

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

# Docker Engine HTTP API

For code that speaks the API over a socket rather than shelling out to `docker`.
The CLI hides everything below; a client has to handle it. Endpoint lists live
in the daemon's own reference — what follows is what the reference states once
and clients get wrong repeatedly.

## Versioning

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.

.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

Both are active in **every** file, without exception. How they get there differs:

- **Scripts, tests, plain modules:** `use strict; use warnings;` explicitly.
- **Classes:** `Moo`, `Moose`, `Catalyst` and anything derived from them enable both on import — do not repeat them there.

The rule is "always on", never "leave them out". Omitting them from a class is correct only because the object system already did it.

## Object system

- **One object system per distribution.** Pick Moo or Moose and use it everywhere; mixing is for boundaries a framework forces (e.g. RapidApp), not a choice.
- **`is => 'ro'` is the default.** `rw` is the exception and needs a reason.
- **`lazy_build => 1` + `sub _build_foo`** over `default => sub { ... }` for anything non-trivial.
- **`weak_ref => 1`** on attributes holding a reference back to a parent/owner — standard for nested object graphs, prevents circular refs.
- **`namespace::autoclean`** on every class file. Classes extending DBIx::Class (`MooseX::NonMoose`) use **`MooseX::MarkAsMethods autoclean => 1`** instead.
- **`no Moose;` + `__PACKAGE__->meta->make_immutable;`** at the bottom of every Moose class.
- **Types:** the tendency is to type what arrives from outside — Moose's own constraints where Moose is already there, `Types::Standard` where it is not. Not every distribution needs a type system, and none needs one for every field: `getty-perl-ty...

## Singletons

- **`->instance`** for `MooseX::Singleton` / `MooX::Singleton` classes. Never `->new` on a singleton.
- **`->new`** for everything else.

## Subroutines

- **`my ( $self, $x ) = @_;`** as the first line — explicit destructure, spaces inside the parens. Never `my $self = shift;` as argument unpacking.
- **One-liners skip unpacking** and use `shift->` or `$_[0]->`: `sub trace { shift->_logger->trace(@_) }`. This is the one place `shift` is right.
- **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.
- **Single quotes by default.** `'...'` and `"..."` are genuinely different in Perl — `"` interpolates and processes escapes, `'` does not. Reach for `"` when you need that, not by habit.
- **Import lists as `qw( croak confess )`** — spaces inside the parens. Never rely on default exports.

## Control flow

- **Postfix `if`/`unless`** for guards and short conditions: `croak(...) if $self->readonly;`
- **`unless $x`** instead of `if !$x`.
- **Guard clauses return bare:** `return unless $res->is_success;` — not `return undef;`.
- **Nested ternaries** for a return that picks between expressions, instead of an if/elsif chain.

## Data

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

- **`JSON::MaybeXS`** always — never `JSON::PP`, `JSON::XS`, `Cpanel::JSON::XS` directly. Encoders get `canonical => 1, convert_blessed => 1`.
- **Every serialiser is deterministic.** MessagePack `->canonical`, DBIC `serializer_options => { canonical => 1 }`. Same rule, every format.
- **Booleans: `JSON->true` / `JSON->false`.** `use JSON::MaybeXS;` covers codec and booleans.
- `$YAML::XS::Boolean = 'JSON::PP'` is one of YAML::XS's fixed mode names, not a module choice — leave it alone.
- **Align `=>` in multi-line hash literals** when keys are of similar length.
- **Optional pairs inline:** `$cond ? ( experimental => 1 ) : (),`

## Configuration

Config comes from environment variables prefixed with the project name
(`$ENV{MYPROJECT_TIME_ZONE}`), each with a default in code. Where many
attributes share that shape, write a generator that wraps `has` rather than
repeating the declaration.

## DBIC-ish result classes

- Column defs via **`DBIx::Class::Candy`** or **`DBIO::Candy`** — `primary_column` / `column` macros, not `__PACKAGE__->add_column(...)`.
- **`keep_storage_value => 1`** on enum and integer columns that shouldn't be inflated/deflated.
- **`\'NOW()'`** (literal scalar ref) for DB-side timestamp defaults.

## Style, comments, structure

- **2-space indentation.** Not 4. Not tabs. Every Getty Perl file.
- **No trailing commas** at the end of multi-line lists (unlike Python).
- **Section long files with a figlet banner** as a comment block. Pick from `standard`, `slant`, `small`, `banner`. Where figlet is unavailable or the file is short, a `#### <Name>` rule does the job.
- **Commented-out debug lines stay** (`#use DDP; p($res);`). They mark where debugging was needed before — deleting them as dead code removes a warning sign, and sometimes the precaution it guards.

## cpanfile

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

Every distribution ships a `Changes` file with a `{{$NEXT}}` token at the top (Dist::Zilla's `[NextRelease]` fills it at release time).

- **Add a bullet under `{{$NEXT}}` in the SAME commit as any user-facing change** — new bindings, behaviour changes, bug fixes, deprecations. If a CPAN consumer would notice, it belongs there.
- **Match the existing style:** two-space indent, `  - ` bullets, wrap near 78 columns, present-tense imperative ("New binding X", "Fix Y on macOS").
- **One topic, one bullet, one to three lines** — touching an area again rewrites the bullet that is already there instead of adding a second. Wording and length: `getty-git-commit-style`.
- **Skip pure dev-tooling noise** — skill hardlinks, editor config, internal CI refactors. A CI fix that unbreaks the build for everyone IS worth a line.
- **Never hand-edit the version line or timestamp** — `[NextRelease]` owns those.

## Forbidden

❌ `require Foo` inside a method to "speed up startup" · ❌ a Getty repo's `$VERSION` as a cpanfile requirement · ❌ `'0'` or `'>= x'` as a version argument · ❌ `default => sub {...}` for a non-trivial attribute default · ❌ 4-space indent ...

## When in doubt

Grep hand-written Getty code for how the pattern is used there — the reference is an older repo with no AI commits in its history. Newer repos may show an agent's guess rather than the house rule.

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

# Perl/Moo – Architecture & Implementation Patterns

## Core Principle
Use **inheritance sparingly** (stable "is-a" contracts), **roles heavily** (horizontal reuse). When in doubt: role, not subclass.

---

## House conventions

- **`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;
use Moo::Role;
requires 'name';                          # contract: consumer must have name()
sub uppercase_name { uc $_[0]->name }

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

sub import {
  my $target = caller;
  strict->import::into($target);
  warnings->import::into($target);
  Moo->import::into($target);
  namespace::clean->import::into($target);   # after Moo, cleans stray imports
}

package App::Thing;
use My::Mooish;
has x => (is => 'ro', default => sub { 1 });
```

**Rules:** Order matters: imports → `use Moo` → `namespace::clean`. Use `namespace::autoclean` ≥ 0.16 only (older versions inflate Moo classes to Moose). Use `strictures` v2 with Moo 2.

---

## Pattern 5 – Delegation via `handles`

```perl
package App::UsesCounter;

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


```perl
package Kitchen;
use Moo;
use Sub::HandlesVia;
use Types::Standard qw(ArrayRef Str);
has food => (
  is          => 'ro',
  isa         => ArrayRef[Str],
  handles_via => 'Array',
  default     => sub { [] },
  handles     => { add => 'push', find => 'grep' },
);
```

**Use `Sub::HandlesVia` over `MooX::HandlesVia`** – the latter documents that triggers/coercions don't fire on delegated mutations. Load `Sub::HandlesVia` *after* `use Moo`.

---

## Pattern 7 – Method Modifiers

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

```

**Rules:** `before`/`after` cannot alter return value; `around` can. Always forward `@_` correctly in `around`. Multiple modifiers from multiple roles stack – order is composition-order-sensitive.

---

## Pattern 8 – Attribute Options Cheatsheet

```perl
has name   => (is => 'ro',   required => 1);
has tags   => (is => 'ro',   default  => sub { [] });    # ALWAYS coderef for refs
has id     => (is => 'lazy');                             # built on first access
sub _build_id { "id:" . $_[0]->name }

has status => (
  is      => 'rw',
  trigger => 1,                                        # calls _trigger_status on set
);
sub _trigger_status { die "bad" unless $_[1] =~ /\A(new|ok)\z/ }

has _secret => (is => 'ro', init_arg => 'secret');       # constructor param alias
```

`trigger` fires on `new()` and `set`, NOT on `default`/`builder`. Old value is NOT passed (unlike Moose). `is => 'lazy'` = lazy reader, runs builder on first access.

---

## Pattern 9 – Lifecycle Hooks

```perl
around BUILDARGS => sub {
  my ($orig, $class, @args) = @_;
  return { source => $args[0] } if @args == 1 && !ref $args[0];  # normalize
  $class->$orig(@args);

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

## Pattern 12 – Parameterized Roles

```perl
package Counter;
use Moo::Role;
use MooX::Role::Parameterized;
parameter name => (is => 'ro', required => 1);
role {
  my ($p, $mop) = @_;
  my $n = $p->name;
  $mop->has($n => (is => 'rw', default => sub { 0 }));
  $mop->method("inc_$n" => sub { $_[0]->$n($_[0]->$n + 1) });
};

package Thing;
use Moo;
use MooX::Role::Parameterized::With;
with Counter => { name => 'hits' };

Thing->new->inc_hits;   # generates: hits attribute + inc_hits method
```

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

---

## Type Constraints

Moo has no built-in type system — `isa` takes a coderef, and `Type::Tiny` objects
are coderefs, so `Types::Standard` plugs straight in:

```perl
use Types::Standard qw( Str ArrayRef );
has name => ( is => 'ro', isa => Str );
has tags => ( is => 'ro', isa => ArrayRef[Str], default => sub { [] } );
```

Where to type and where not, own type libraries, parameter signatures:
**`getty-perl-typing`**.

---

## Decision Guide

| Situation | Use |

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

| Cross-project boilerplate | `Import::Into` house-style module |
| Named types | `Type::Tiny` / `Types::Standard` |
| Multiple roles define same method | Sequential `with` or refactor |
| Legacy non-Moo parent | `FOREIGNBUILDARGS` |
| Multiple inheritance | Last resort; use `mro 'c3'` |

---

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

directory fails with the identical message — which reads as though `genlicense`
did nothing.

Re-run `genlicense` after changing `license`, `copyright_holder` or
`copyright_year`. Checking the file against them is the whole point of the
plugin: a committed LICENSE otherwise keeps serving the old licence silently,
with no warning and no build failure.

The target is the repository, not the tarball. GitHub, Gitea and Forgejo detect
a licence only from a committed `LICENSE`, and a generated one never exists
outside the build — which is why a distribution built the default way shows up
as unlicensed on its own project page. `genlicense` writes the bare licence
text, not the `fulltext` that `[License]` generates: the copyright notice
`fulltext` prefixes is enough to make GitHub report `NOASSERTION` instead of
naming the licence.

`[LicenseFile]` is a recent addition to the bundle, so distributions that built
fine before it landed fail on their next build. When picking up an older dist,
`ls LICENSE` is cheaper than diagnosing it mid-release. For a dist that
deliberately ships no committed LICENSE, `generate_license = 1` restores the
generated file and adds no check.

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

## `# ABSTRACT` lines

Every file the bundle processes carries `# ABSTRACT: <one line>` directly under
`package`, before any `use`; executables in `bin/` carry it under the shebang.
PodWeaver turns it into the NAME section. Outside a bundle-managed distribution
the line does nothing — do not scatter it into files Dist::Zilla never sees.

## @Author::GETTY Options

### Feature Toggles (Boolean)
- `no_cpan` - Skip UploadToCPAN; also defaults `version_finder` to `:MainModule`
- `no_podweaver` - Skip PodWeaver
- `no_changes` - Skip NextRelease
- `no_installrelease` - Skip InstallRelease
- `no_makemaker` - Skip MakeMaker
- `xs` - Use ModuleBuildTiny (for pure-Perl XS without Alien deps)
- `deprecated` - Add Deprecated plugin
- `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

### Docker
- `docker_image` - Image repository. Auto-adds one Docker::API plugin, which is a working Releaser on its own (no UploadToCPAN needed for non-CPAN dists)
- `docker_tags` - Whitespace-separated tag list (default: `latest %V %v`)
- `docker_local` - Build and tag the image, but don't push
- `docker_default` - Set to 0 to suppress the auto-added plugin when you configure builds exclusively through `[@Author::GETTY::Docker / name]` subsections

### Support
- `irc = #channel` - IRC channel
- `irc_server` - Server (default: irc.perl.org)
- `irc_user` - Username for SUPPORT section

### Git
- `release_branch` - Branch for releases (default: main)
- `tag_format` - Release tag format. Default `%v`, the bare `$VERSION` (`0.317`) — *not* a v-prefixed SemVer tag. Use `v%v.0` when the tag must satisfy strict vMAJOR.MINOR.PATCH (Perl's decimal `$VERSION` has only two parts, the `.0` supplies the p...
- `commit_files_after_release` - Multi-value; extra files folded into the release commit (via Git::Commit's `allow_dirty`). For artefacts a `run_before_release` hook rewrites, e.g. a sibling Python/JS version file

### Alien (prefix `alien_`)

- `alien_build = 1` - Alien::Build-based dist: adds AlienBuild (Makefile.PL driven by Alien::Build::MM), implies `no_makemaker`, expects an `alienfile` in the dist root

For wrapping C libraries with Alien::Base:

**Required:**
- `alien_repo` - URL to download releases from

**Library identification:**
- `alien_name` - Name of the alien package
- `alien_bins` - Executables to install (multi-value)

**Archive pattern matching:**
- `alien_pattern` - Full regex pattern for archive matching
- `alien_pattern_prefix` - Prefix (e.g., `mylib-`)
- `alien_pattern_version` - Version regex (default: `([\d\.]+)`)
- `alien_pattern_suffix` - Suffix (e.g., `\.tar\.gz`)

**Build configuration:**
- `alien_msys` - Use MSYS on Windows
- `alien_autoconf_with_pic` - Pass --with-pic to autoconf
- `alien_isolate_dynamic` - Isolate dynamic libraries
- `alien_version_check` - Command to check installed version

**Custom build commands (for non-autoconf projects):**
- `alien_build_command` - Custom build commands (multi-value, use `%s` for prefix)

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

and BumpVersionAfterRelease both stop after the first match, so a file holding two
packages leaves the second one frozen at whatever version it was written with —
while MetaProvides::Update happily reports the real release version. The result is
a distribution whose META and whose code disagree, silently, for as many releases
as it takes someone to notice.

So: **one package per file.** If you find several `package` statements in one file,
split them out before releasing.

**Executables belong in `bin/`, never `script/`.** The bundle sets no `ExecDir`, so
Dist::Zilla's default of `bin` applies: files under `script/` are not installed as
executables and their `$VERSION` is never rewritten. A distribution with a `script/`
directory should have it renamed to `bin/` — otherwise none of the above takes
effect.

## Release Workflow

```bash
dzil release        # Builds, tests, uploads to CPAN, bumps version, commits, tags
```

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

karr list --archived                         # the archive, and nothing else
karr list -s "search term"                   # search title/body/tags
karr list --sort priority --reverse          # sort and reverse
karr list --sort priority -n 5 --json        # the five most urgent open cards
karr list --claimed-by agent-1               # filter by claim owner
karr list --unclaimed                        # only what no live claim holds
karr list --compact                          # one-line output (agent-friendly)
karr list --json                             # JSON output
```

Finished work is out of `list` by default: the board's final column (`done` on
a default board) and `archived` are shown only when asked for by name
(`--status done`, `--archived`). `--sort` takes `id`, `title`, `status`,
`priority`, `created`, `updated` or `due`, and `priority` sorts most urgent
first. `-n`/`--limit` cuts after filtering **and** after sorting, so
`--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

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

```bash
karr board                                   # every column but the last one
karr board --done                            # include the final column too
karr board --tags                            # tags on an extra line per card
karr board --compact                         # status(count): ids, one per column
karr board --json                            # JSON output
```

Groups the board's cards into one `## Status` section per column, in board
order and empty sections included, with a footer totalling tasks, claims and
blocks. The board's final column (`done` on a default board) is hidden unless
`--done` is given, and the footer says how many it withheld -- `(2 done
hidden)`. Archived cards are in none of it, in any output mode: `board` reports
the columns the board works in, and `karr list --archived` is where filed-away
cards are read.

### Multi-board dashboard

```bash
karr dashboard                                # scan the current directory
karr dashboard ~/projects --depth 2           # scan elsewhere, shallower

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

Atomically finds and claims the next available task. Respects claim timeouts, blocked state, and class-of-service priority ordering (expedite > fixed-date > standard > intangible); where two `fixed-date` cards meet, the due date is asked before prior...

### Unlock a stuck task

```bash
karr unlock                                  # list the pick locks currently held
karr unlock ID                               # break one
karr unlock --all                            # break all of them
```

`karr pick` takes a lock ref and gives it back inside the same command, so normally there is nothing here to see. An agent that dies mid-pick leaves one behind. Locks expire on their own after `lock_timeout` (default `5m`, board config); this is how ...

### Handoff task for review

```bash
karr handoff ID --claim agent-1              # move to review, refresh claim
karr handoff ID --claim agent-1 --note "Done, needs QA" --timestamp
karr handoff ID --claim agent-1 --block "waiting for feedback" --release
```

Moves the task to the board's review column, refreshes the claim, and optionally appends a timestamped note, blocks, or releases the claim. On a board that configures a `review` status that is the target; a board without one hands off to its last non...

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

card over and says what it waits on. The `blocked` flag is what keeps the card
out of `pick` and out of karr-foundation's selection -- the link is the fact,
`blocked` is the decision.

### Config

```bash
karr config                                  # show all config values
karr config get KEY                          # get a single value
karr config set KEY VALUE                    # set a writable value
karr config show --defaults                  # karr's defaults, no board read
karr config --json                           # JSON output
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

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

```bash
karr context                                 # print markdown summary
karr context --write-to AGENTS.md            # create/update file with sentinels
karr context --sections blocked,overdue      # filter sections
karr context --days 14                       # lookback for recently-completed
karr context --activity-limit 10             # other agents' log entries in Recent Activity
karr context --json                          # JSON output
karr context --compact                       # board_name and the four counts, key=value
```

Generates a markdown summary with sections: In Progress, Blocked, Overdue, Recently Completed, Recent Activity (other agents' log entries, newest first, bounded by `--activity-limit`, default 5). `--sections` takes the slugs `in-progress,blocked,over...

### Skill management

```bash
karr skill install                           # install skill for detected agents
karr skill install --agent claude-code       # install for specific agent
karr skill install --global                  # install globally (~/)
karr skill install --force                   # force reinstall
karr skill check                             # check if installed skills are current
karr skill update                            # update outdated skills

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

  - todo
  - name: in-progress
    require_claim: true
  - name: review
    require_claim: true
  - done
  - archived
priorities: [low, medium, high, critical]
classes: [expedite, fixed-date, standard, intangible]
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`.

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

# 2. Work on task...

# 3. Hand off for review
karr handoff ID --claim $NAME --note "Implementation complete" --timestamp

# 4. Or: release and mark done directly
karr edit ID --release
karr move ID done
```

Claims expire after the configured timeout (default: 1h). Statuses with `require_claim: true` enforce that moves include `--claim`.

Perl remains the primary local installation path, but a Docker alias around
`raudssus/karr:latest` or `raudssus/karr:user` works with the same commands when
another repository vendors `karr` instead of installing it locally.

## Helper-ref workflow

```bash
# 1. Publish a shared planning blob
karr set-refs superpowers/spec/1234.md initial draft ready for review

CLAUDE.md  view on Meta::CPAN

    not trust a number written down here.

12. **`{{$NEXT}}` in `Changes` is the placeholder for the upcoming
    release.** Add entries under it as you change behavior; `dzil
    release` replaces it with the version + timestamp.

## What this distribution is

A pure-Perl client for the Docker Engine API. No LWP, no shell-outs —
HTTP/1.1 (incl. chunked) is spoken directly over the daemon's Unix
socket (default) or a TCP endpoint. Any engine serving that API works;
Podman needs nothing but `DOCKER_HOST`.

The synchronous `_request` core lives in
`API::Docker::Role::HTTP`; resource-specific API methods live in
`API::Docker::API::*`. Entities hang off the resource APIs: every resource
returns generated `API::Docker::Type::*` classes with an
`API::Docker::Role::Entity::*` composed onto them (karr k79 step 6/7,
finished in k84). There are no hand-written entity wrapper classes left.

Architecture, transport invariants, the streaming and `X-Registry-Auth`

CLAUDE.md  view on Meta::CPAN

## Build and test

```bash
prove -lr t/            # canonical — recursive; plain `prove -l t/` skips subdirs
prove -lv t/images.t    # single test
dzil build              # build the dist
dzil test               # full suite incl. generated xt/
cpanm --installdeps .   # install deps from cpanfile
```

By default tests are fixture-driven — no daemon, no network, and it stays
that way. For the read-only live paths set `API_DOCKER_TEST_HOST`; add
`API_DOCKER_TEST_WRITE=1` for the mutating ones (they create and remove
real containers, images and volumes).

Which engine is available is a fact about the machine, not about this
file — establish it before every live run rather than assuming it:

```bash
# which sockets exist
ls -l /var/run/docker.sock "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman/podman.sock" 2>/dev/null

CLAUDE.md  view on Meta::CPAN


## Delegation

Don't touch behavior-relevant code yourself — hand it to the right agent.
The principle, the lanes and the repo's hazards are in
`.claude/rules/api-docker-rules.md`.

| Task | Agent |
|---|---|
| What the daemon does or expects — endpoints, wire formats, filters, registry auth | `api-docker-engine-worker` |
| The Perl side — Moo, transport internals, entities, refactoring, cpanfile | `api-docker-worker` (default) |
| Write/extend tests, add fixtures | `api-docker-test-writer` |
| The generated type model — `API::Docker::Type::*`, the DSL, the drift checker, `spec/` | `api-docker-type-writer` |
| Pre-release audit | `api-docker-release-checker` |
| POD and README | `api-docker-doc-writer` |

The two workers split by question, not by file. Only `api-docker-engine-worker`
is briefed with `docker-engine-api`, the shared Engine API reference.

`api-docker-type-writer` is briefed with `api-docker-type-model`, which carries the
pattern for the generated classes; see karr k79.

Changes  view on Meta::CPAN

    old names (`API::Docker::Container`, `::Image`, `::Network`, `::Volume`,
    `::Plugin`, `::Secret`, `::Config`) ship as stubs that croak, naming what
    replaces them.
  - New streaming options `on_event`, `on_frame` and `on_chunk` on the HTTP
    verbs, wired into `system->events`, `containers->logs`/`stats`/`attach`,
    `exec->start`, the `images` build/pull/push/load/get family and the
    `plugins` install/upgrade/push. A callback receives each event, frame or
    chunk as it arrives; `$stop->()` ends the stream early. Without one the
    unbounded endpoints still block.
  - New `read_timeout` and `connect_timeout`, as client attributes and
    per-request options, off by default. `read_timeout` is idle time since
    the last byte; both croak `API::Docker::Error::Timeout`, which carries
    whatever already arrived. A new `TIMEOUTS` section in `API::Docker`
    documents what each bounds. `containers->stats` is deliberately not
    bounded by `read_timeout` -- its stream keeps producing rather than
    going idle.
  - New `API::Docker::Role::Using`: `$docker->containers->using(read_timeout
    => 5)->list` clones a resource class to bound a run of calls. Every
    request the run makes, version negotiation included, carries the bound;
    an explicit `0` turns a client-wide default off.
  - A truncated response is now an exception. A body shorter than its
    Content-Length, a short or malformed chunk, a missing zero chunk, a
    malformed status line, a bad `Content-Length` or a stray 1xx all croak
    `API::Docker::Error::Truncated` instead of being handed back as a whole
    response.
  - A status of 400 or above croaks `API::Docker::Error::HTTP` instead of a
    plain string, carrying `status`, `reason`, `body` and decoded `data`. It
    stringifies exactly as the old string croak did, so text-matching
    callers are unaffected.
  - `tls => 1` now speaks real TLS over `tcp://`. `cert_path` names the
    `docker` CLI's `ca.pem`/`cert.pem`/`key.pem` layout, `tls_insecure`
    turns verification off, and `tls` defaults from
    `$ENV{DOCKER_TLS_VERIFY}`. `IO::Socket::SSL` is a recommended
    dependency, loaded on the first TLS connection.
  - New `response => \%h` option fills `status`, `reason` and `headers`,
    including for a request that croaked. New `head` verb beside
    `get`/`post`/`put`/`delete_request`, which never waits for a body.
  - `negotiate_version` croaks, naming `GET /version` and the expected
    shape, when the body is not a JSON object carrying an `ApiVersion` of the
    form `N.N`.
  - New `API::Docker::Role::Filters`, consumed by all eight resource classes
    and applied at every `filters` call site: a bare value, a boolean or a

Changes  view on Meta::CPAN

  - An ArrayRef query parameter expands into one repeated `k=v` pair per
    element (`names=a&names=b`), which some endpoints require.
  - A bare JSON scalar body (`null`, `true`, a number, a quoted string) is
    decoded rather than handed back as raw bytes; `raw` and `ndjson` return
    `''` and `[]` for a zero-byte body instead of `undef`.
  - Registry credentials reach `images->pull` (`auth`, sent as
    `X-Registry-Auth`) and `images->build` (`registry_config`, sent as
    `X-Registry-Config`), sent only when given. An already-base64 auth value
    in the standard alphabet is respelled URL-safe, which the engine
    requires.
  - `images->pull` no longer appends a default `tag` onto a reference that
    already carries a `:tag` or `@digest`.
  - New `images->get`, `->get_all` and `->load`: the image tar roundtrip in
    and out of a daemon without a registry. New `images->commit`
    (POST /commit) and `images->build_prune` (POST /build/prune, the
    BuildKit cache, a different store from the dangling images
    `images->prune` deletes).
  - New container endpoints: `get_archive`, `put_archive`, `stat_archive`
    (the `docker cp` primitives), `changes`, `export`, `resize` and the
    one-way half of `attach`. `attach` defaults to `stream => 0, logs => 1`
    (replay and return) and refuses a container that is not running unless
    `require_running => 0`.
  - `containers->start`/`stop`/`restart`/`pause`/`unpause` return 1 when the
    call changed the container's state and 0 when it was already in it (the
    engine answers a no-op with 304), instead of always undef.
  - `containers->stats` croaks `API::Docker::Error::HTTP` when Podman reports
    a failure inside a 200 response, instead of handing the error object back
    as a reading.
  - New `API::Docker::API::Plugins` (`$docker->plugins`): `list`,
    `privileges`, `install`, `inspect`, `remove`, `enable`, `disable`,

Changes  view on Meta::CPAN

    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
    and the way round it, which is to terminate TLS in front of the
    daemon (stunnel, socat, `ssh -N -L`) and point `host` at the local
    end. Both attributes are kept. `cert_path` on its own does not
    croak: it defaults from `DOCKER_CERT_PATH`, which is exported on
    plenty of machines that also run the docker CLI, so croaking on it
    would break constructions over a value the caller never passed, and
    on its own it transmits nothing and makes an unencrypted connection
    look no different. The POD called TLS "experimental", as though it
    partly worked; it never worked at all.
  - A header name passed through the transport's `headers` option is now
    validated against the RFC 9110 token grammar and rejected if it does
    not match. Only values were sanitised before, so a caller-supplied
    key carrying CR/LF could open a header line of its own. Not
    reachable from this distribution -- the one caller, `push`, passes
    the literal `X-Registry-Auth` -- but the option is public. Names are
    rejected rather than stripped, unlike values: a value can pick up a
    stray newline honestly (`encode_base64` wraps its output by
    default), and flattening it keeps what the caller meant, while a
    name is a literal the programmer wrote and rewriting
    "X-Foo\r\nX-Bar" into "X-FooX-Bar" would put a header on the wire
    under a name nobody asked for. The check also catches spaces and
    colons, which corrupt the request without injecting anything.
  - `containers->logs` and `exec->start` now demultiplex the Docker
    stream format and return an ArrayRef of frames, each a HashRef with
    `stream` and `data`:
      [ { stream => 'stdout', data => "OUT\n" },
        { stream => 'stderr', data => "ERR\n" } ]
    Both used to hand the caller the framed bytes, so the 8-byte frame

META.json  view on Meta::CPAN

         },
         {
            "class" : "Dist::Zilla::Plugin::ShareDir",
            "name" : "@Author::GETTY/@Filter/ShareDir",
            "version" : "6.037"
         },
         {
            "class" : "Dist::Zilla::Plugin::MakeMaker",
            "config" : {
               "Dist::Zilla::Role::TestRunner" : {
                  "default_jobs" : 1
               }
            },
            "name" : "@Author::GETTY/@Filter/MakeMaker",
            "version" : "6.037"
         },
         {
            "class" : "Dist::Zilla::Plugin::Manifest",
            "name" : "@Author::GETTY/@Filter/Manifest",
            "version" : "6.037"
         },

META.yml  view on Meta::CPAN

      name: '@Author::GETTY/@Filter/ExecDir'
      version: '6.037'
    -
      class: Dist::Zilla::Plugin::ShareDir
      name: '@Author::GETTY/@Filter/ShareDir'
      version: '6.037'
    -
      class: Dist::Zilla::Plugin::MakeMaker
      config:
        Dist::Zilla::Role::TestRunner:
          default_jobs: 1
      name: '@Author::GETTY/@Filter/MakeMaker'
      version: '6.037'
    -
      class: Dist::Zilla::Plugin::Manifest
      name: '@Author::GETTY/@Filter/Manifest'
      version: '6.037'
    -
      class: Dist::Zilla::Plugin::TestRelease
      name: '@Author::GETTY/@Filter/TestRelease'
      version: '6.037'

cpanfile  view on Meta::CPAN

requires 'Package::Stash';
requires 'Path::Tiny';
requires 'Scalar::Util';
requires 'Socket';
requires 'Types::Standard';

# Only the tcp:// transport with tls => 1 loads this, and it is loaded at the
# moment that connection is opened. It brings in Net::SSLeay, which is XS
# compiled against libssl; requiring it would make this client unbuildable
# where there are no OpenSSL headers, for the sake of a transport that the
# unix:// default -- local Docker, rootless Podman -- never uses.
recommends 'IO::Socket::SSL';

on test => sub {
    requires 'Test::More';
    requires 'Path::Tiny';
    requires 'Exporter';
};

# The drift checker under maint/ reads Docker's swagger from spec/ and the
# exceptions file beside itself. YAML::XS rather than YAML::PP is not a

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

use API::Docker::API::Exec;
use API::Docker::API::Distribution;
use API::Docker::API::Secrets;
use API::Docker::API::Configs;
use API::Docker::API::Plugins;
use namespace::clean;


has host => (
  is      => 'ro',
  default => sub { $ENV{DOCKER_HOST} // 'unix:///var/run/docker.sock' },
);


has api_version => (
  is      => 'rwp',
  default => undef,
);


has tls => (
  is => 'lazy',
);

sub _build_tls {
  my ($self) = @_;

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

  # Every non-empty value turns TLS on, DOCKER_TLS_VERIFY=0 included. Perl
  # truthiness would read that '0' as off and disagree with the CLI on exactly
  # the value a user is most likely to type for "off", so the test is
  # defined-and-not-empty rather than a boolean one.
  return 0 unless defined $ENV{DOCKER_TLS_VERIFY}
    && $ENV{DOCKER_TLS_VERIFY} ne '';

  # And the CLI ignores TLS on a socket host without saying so
  # (cli/context/docker/load.go, "there's no need to configure TLS for a
  # socket connection"). Ignoring it here is not politeness: BUILD croaks on
  # tls => 1 with a non-tcp:// host, so a host-blind default would make a bare
  # API::Docker->new die on every unix:// machine that exports the variable.
  return $self->host =~ m{^tcp://} ? 1 : 0;
}


has cert_path => (
  is      => 'ro',
  default => sub { $ENV{DOCKER_CERT_PATH} },
);


has tls_insecure => (
  is      => 'ro',
  default => 0,
);


sub BUILD {
  my ($self) = @_;

  # Both checks are here rather than at connect time so that a request for
  # encryption that cannot be honoured is refused before the caller has a
  # client to hand credentials to.
  croak __PACKAGE__ . '->new tls_insecure => 1 without tls => 1 does '

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

  croak __PACKAGE__ . '->new tls => 1 is only meaningful for a tcp:// host, '
    . 'and this one is ' . $host . '. A Unix socket is a file rather than a '
    . 'wire and carries nothing to encrypt, so honouring the option is not '
    . 'possible and ignoring it would answer a request for an encrypted '
    . 'transport with an unencrypted one'
    unless $host =~ m{^tcp://};
}

has _version_negotiated => (
  is      => 'rw',
  default => 0,
);

with 'API::Docker::Role::HTTP';

has system => (
  is      => 'lazy',
  builder => sub { API::Docker::API::System->new(client => $_[0]) },
);


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


Docker daemon connection URL. Defaults to C<$ENV{DOCKER_HOST}> or
C<unix:///var/run/docker.sock>.

No other source is consulted; see L</Socket discovery>.

Supported formats:

=over

=item * C<unix:///path/to/socket> - Unix socket (default)

=item * C<tcp://host:port> - TCP connection

=back

=head2 api_version

Docker API version to use (e.g., C<1.41>). If not set, the client will
automatically negotiate the highest API version supported by the daemon.

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

Speak TLS on a C<tcp://> connection. Defaults to C<1> when
C<$ENV{DOCKER_TLS_VERIFY}> holds any non-empty value and L</host> is a
C<tcp://> one, and to C<0> -- plaintext -- otherwise.

    my $docker = API::Docker->new(
      host      => 'tcp://dockerhost:2376',
      tls       => 1,
      cert_path => '/home/me/.docker',
    );

The default follows the C<docker> CLI rather than the Go SDK's C<FromEnv>: the
CLI reads the variable as C<< != "" >>, so B<every non-empty value turns TLS
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

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


Directory holding the TLS certificates, in the layout the C<docker> CLI
writes: F<ca.pem> as the trust anchor, F<cert.pem> and F<key.pem> as this
client's certificate and key. Defaults to C<$ENV{DOCKER_CERT_PATH}>.

Each file is used if it is there. F<ca.pem> alone is a daemon this client
verifies but does not authenticate to; F<cert.pem> without F<key.pem> or the
reverse is a croak, since half a client certificate is an accident rather than
a mode. A C<cert_path> naming something that is not a directory croaks too.

B<Read only when L</tls> is set.> The default comes from the environment, and
C<DOCKER_CERT_PATH> is exported on plenty of machines that run the C<docker>
CLI, so a client that never asked for TLS is unaffected by having it set. A
TLS client that wants the system trust store rather than the CLI's private one
on such a machine passes C<< cert_path => undef >> explicitly.

=head2 tls_insecure

Turn certificate verification off. Default C<0>. Only read when L</tls> is
set, and named for what it does.

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

replaces three earlier failures on the same path -- a non-object body dying in
C<strict refs>, an object with no C<ApiVersion> silently leaving the client
sending every request unversioned, and a malformed C<ApiVersion> being copied
verbatim into the request path.

Options:

=over

=item * C<read_timeout> - Seconds of silence after which the request gives up
and croaks with an L<API::Docker::Error::Timeout>. Off by default; see
L<API::Docker::Role::HTTP/"Bounding a request that never ends">

=item * C<connect_timeout> - Seconds after which opening the connection gives
up and croaks with an L<API::Docker::Error::Timeout> whose C<< ->phase >> is
C<'connect'>. Off by default; see
L<API::Docker::Role::HTTP/"Bounding the connection itself">

=back

Called on its own, with no options, the negotiation is bounded by the
L<API::Docker::Role::HTTP/read_timeout> and
L<API::Docker::Role::HTTP/connect_timeout> attributes of the client, like any
other request. Reached the way it normally is -- automatically, from the first
request -- it inherits that request's own bounds instead; see
L</"What a timeout covers">.

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


    # the exception, for this run of calls
    $docker->containers->using(read_timeout => 5)->list;
    $docker->system->using(read_timeout => 0)->events;

L<API::Docker::Role::Using/using> returns a clone of the resource class
carrying the bounds, and every request made through that clone is given them.
There is deliberately no third way: the individual methods take no timeout
options, so their arguments are the request and nothing else.

Both are off by default, which is the behaviour this distribution has always
had. C<0> means the same as unset -- no bound -- and is how a client-wide
default is turned off for a run of calls: what C<using> carries is read with
C<exists> rather than for truth, so a C<0> reaches the transport instead of
vanishing into "no opinion".

Three things they do not do:

=over

=item * B<C<read_timeout> is an idle timeout, not a deadline.> The clock
measures the time since the last byte arrived, not the time since the request
started. A stream that keeps producing runs as long as it likes; one that

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

L</host> resolves in two steps and no more: C<$ENV{DOCKER_HOST}>, then
C<unix:///var/run/docker.sock>. It deliberately does B<not> read Docker
contexts. C<currentContext> in F<~/.docker/config.json> and the matching
F<~/.docker/contexts/meta/*/meta.json> are ignored, so if you switch daemons
with C<docker context use>, that choice is not picked up here. Set
C<DOCKER_HOST> explicitly instead.

Other clients sit at different points on that scale. The C<docker> CLI and
docker-java resolve contexts, with C<DOCKER_HOST> outranking them when set.
docker-py's C<from_env()> reads C<DOCKER_HOST> and otherwise falls back to the
default socket, leaving contexts to a separate API. Testcontainers layers its
own F<~/.testcontainers.properties> and a rootless probe list
(C<$XDG_RUNTIME_DIR/docker.sock>, F<~/.docker/run/docker.sock>,
F<~/.docker/desktop/docker.sock>, C</run/user/$UID/docker.sock>) on top.

What none of them do is guess Podman's socket path: that probe list is for
rootless Docker, not for Podman. Every one of those projects documents
C<DOCKER_HOST> as the way to reach Podman, which is the same answer given
above.

=head1 ENVIRONMENT VARIABLES

=over

=item C<DOCKER_HOST>

Docker daemon connection URL. Used as default for L</host> if not explicitly set.

Examples: C<unix:///var/run/docker.sock>, C<tcp://localhost:2375>

Also the supported way to reach a non-Docker engine such as Podman:
C<unix://$XDG_RUNTIME_DIR/podman/podman.sock>. See L</CONTAINER ENGINES>.

=item C<DOCKER_CERT_PATH>

Path to the TLS certificate directory (F<ca.pem>, F<cert.pem>, F<key.pem>).
Used as the default for L</cert_path>, which is read only when L</tls> is set
-- so having it exported, as machines running the C<docker> CLI usually do,
changes nothing for a client that speaks plaintext or over a Unix socket.

=back

=head1 SEE ALSO

=over

=item * L<API::Docker::Role::HTTP> - HTTP transport implementation

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

  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;

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


    # 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)
    my $tar = $docker->containers->get_archive($result->{Id},
        path => '/etc/hostname');
    $docker->containers->put_archive($result->{Id}, $tar, path => '/tmp');

=head1 DESCRIPTION

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

    my $containers = $containers->list(%opts);

List containers. Returns an ArrayRef of
L<API::Docker::Type::ContainerSummary> objects -- see L</"The two container
shapes"> for what a summary carries and L</inspect> does not.

Options:

=over

=item * C<all> - Show all containers (default shows just running)

=item * C<limit> - Limit results to N most recently created containers

=item * C<size> - Include size information

=item * C<filters> - HashRef of filter name to ArrayRef of string values, e.g.
C<< { status => ['running'], label => ['stage=build'] } >>. Shape-checked and
normalised by L<API::Docker::Role::Filters>

=back

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

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

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

Options:

=over

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

=item * C<signal> - Signal to send (default SIGTERM)

=back

=head2 restart

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

Restart a container. Optionally specify C<timeout> in seconds.

Reports 1/0 like L</start>, but a restart has no no-op state to report: the

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


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

Options:

=over

=item * C<signal> - Signal to send (default C<SIGKILL>)

=back

=head2 remove

    $containers->remove($id, force => 1, volumes => 1);

Remove a container.

Options:

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

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


    PODMAN  before   inspect: exited 4    wait: { StatusCode => 4 }
    PODMAN  after    inspect: created 0   wait: { StatusCode => -1 }
    DOCKER  before   inspect: exited 4    wait: { StatusCode => 4 }
    DOCKER  after    inspect: exited 4    wait: { StatusCode => 4 }

Podman reverts the container to C<created>, resets C<ExitCode> to 0, and
answers a later L</wait> with the sentinel C<-1> inside a 200. The real value
is gone from the engine; there is nothing to read it back from. B<All three
variants do it> -- C<< stream => 1 >> with and without C<logs>, and the
C<< stream => 0, logs => 1 >> this method now sends by default, which is the
one that returns cleanly in milliseconds. It is the call that does it, not
the hang.

L</logs> does B<not> do it, and Docker does not do it at all. So on Podman
the sequence "attach to collect the output, then L</wait> for the exit code"
cannot work: read the output with L</logs> instead, or take the exit code
before attaching.

=head2 This method refuses a container that is not running

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

=item * B<You cannot write to the container.> C<< stdin => 1 >> is passed to
the engine, but this client sends no bytes after the request headers and then
reads until the daemon closes, so there is no moment at which input could be
supplied. Use L<API::Docker::API::Exec> to run something interactive-shaped,
or wait for the upgraded variant

=item * B<Without a callback it returns when the stream ends, not before.>
With C<< stream => 1 >>, attaching to a container that keeps running blocks
until it exits or the daemon closes the connection -- and on a container that
is B<not> running it never returns at all, see
L</"The defaults follow the engine"> below. Pass C<on_frame> to read the
stream as it arrives and stop where you like, exactly as L</logs> does under
L</"Following the log">; the return value is then the summary HashRef
C<< { delivered => N, stopped => 0|1 } >> rather than the frames, and C<tty>
becomes a declaration the transport takes at its word -- an undeclared
unframed stream croaks. For a running container that need not be attached to,
L</logs> with C<tail> reads the same output and returns immediately

=back

C</containers/{id}/attach/ws>, the WebSocket variant, is not implemented
either.

=head2 The defaults follow the engine

C<stream> defaults to B<0> -- the engine's own default -- and C<logs> to
B<1>, which is the one flag that keeps the call useful without it. So
C<< $containers->attach($id) >> B<replays> what the container has written and
returns.

B<This is a change.> Up to and including the previous release C<stream>
defaulted to 1, so the same call opened an open-ended subscription; a caller
who wants the live stream now has to ask for it with C<< stream => 1 >>.

The reason is that the subscription has exactly one terminator: the container
ending. C<stream> means I<stream attached streams from the time the request
was made onwards>, so on a container that has B<already exited> that
terminator is in the past and will not happen again. attach also hijacks the
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->() >>.

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

=back

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

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

Options:

=over

=item * C<stream> - Subscribe to what the container writes from the time of
the request onwards. Default B<0>, which is the engine's own default.
C<< stream => 1 >> on a container that is not running never returns; see
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


    my $processes = $containers->top($id, ps_args => 'aux');

List running processes in a container. Returns hashref with C<Titles> and C<Processes> arrays.

Options:

=over

=item * C<ps_args> - Arguments passed to C<ps> inside the container, e.g.
C<'aux'>. Omitted, the engine uses its own default

=back

=head2 stats

    my $stats = $containers->stats($id);

Get container resource usage statistics (CPU, memory, network, I/O). With no
options this is the one-shot call it always was: a single reading, returned as
a HashRef.

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


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

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

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

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

    raw_body     => $raw,
    content_type => 'application/x-tar',
    params       => \%params,
    %headers ? ( headers => \%headers ) : (),
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $opts{on_event} ) : ( ndjson => 1 ),
  );
}


# The tag query parameter is not a default to hand out unconditionally: the
# engine appends it to whatever reference `fromImage` already carries. Docker
# lets tag take precedence and silently rewrites `nginx:1.25` to `nginx:latest`
# (a wrong image, reported as success); Podman concatenates to
# `nginx:1.25:latest` and answers 500 `invalid reference format`. A digest
# reference breaks the same way on both. So `tag` is defaulted only when the
# reference carries neither -- a `:tag` in the segment after the last `/`, or an
# `@digest` anywhere. The colon in a registry `host:port/` is before that
# segment, so it is not mistaken for a tag.
sub _reference_has_tag_or_digest {
  my ($self, $ref) = @_;
  return 1 if $ref =~ /\@/;
  my ($last_segment) = $ref =~ m{([^/]*)\z};
  return $last_segment =~ /:/ ? 1 : 0;
}

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


=item * C<< ->labels >> is top-level on a summary only. An inspect carries
the labels under C<< ->config->labels >>, where C<< ->config >> is the
L<API::Docker::Type::ImageConfig> the image runs containers with --
C<< ->cmd >>, C<< ->env >>, C<< ->entrypoint >>, C<< ->exposed_ports >> and
the rest.

=item * C<< ->containers >> (how many containers use the image) and
C<< ->shared_size >> come from a summary only. The swagger says of both that
C<-1> means the value was not calculated, and of C<SharedSize> that it is not
calculated by default -- so treat C<-1> as "unknown", not as a count.

=item * C<< ->architecture >>, C<< ->os >>, C<< ->os_version >>,
C<< ->variant >>, C<< ->author >>, C<< ->comment >>, C<< ->docker_version >>,
C<< ->config >>, C<< ->root_fs >>, C<< ->graph_driver >> and
C<< ->metadata >> come from an inspect only.

=item * C<< ->id >>, C<< ->repo_tags >>, C<< ->repo_digests >>, C<< ->size >>,
C<< ->descriptor >> and C<< ->manifests >> are on both and mean the same
thing. The swagger declares every field of a summary required and no field of
an inspect, which the model records but does not enforce -- see

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


    my $images = $images->list(all => 1);

List images. Returns an ArrayRef of L<API::Docker::Type::ImageSummary>
objects, each carrying the methods of L<API::Docker::Role::Entity::Image>.

Options:

=over

=item * C<all> - Show all images (default hides intermediate images)

=item * C<digests> - Include digest information

=item * C<filters> - HashRef of filter name to ArrayRef of string values, e.g.
C<< { dangling => ['true'] } >>. Shape-checked and normalised by
L<API::Docker::Role::Filters>

=back

=head2 build

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


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

=item * C<context> - Tar archive bytes (required)

=item * C<dockerfile> - Path to Dockerfile within the archive (default: C<Dockerfile>)

=item * C<t> - Tag for the image (e.g. C<name:tag>)

=item * C<q> - Suppress verbose build output

=item * C<nocache> - Do not use cache when building

=item * C<pull> - Always pull base image

=item * C<rm> - Remove intermediate containers (default: true)

=item * C<forcerm> - Always remove intermediate containers

=item * C<buildargs> - HashRef of build-time variables

=item * C<labels> - HashRef of labels to set on the image

=item * C<memory> - Memory limit in bytes

=item * C<memswap> - Total memory (memory + swap), -1 to disable swap

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

    }

=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.

C<tag> defaults to C<latest> B<only when C<fromImage> carries no tag or digest
of its own>. The engine appends C<tag> to the reference rather than treating it
as a fallback, so defaulting it onto an already-qualified name breaks the pull:
measured against Docker 29.7.2 (API 1.55) C<< pull(fromImage => 'nginx:1.25')
>> would silently fetch C<nginx:latest> and report success, and against Podman
5.8.4 (compat API 1.44) the same request answers C<500 invalid reference
format> for C<nginx:1.25:latest>. A digest reference breaks the same way on
both. So C<tag> is sent only if given explicitly, or defaulted to C<latest>
when the name carries neither a C<:tag> (in the segment after the last C</>)
nor an C<@digest>. A registry C<host:port/> prefix is not mistaken for a 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 pull croaks either way, but which way depends on the engine, so do
not write code that expects one of them:

=over

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

=item * C<container> - Container id or name to commit (required)

=item * C<repo> - Repository for the new image, e.g. C<myapp>

=item * C<tag> - Tag for the new image

=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

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

    params => \%params,
    %{ $self->_request_options },
  );
}


sub enable {
  my ($self, $name, %opts) = @_;
  croak __PACKAGE__ . '->enable plugin name required' unless $name;
  # Always sent, and not conditional on the caller passing it: the daemon
  # parses this parameter with strconv.Atoi and has no default, so an absent
  # timeout is parsed as the empty string and answers 400. See the POD.
  my %params = ( timeout => $opts{timeout} // 0 );
  return $self->client->post("/plugins/$name/enable", undef,
    params => \%params,
    %{ $self->_request_options },
  );
}


sub disable {

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


This is the first half of the install; see L</"Installing is two calls, and
the engine enforces it">. Reading it is the point -- the result is what you
hand to L</install>, and the daemon accepts the install only if the two
lists agree.

A plugin that demands nothing answers with an empty ArrayRef.

The C<remote> reference is normalised by the daemon, so C<vieux/sshfs> and
C<docker.io/vieux/sshfs:latest> name the same plugin; C<:latest> is the
default when no tag is given.

Options:

=over

=item * C<auth> - Registry credentials for a plugin in a private registry;
HashRef of C<username> / C<password> / C<serveraddress> / C<identitytoken>,
or a pre-encoded base64 string. Sent as C<X-Registry-Auth>. The Engine API
reference does not document this header on this endpoint, but the daemon
reads it here exactly as it does on the pull

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

    $plugins->install('vieux/sshfs:latest', privileges => $privileges);

    # blanket grant, in one call
    $plugins->install('vieux/sshfs:latest', accept_privileges => 1);

Pull and install a plugin (C<< POST /plugins/pull >>). The plugin is installed
disabled -- call L</enable> afterwards.

C<privileges> is required. Without it this croaks and names both ways
forward; see L</"Installing is two calls, and the engine enforces it"> for
why it is not defaulted.

Options:

=over

=item * C<privileges> - ArrayRef of privilege HashRefs from L</privileges>.
Required, unless C<accept_privileges> is set

=item * C<accept_privileges> - Fetch the privileges and grant them, in one
call. A blanket grant: use it where the call site is allowed to trust the

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

    $plugins->enable('vieux/sshfs:latest');
    $plugins->enable('vieux/sshfs:latest', timeout => 30);

Enable an installed plugin. Returns C<undef>.

Options:

=over

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

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


=over

=item * C<username> - Registry account name

=item * C<password> - Its password or token

=item * C<email> - Legacy field, accepted and ignored by current registries

=item * C<serveraddress> - Registry to check against, e.g. C<ghcr.io>.
Omitted, the engine uses its default registry

=item * C<identitytoken> - Bearer token, instead of username and password

=item * C<auth> - The whole AuthConfig at once, in any shape
L<API::Docker::API::Images/push> accepts it: a HashRef, a JSON object, or a
base64url-encoded one. Cannot be combined with the keys above

=item * C<response> - HashRef the status line and the response headers are
written into, as for L<API::Docker::Role::HTTP/get>

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



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


has location => (
  is      => 'ro',
  default => sub { '' },
);


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


has reason => (
  is      => 'ro',
  default => sub { '' },
);


has body => (
  is      => 'ro',
  default => sub { '' },
);


has data => (
  is => 'ro',
);


sub as_string { $_[0]->message . $_[0]->location }

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



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


has events => (
  is      => 'ro',
  default => sub { [] },
);


has location => (
  is      => 'ro',
  default => sub { '' },
);


sub as_string { $_[0]->message . $_[0]->location }



1;

__END__

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



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


has location => (
  is      => 'ro',
  default => sub { '' },
);


has endpoint => (
  is      => 'ro',
  default => sub { '' },
);


has phase => (
  is      => 'ro',
  default => sub { 'read' },
);


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


has partial => (
  is      => 'ro',
  default => sub { '' },
);


has summary => (
  is => 'ro',
);


sub as_string { $_[0]->message . $_[0]->location }

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

that stalls is cut off. That is the distinction the endpoints this exists for
need -- a hung C</containers/{id}/attach> has already delivered its buffered
frames before it stalls, so "nothing yet" would never have fired.

=head2 Why it is fatal, on every path

A timeout is not information about the response; it is the absence of it. The
transport cannot know whether the daemon was about to send the rest, so it
cannot decide for the caller that what arrived is usable -- and every return
shape this distribution promises would hide the question if it tried. C<ndjson>
promises an ArrayRef of events, C<raw> promises the response bytes, the default
promises the decoded body: a truncated value satisfies all three and is
indistinguishable from a complete one. A half tarball that looks whole is a
worse outcome than the hang it replaced.

That holds for the callback streams too, even though they have already handed
the caller every complete unit. Returning normally there would run the stream
handler's finish step, which is written for a daemon that closed: it treats a
trailing partial line as a complete final event, and reports leftover bytes as
a frame the daemon cut in half. Neither statement is true of a timeout. One
rule -- a timeout is fatal -- also keeps C<read_timeout> meaning the same thing

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


=head2 endpoint

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

=head2 phase

Which of the two bounds fired: C<'read'> for
L<API::Docker::Role::HTTP/read_timeout>, C<'connect'> for
L<API::Docker::Role::HTTP/connect_timeout>. C<'read'> is the default, so an
object built without it describes what every one of them used to describe.

One class rather than two, because the question a caller catches this to ask
-- "did the request finish in time?" -- has the same answer either way, and a
second class would make every such caller name two of them or find their
common base. What differs is one bit: whether the daemon was ever reached.
That bit is this attribute.

It is also the only thing that tells a C<'connect'> timeout apart from a
C<'read'> one that expired before the first byte: L</partial> is the empty

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



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


has location => (
  is      => 'ro',
  default => sub { '' },
);


has endpoint => (
  is      => 'ro',
  default => sub { '' },
);


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


has expected => (

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

);


has received => (
  is => 'ro',
);


has partial => (
  is      => 'ro',
  default => sub { '' },
);


has summary => (
  is => 'ro',
);


sub as_string { $_[0]->message . $_[0]->location }

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

C<application/vnd.docker.raw-stream> family -- announces no end and has no
terminator either, so there an EOF B<is> the end and this is never raised.
Its B<head> is framed like any other, and is checked like any other.

=head2 Why it is fatal

For the same reason L<API::Docker::Error::Timeout> is, and the two are the
same defect reached by different routes: a short body satisfies every return
shape this role promises and is indistinguishable from a complete one.
C<ndjson> promises an ArrayRef of events and gets a shorter one; C<raw>
promises the response bytes and gets fewer of them; the default promises the
decoded body and gets whatever the truncated bytes happened to parse as. A
half tarball that looks whole is the worst of them, and it is the case this
exists for.

Nothing is lost by raising it: L</partial> carries the bytes a buffered read
had collected and L</summary> the count a streamed one had delivered, so a
caller who wants what arrived can have it. What it cannot do any more is
mistake it for everything.

=head2 What it is not

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

Get fresh config information. Returns another L<API::Docker::Type::Config> --
the same class, since the daemon describes a config one way.

=head2 update

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

Update the config. Passes C<< ->id >> and, by default, L</version_index> to
L<API::Docker::API::Configs/update>; everything else is the spec and becomes
the request body.

The default is only a default. A C<version> key in the arguments is used
verbatim and removed before the spec goes out -- the spec's own fields are all
capitalised (C<Name>, C<Labels>, C<Data>, ...), so a lowercase C<version>
cannot collide with one:

    $config->update(version => $index, %spec);

A C<Data> passed here is raw bytes and gets encoded on the way out, so the
C<Data> that C<< $config->spec->TO_JSON >> brings along -- already base64 --
would be encoded a second time. Drop it, or pass L</decoded_data> in its
place.

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

=head2 attach

    my $frames = $container->attach;

Attach to the container's output and return the frames, one-way. Every option
goes to L<API::Docker::API::Containers/attach>, C<on_frame> included; with a
callback the return value is that method's summary HashRef rather than the
frames. Without options it replays what the container already wrote and
returns; C<< stream => 1 >> on a container that is not running never
returns -- not even with a callback -- see
L<API::Docker::API::Containers/"The defaults follow the engine">.

B<The container must be running.> Attaching to one that has already exited
destroys its exit status on Podman, so the call checks first and croaks rather
than attaching; L</logs> is how a finished container's output is read.
C<< require_running => 0 >> attaches anyway. The check is a pre-flight one and
does not close the race against a container stopping underneath it -- see
L<API::Docker::API::Containers/"This method refuses a container that is not running">.

=head2 inspect

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

C<< $plugin->settings->env >>, not the objects of C<< $plugin->config->env >>
-- see L</"Two shapes of Env, one level apart">.

=head2 upgrade

    my $privileges = $docker->plugins->privileges($plugin->plugin_reference);
    $plugin->upgrade(remote => $plugin->plugin_reference,
        privileges => $privileges);

Upgrade the plugin in place. C<privileges> is required, as it is on
L<API::Docker::API::Plugins/upgrade>, and C<remote> defaults to
L<API::Docker::Type::Plugin/name> -- which is not what you want for a
plugin installed under a local name, hence
C<< ->plugin_reference >>.

=head2 push

    $plugin->push(auth => { username => 'me', password => 'secret' });

Push the plugin to a registry. B<This writes to a real registry> under the
credentials given.

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


Get fresh secret information. Returns another L<API::Docker::Type::Secret> --
the same class, since the daemon describes a secret one way.

=head2 update

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

Update the secret. Passes C<< ->id >> and, by default, L</version_index> to
L<API::Docker::API::Secrets/update>; everything else is the spec and becomes
the request body.

The default is only a default. A C<version> key in the arguments is used
verbatim and removed before the spec goes out -- the spec's own fields are all
capitalised (C<Name>, C<Labels>, C<Data>, ...), so a lowercase C<version>
cannot collide with one:

    $secret->update(version => $index, %spec);

Send the whole spec back with the one key edited; the Engine API accepts a
change to C<Labels> only and wants every other field unchanged. Podman does
not implement this endpoint and answers 501.

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

    }
    return $sock;
  }
  else {
    croak "Unsupported host format: $host (expected unix:// or tcp://)";
  }
}

# Loaded here rather than with the other modules at the top of the file.
# IO::Socket::SSL pulls in Net::SSLeay, which is XS compiled against libssl,
# and the unix:// transport -- local Docker, rootless Podman, the default and
# the only one most installations use -- never needs a byte of it. A hard
# dependency would make this client unbuildable on a machine with no OpenSSL
# headers for the sake of a transport it is not using, so it is a recommended
# one and this is the point where its absence becomes an error.
sub _load_ssl {
  my ($self) = @_;

  return 1 if eval { require IO::Socket::SSL; 1 };
  my $why = $@ || 'unknown error';
  $why =~ s/\s+\z//;

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


# cert.pem, key.pem and ca.pem in one directory -- the layout the docker CLI
# writes and the one cert_path has always pointed at, whether or not anything
# read it.
sub _ssl_certificates {
  my ($self) = @_;

  my $dir = $self->cert_path;
  return () unless defined $dir && length $dir;

  # cert_path defaults from DOCKER_CERT_PATH, so it can arrive from a machine's
  # environment rather than from this caller -- but it is only looked at once
  # TLS was asked for, and at that point a path naming nothing is a mistake
  # worth stopping on rather than quietly connecting without the certificates
  # the caller believes are in use.
  my $path = path($dir);
  croak __PACKAGE__ . ": cert_path $dir is not a directory. TLS expects the "
    . 'layout the docker CLI writes -- ca.pem, cert.pem and key.pem in one '
    . 'directory -- and this names nothing that could hold it'
    unless $path->is_dir;

  my %ssl;

  # No ca.pem is not an error: verifying a daemon behind a terminator with a
  # publicly trusted certificate needs no private trust anchor, and the
  # default store is then the right one. See L</"TLS on a tcp:// connection">.
  my $ca = $path->child('ca.pem');
  $ssl{SSL_ca_file} = "$ca" if $ca->exists;

  my $cert = $path->child('cert.pem');
  my $key  = $path->child('key.pem');
  my @half = grep { !$_->[1]->exists }
    ( [ 'cert.pem', $cert ], [ 'key.pem', $key ] );

  # One of the two is never a mode, only ever an accident: a key with no
  # certificate proves nothing and a certificate with no key cannot be used.

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

      . (defined $endpoint && length $endpoint ? ' (' . $endpoint . ')' : '')
      . ': ' . $where . ' did not accept within ' . $pending->{timeout} . 's',
    location => shortmess(''),
    endpoint => defined $endpoint ? $endpoint : '',
    timeout  => $pending->{timeout},
    phase    => 'connect',
  );
  croak $error;
}

# undef for "no timeout", which is both the default and the explicit 0, so a
# client carrying a default can be opted out of for one request. Anything that
# is not a non-negative number is a caller mistake and is refused rather than
# rounded to something: silently reading a typo as "off" would hand back the
# hang the caller was asking to be protected from.
sub _timeout_value {
  my ($self, $name, $timeout) = @_;

  return undef unless defined $timeout;
  croak __PACKAGE__ . '->_request ' . $name . ' must be a non-negative number '
    . 'of seconds (0 or undef for none), not "' . $timeout . '"'
    unless !ref $timeout && looks_like_number($timeout) && $timeout >= 0;

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


  $request .= "\r\n";
  $request .= $body_content if defined $body_content;

  my $handler = @streaming
    ? $self->_stream_handler($endpoint, $streaming[0], $opts{$streaming[0]},
        $opts{croak_on_error} // 1)
    : undef;

  # Resolved with exists rather than truth, so `read_timeout => 0` is a
  # request to wait as long as it takes and can turn a client-wide default off
  # for one call -- which `//` would have read as "no opinion" and overridden.
  my $timeout = $self->_read_timeout_value(
    exists $opts{read_timeout} ? $opts{read_timeout} : $self->read_timeout);

  # Same resolution, same reason.
  my $connect_timeout = $self->_connect_timeout_value(
    exists $opts{connect_timeout}
      ? $opts{connect_timeout} : $self->connect_timeout);

  # The endpoint without its query string, for the same reason the >= 400

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

    . 'a space, CR, LF, ? or # in a container name or image reference would '
    . 'rewrite the request rather than name a resource. A path is rejected '
    . 'rather than sanitised: percent-encoding it here cannot tell a path '
    . 'separator from data -- pass query parameters as `params`, not in the '
    . 'path';
}

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

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

sub _read_head {
  my ($self, $sock, $ctx) = @_;

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


version 0.004

=head1 SYNOPSIS

    package MyDockerClient;
    use Moo;

    has host         => (is => 'ro', required => 1);
    has api_version  => (is => 'ro');
    has tls          => (is => 'ro', default => 0);
    has cert_path    => (is => 'ro');
    has tls_insecure => (is => 'ro', default => 0);

    with 'API::Docker::Role::HTTP';

    # Now use get, post, put, delete_request, head methods
    my $data = $self->get('/containers/json');

=head1 DESCRIPTION

This role provides HTTP transport for the Docker Engine API. It implements
HTTP/1.1 communication over Unix sockets and TCP sockets without depending on

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

=back

The two halves of the client certificate go together: one of them present
without the other is a croak, because a key with no certificate proves nothing
and a certificate with no key cannot be used. A directory holding only
F<ca.pem> is fine -- that is a daemon this client verifies but does not
authenticate to. A C<cert_path> that names nothing is a croak: it is read only
once TLS was asked for, and at that point a path pointing nowhere means the
caller believes certificates are in use that are not.

C<cert_path> defaults from C<DOCKER_CERT_PATH>, so on a machine that also runs
the C<docker> CLI it arrives set. Without C<< tls => 1 >> nothing reads it, so
that costs nothing; with it, pass C<< cert_path => undef >> to use the system
trust store instead of the CLI's private one.

=head3 TLS with no certificates at all

It means B<encrypt and verify against the system trust store>, not an error.

C<tls> asks for a connection that is encrypted and whose far end is
authenticated. It does not ask to authenticate this client, which is what the

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


It exists for a self-signed daemon certificate whose CA is genuinely not to
hand. The better answer to that is nearly always F<ca.pem>: a self-signed
certificate is its own CA and can be used as the anchor directly.

=head3 The dependency

L<IO::Socket::SSL> is a B<recommended>, not a required, dependency, and it is
loaded at the moment the first TLS connection is opened. It brings in
L<Net::SSLeay>, which is XS compiled against libssl, and the C<unix://>
transport -- local Docker, rootless Podman, the default -- never needs any of
it; requiring it would make this client unbuildable on a machine with no
OpenSSL headers for the sake of a transport it is not using. Without it,
C<< tls => 1 >> croaks naming the module and how to install it, at the same
point every other connection failure is reported.

=head2 read_timeout

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

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

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

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

=head2 connect_timeout

Seconds after which opening the connection gives up and croaks with an
L<API::Docker::Error::Timeout> whose C<< ->phase >> is C<'connect'>. C<undef>
-- the default, and what every existing caller gets -- means no bound and is
the behaviour this distribution has always had; C<0> means the same and is the
way to say it explicitly.

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

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

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


=head3 It is an idle timeout, not a deadline

The clock measures the time since the last byte arrived, not the time since
the request started. A stream that keeps producing runs as long as it likes;
one that stops producing is cut off. That distinction is the whole point --
both hangs above deliver data first and stall afterwards, so a bound on the
total time would have to be set longer than any legitimate stream, and a bound
on the time to the first byte would never fire at all.

=head3 There is no default, and no per-endpoint default either

Off unless asked for, everywhere. Whether a silence is a stall or normal is a
property of the workload rather than of the endpoint: C</build> with a large
context is legitimately quiet for as long as C</events> is, and a built-in
default on C<attach> would kill a perfectly healthy session at an idle shell
prompt. So no existing call changes behaviour, and picking the number is the
caller's -- who is the only one who knows what the request is for.

For the two endpoints above, if you want a figure to start from: a couple of
seconds is right for C<attach> or C<logs> used to collect what is already
there, and something above the daemon's own emit interval -- Docker sends a
stats reading about once a second -- for C<stats>.

=head3 What happens when it expires

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

the bound.

Over TLS it is not quite an idle timer on the plaintext. C<SO_RCVTIMEO> bounds
each blocking receive on the underlying socket, and one plaintext read can
consume several of those while a TLS record arrives in pieces -- so a record
dribbling in slowly enough resets the clock without a byte reaching the
caller. It still bounds the hang, which is what it is for.

=head2 Bounding the connection itself

L</connect_timeout> is the other half, and it is off by default for the same
reason: nothing here changes behaviour unless it is asked for.

    my $docker = API::Docker->new(connect_timeout => 5, read_timeout => 30);

What it does is not the same on all three transports, and the difference was
measured rather than assumed:

=over

=item * C<tcp://> -- a real bound. Against a host that drops SYNs, an unbounded

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

object stringifies to the reason plus Carp's usual location suffix, so
C<eval>-and-inspect-C<$@> code cannot tell it from the plain croak it
replaces; C<< $err->events >> carries the complete event list, so the progress
output that led up to the failure is not lost with the return value.

The trigger is the C<errorDetail> key alone. The flat C<error> key the engine
sends beside it holds the same text and is used only as a fallback message,
never as the trigger on its own.

C<< croak_on_error => 0 >> turns the scan off for a stream that is a feed
rather than an operation. The check is on by default, and opting out is per
endpoint, because the set of operation-shaped streaming endpoints is
open-ended while the feed-shaped ones are C</events> and nothing else: a new
endpoint added without a thought about this gets the loud behaviour, not the
silent one.

=head2 Failure in the middle of a response

The daemon can also stop saying anything in the middle of saying it. A status
line with no terminator, a header block with no blank line to close it, a body
shorter than its C<Content-Length>, a chunk shorter than its own header, a

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

how the piece is framed: where the response announced a length, what arrived
is compared against it; where the framing is by terminator instead -- the head
and the chunk headers -- it asks whether the terminator came before the stream
ended, which is decidable without anything to compare. The exception carries
what did arrive: C<< ->partial >> for a buffered request, C<< ->summary >> for
a streamed one, and C<< ->phase >> for which piece of the framing ran out.

This B<is> a behaviour change and not a bug fix in passing. Until it existed
every shape above was returned rather than raised, and none of them was
distinguishable from a complete response: C<ndjson> gave a shorter ArrayRef,
C<raw> gave fewer bytes, the default gave whatever the truncated bytes
happened to parse as. A cut head was quieter still -- the response was read on
with whichever headers had arrived, and one cut before C<Content-Length> and
C<Transfer-Encoding> left neither, which is the close-delimited path below,
where an EOF is the legitimate end and nothing looks wrong. Code that was
silently receiving half a response now gets an exception where it used to get
a value.

The one thing here that is B<not> raised as an object: a connection that
closed without a single byte of a status line still croaks with the plain
C<No response from Docker daemon> string it always has. Nothing about it was

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

cannot tell a caller what the engine said when it did not finish saying it.
C<< ->partial >> holds the part of the error body that did arrive.

=head2 Header names are rejected, header values are stripped

A CR or LF in a header B<value> is stripped and the value is flattened onto
its own line. A header B<name> that is not an RFC 9110 token is refused with
a croak instead.

The asymmetry is deliberate. A value can pick up a stray newline honestly --
C<MIME::Base64::encode_base64> wraps its output by default, and a token pasted
out of a file brings its line ending along -- and flattening it preserves what
the caller meant. A name is a literal the programmer wrote; there is no benign
way for one to contain CR, LF, a space or a colon, and quietly rewriting
C<< "X-Foo\r\nX-Bar" >> into C<X-FooX-Bar> would put a header on the wire
under a name nobody asked for. Validating against the token grammar also
catches the separators that would corrupt the request without injecting
anything.

=head2 A request path is rejected, not sanitised

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

# attribute (it calls _invalidate_docker_cache), so they can be cached. Keyed
# by class name, one entry each.
my %ATTR_CACHE;    # class -> { perl_name => info }
my %ORDER_CACHE;   # class -> [ perl_name, ... ]
my %WIRE_CACHE;    # class -> { wire_name => perl_name }
my %ENTITY_CACHE;  # class -> { perl_name => 1 }  (not daemon fields)


has unknown_fields => (
  is      => 'ro',
  default => sub { {} },
);


has rejected_fields => (
  is      => 'ro',
  default => sub { {} },
);

# True while `from_data` is inflating a decoded engine response, so that the
# hashref coercion in API::Docker::Type reads a nested literal the same way
# the outermost one was read. Dynamically scoped: `local`ised around the
# whole construction, which is what a nested from_data sees.
our $RESPONSE = 0;


# Constructor-side name resolution for `new`, and one of the two places



( run in 3.399 seconds using v1.01-cache-2.11-cpan-6736b670a1e )