API-Docker

 view release on metacpan or  search on metacpan

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

fields, healthcheck details, error message text — is unverified until you have measured
it there, and a difference from Docker is worth writing into the `Changes` entry.

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

## Verification

`prove -lr t/` for the fixture suite. For live checks against the Podman socket, set
`API_DOCKER_TEST_HOST`; only add `API_DOCKER_TEST_WRITE=1` when the task genuinely needs
containers created, and clean up what you create. Never run `images->push` against a real
registry, and never `dzil release`.

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

   The house standard here is a measured claim, not a summary: existing entries name the
   engine's exact error string and the observed before/after. Flag an entry that asserts
   daemon behavior without evidence.
5. **POD in sync with the code.** Every public method has `=method`, every attribute
   `=attr`, and the option lists match what the method actually forwards — the drift most
   likely to ship is an option added to a `%params` block and never documented. Check
   `README.md` against `lib/API/Docker.pm`'s SYNOPSIS too.
6. **`dzil build`** — clean, no missing files, no warnings; then `dzil test` green,
   including the generated `xt/` author and release tests (pod-syntax,
   changes_has_content).
7. **`prove -lr t/`** green with no environment set — the suite must not require a
   daemon. If every file dies with exit 2 and no plan, report a missing build dependency,
   not a test failure.
8. **No Getty-authored dependency here yet.** If one appears in `cpanfile`, it must be
   pinned to its actual released CPAN version (`cpanm --info`), never to the version in
   the sibling repo's `lib/` — that one is unreleased.

Report: ready, or a concise list of what blocks release. File blockers as karr tickets on
this repo's board.

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

    - 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,
  the NDJSON fallback — is invisible to a route-table test. Transport behavior is tested

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


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.
Check the swagger for `additionalProperties` before deciding a hash's keys are structure.

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


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

## Parallel fan-out — isolate the working tree

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

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

## Coordination — karr board (always in scope)

Ticket coordination is the orchestrating agent's job, so `karr` is always in scope —
don't invoke the `kanban-issues-karr-cli` skill first, just use it. Git-native kanban;
state lives in `refs/karr/*`; one board, this repo. Day-to-day: `karr list --compact` /
`karr board` for open work; `karr show ID` for detail; `karr create/edit/move/handoff`
for the usual flow; mutating commands auto-sync. Use it for drift to reconcile and
follow-up work that must not block the current change. Full surface: skill
`kanban-issues-karr-cli`.

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

  removes actual containers, images, networks and volumes; cleanup runs in an `END`
  block, so an interrupted run leaves them behind. Run only when the task is about live
  behavior.
- **`prune` destroys, and `dangling => 0` destroys MORE, not less.** `POST
  /images/prune` with `filters => { dangling => ['false'] }` removes every
  unused *tagged* image on the engine, locally built ones included, and they
  are not recoverable. It reads like a narrowing filter and is the opposite.
  This has already cost a locally built image, during what its caller
  believed was a read-only probe. **No `prune` of any
  kind -- images, containers, networks, volumes, build cache -- and no
  `rm -a` or `system reset`, on either engine, ever, unless the user names
  the command.** Probing what an endpoint answers is not a reason: measure
  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

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

untyped passthrough.

**An unknown field passes through unchanged.** A caller whose engine is newer
than the spec we generated from must still reach the daemon; the model
translates what it knows and forwards the rest verbatim. This is not
theoretical: a real Podman `/info` answers with fields the swagger does not
have, and `ImageSummary` still serves `VirtualSize`, which Docker dropped from
the spec after v1.41.

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

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


Runs of capitals are one word. When the derivation produces something that
collides with another field in the same class, or reads wrong, pass `wire`
explicitly and choose the Perl name by hand — do not bend the derivation to
fit one case.

## Serialisation, both directions

`TO_JSON` walks the registry, not the object's keys:

- an attribute that was never set is omitted, not sent as null — and a known
  field the engine sent as an explicit null is such an attribute, so its key
  does not come back (the daemon cannot tell null from absent; measured, see
  `API::Docker::Role::Type`)
- a nested object is serialised by its own `TO_JSON`
- an ArrayRef of objects maps over them
- a HashRef whose keys are caller data passes its keys through untouched
- anything the caller stored under a name the registry does not know is
  forwarded verbatim, so a newer engine's field still reaches the daemon —
  its null included, because an untyped name has no zero value to read one as

Inflation is the mirror: a known wire name becomes the typed attribute, an
unknown one is kept as-is under its original name.

## Booleans

Docker distinguishes an absent flag from a false one. `Bool` must serialise to
JSON `true`/`false` and never to `1`/`""`, and an unset Bool must be absent
rather than false. Two traps make this harder than it looks in Perl: every
reference is true, so `\0` and a `JSON::PP::Boolean` must be dereferenced
rather than tested, and `'false'` is a non-empty string and therefore true, so
the strings have to be spelled out. `_normalize_bool` in
`lib/API/Docker/Type.pm` is the one place this is decided; anything that can
mean true or false goes through it, and anything that cannot dies.

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


## cpanfile

- **A `cpanfile` carries the requirements** — that is the file, not `dist.ini` prereq blocks.
- **`requires 'Module::Name';`** — the version argument is optional, omit it when unpinned. Never write `'0'`.
- **A version means "or higher".** `requires 'Foo', '5.0';` already accepts 5.1 — never write `'>= 5.0'`.
- **Alphabetical order**, phase blocks (`on test => sub {...}`) at the end.

### Getty-authored dependencies — CRITICAL

Getty's `dist.ini` uses `[@Author::GETTY]`, which sets `$VERSION` in the repo to the **next, unreleased** version (`0.402` while CPAN is at `0.401`). The repo is ALWAYS one ahead of CPAN.

1. **Pin what the code actually needs, not the number that happened to be in front of you.** Opening a sibling repo, reading its `$VERSION` and pinning that — while depending on nothing that version introduced — is the mistake this section exists...
2. **Check `cpanm --info Module::Name`** for the released version. If the released one carries what you use, that is the pin.
3. **Pinning the next, unreleased version is correct when the change spans both repos** — the sibling gained what this code calls, or a release is being prepared and the distributions are tested together from their working trees. It commits you to ...
4. **Pin every Getty-authored distribution.** Not stale, not omitted — current.
5. **Re-check on upgrade.**

```bash
cpanm --info Module::Name | tail -1
# → GETTY/Module-Name-1.234.tar.gz  ← pin to 1.234

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


---

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

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

---

## Pattern 5 – Delegation via `handles`

```perl
package App::UsesCounter;
use Moo;
has counter => (
  is      => 'ro',
  required => 1,
  handles => 'App::Role::CounterAPI',   # role defines the interface (inc/reset/value)
);
```

Three forms: `handles => 'RoleName'` (interface from role), `handles => [qw(inc reset)]` (list), `handles => { add => 'push' }` (rename). Does not trigger `isa`/`coerce`/`trigger` on the delegate itself.

---

## Pattern 6 – Native-Trait Delegation via `Sub::HandlesVia`

```perl
package Kitchen;
use Moo;
use Sub::HandlesVia;
use Types::Standard qw(ArrayRef Str);

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

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

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

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

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

---

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


---

## Decision Guide

| Situation | Use |
|---|---|
| Shared attributes/methods, stable "is-a" | `extends` |
| Optional/horizontal feature | `Moo::Role` + `with` |
| Same pattern, different config | `MooX::Role::Parameterized` |
| Delegate method set to sub-object | `handles` |
| Array/Hash operations on attribute | `Sub::HandlesVia` |
| Logging/validation/caching wrapper | `before`/`around`/`after` |
| Catch constructor typos | `MooX::StrictConstructor` |
| 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'` |

---

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

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.

## When the bundle applies

`[@Author::GETTY]` is for Getty's own CPAN work. A distribution that is not
released to CPAN — a proprietary application, a deploy artefact — lists its
Dist::Zilla plugins explicitly instead, because the bundle assumes a CPAN
release. Non-CPAN dists that still use the bundle set `no_cpan = 1`.

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

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

1. `{{$NEXT}}` in Changes is replaced with `1.005` + release date
2. The version is bumped to `1.006` (or next AutoVersion value)
3. A Git tag `v1.005` is created

**Do NOT treat the version in dist.ini as the released version.** If the user asks "what version is released?", check CPAN or git tags — not the current `$VERSION` in the files.

**Do NOT bump the version manually before a release** — `dzil release` handles this automatically.

### Every file carries its own `$VERSION`

**Each file under `lib/` and `bin/` needs its own `our $VERSION = '...';`**, set to
the version that will be released NEXT — one higher than what is on CPAN (or
higher). A file without a `$VERSION` ships versionless and breaks consumers that
pin against it.

**Only the FIRST `our $VERSION` in a file gets rewritten.** RewriteVersion::Transitional
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


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
touching it, and it uses the very test `pick` uses. It is not the opposite of
`--claimed-by NAME`: that one is an exact match on the field and matches an
expired claim too, so the two overlap on "cards NAME no longer holds" and
passing both is a usage error. Since it asks about the claim and nothing else,
a blocked card nobody holds is still listed -- `--blocked --unclaimed` is a
real triage query.

### Show task

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

```bash
# in the other repository -- raise the card and record where it came from
karr create "Fix the API" --escalated-from home#5

# here -- record what you are waiting for, block, release the claim, leave
karr edit 5 --add-needs other-repo#7 --block "needs other-repo#7: API change first" --release

# any time -- what is this board waiting on, and is it done yet?
karr needs
karr needs --board other-repo=/srv/other-repo     # where that board is on THIS machine
karr needs --resolve                              # drop settled links, unblock what is free
```

A reference is `BOARD#ID`: the other board's **name** and a task id. Never a
path -- the card is shared state and two clones of the same fleet have
different directories. karr turns the name into a directory from
`--board NAME=PATH` or from the fleet config
(`~/.config/karr-foundation/config.yml`, `--fleet-config` to point elsewhere),
matching the repository's directory basename.

`--resolve` settles a link whose far card has reached one of the **far** board's
own terminal statuses, and lifts the `blocked` flag when a card's last link
settles, printing the reason it lifted. A far card that does not exist settles
nothing. A board this machine cannot place is reported, not fatal.

Like `depends_on`, a cross-board link blocks nothing by itself: `pick` hands the
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

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

`.karr` `command` and `claude: true`, and `--force` does not override it. Nothing
else changes: the board stays fully usable by hand (`karr list`, `karr pick`,
`karr move`, …). Use it for a repository whose backlog is parked rather than
abandoned.

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

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

### Context (board summary for embedding)

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

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

karr destroy --yes
```

Deletes the entire `refs/karr/*` namespace from the repository and prunes the
remote board state too when a remote is configured. Prefer taking a
`karr backup` first.

### Helper refs

```bash
karr set-refs superpowers/spec/1234.md draft ready
karr set-refs superpowers/spec/1234.md < design.md    # multi-line payload
karr get-refs superpowers/spec/1234.md
```

Stores and retrieves helper payloads in Git refs outside protected namespaces
such as `refs/karr/*`, branches, and tags. Use this for shared planning blobs,
agent scratch data, or similar workflow artifacts that should sync through Git
without becoming task cards.

The arguments after the ref are joined with a single space, so they are a
one-line payload. A document goes in on stdin instead — with no content
argument at all, `karr set-refs REF < file` stores the file verbatim and
`karr get-refs REF > file` gives it back unchanged.

### Activity log

```bash
karr log                                     # last 20 entries
karr log --agent swift-fox                   # filter by agent
karr log --task 5                            # filter by task
karr log --last 50 --json                    # more entries, JSON
karr log --compact                           # one line per entry, no padding

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

1. **Need a board?** → `karr init`
2. **New work item?** → `karr create "Title" --priority high`
3. **What's on the board?** → `karr board` or `karr list`
4. **Starting work?** → `karr pick --claim NAME --move in-progress`
5. **Done with task, hand to review?** → `karr handoff ID --claim NAME --note "reason"`
6. **Done with task, close it?** → `karr edit ID --release && karr move ID done`
7. **Blocked?** → `karr edit ID --block "reason"`
8. **Need details?** → `karr show ID`
9. **Soft-delete?** → `karr archive ID`
10. **Board snapshot for agent context?** → `karr context --write-to AGENTS.md`
11. **Check/change config?** → `karr config` / `karr config set KEY VALUE`
12. **Install agent skills?** → `karr skill install`
13. **Need a full board snapshot?** → `karr backup` / `karr restore --yes`
14. **Need shared non-task workflow data?** → `karr set-refs` / `karr get-refs`
15. **Board should never be drained by an automation host?** → `karr disable --reason "why"`
16. **Need to remove the board completely?** → `karr destroy --yes`
17. **Overview of every board under a directory?** → `karr dashboard`

## Multi-agent workflow

```bash
# 1. Generate agent name and pick task
NAME=$(karr agent-name)
karr pick --claim $NAME --status todo --move in-progress

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

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

# 2. Or pipe a whole document in - arguments are joined with a space and
#    would flatten it into one line
karr set-refs superpowers/spec/1234.md < design.md

# 3. Read it back elsewhere
karr get-refs superpowers/spec/1234.md
```

Use helper refs for coordination data that should travel with Git but should
not affect the board state itself.

.gitignore  view on Meta::CPAN

API-Docker-*
.build

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

# karr materialized task view -- never commit
tasks/

CLAUDE.md  view on Meta::CPAN

# CLAUDE.md

Repo-specific guidance for Claude Code working on `API::Docker`.

## The 12 Rules

These are the operating rules for this repo. They inherit from the global
and workspace `CLAUDE.md` — what's listed here is the authoritative set
for this distribution.

1. **Use `mcp__serper__google_search` or `mcp__firecrawl__firecrawl_search`**
   over `WebSearch` for any web lookup.

2. **Use `mcp__firecrawl__firecrawl_scrape`** over `WebFetch` for fetching
   page content.

3. **Use `context7` for library docs** (CPAN, npm, etc.) — *except* this
   distribution itself. For `API::Docker` always read the local source

CLAUDE.md  view on Meta::CPAN


```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
# what each one announces: Platform.Name, ApiVersion, MinAPIVersion

Changes  view on Meta::CPAN

    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
    mis-shaped filter is normalised to the engine's JSON map-of-arrays
    instead of silently matching nothing.
  - JSON request bodies send booleans as real `true`/`false`; a caller may
    pass `1`/`0` or a JSON boolean interchangeably.
  - `_uri_encode` UTF-8-encodes a decoded character string before
    percent-escaping, so a name or tag typed as characters (`ü`, `中`) goes
    out as valid UTF-8.
  - A request path outside the RFC 3986 origin-form character set is refused
    before it reaches the daemon, closing a request-line injection through a
    container name or image reference.
  - An ArrayRef query parameter expands into one repeated `k=v` pair per
    element (`names=a&names=b`), which some endpoints require.
  - A bare JSON scalar body (`null`, `true`, a number, a quoted string) is
    decoded rather than handed back as raw bytes; `raw` and `ndjson` return
    `''` and `[]` for a zero-byte body instead of `undef`.
  - Registry credentials reach `images->pull` (`auth`, sent as
    `X-Registry-Auth`) and `images->build` (`registry_config`, sent as
    `X-Registry-Config`), sent only when given. An already-base64 auth value

Changes  view on Meta::CPAN

    would get, and a separate case pins the exact padded header.
  - Document that this client speaks the Docker Engine HTTP API over a
    socket and never shells out to the `docker` binary, so any engine
    serving that API works -- Podman's rootless socket needs nothing
    but `DOCKER_HOST`. The new CONTAINER ENGINES section also states
    what socket discovery deliberately does not do: Docker contexts
    (`currentContext`, `~/.docker/contexts/meta/*/meta.json`) are never
    consulted, unlike the `docker` CLI, docker-java or Testcontainers.

0.002     2026-05-17 05:36:20Z
  - HTTP role: `_request` now accepts a `headers => {}` option to set
    extra HTTP request headers. Headers are sanitised against CR/LF
    injection. Used by `images->push` to send `X-Registry-Auth`, and
    available to any caller that needs custom headers.
  - `images->push` now always sends an `X-Registry-Auth` header — the
    Docker Engine refuses pushes without it (`HTTP 400: missing
    X-Registry-Auth: invalid X-Registry-Auth header: EOF`). A new `auth`
    option accepts a hashref of credentials (`username`, `password`,
    `serveraddress`, or `identitytoken`) which is JSON-encoded and
    base64url-wrapped per the Docker Engine spec. Without `auth` the
    header carries an empty JSON object so unauthenticated/public

MANIFEST  view on Meta::CPAN

# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.037
.claude/agents/api-docker-doc-writer.md
.claude/agents/api-docker-engine-worker.md
.claude/agents/api-docker-release-checker.md
.claude/agents/api-docker-test-writer.md
.claude/agents/api-docker-type-writer.md
.claude/agents/api-docker-worker.md
.claude/rules/api-docker-rules.md
.claude/settings.json
.claude/skills/api-docker-core/SKILL.md
.claude/skills/api-docker-type-model/SKILL.md
.claude/skills/api-docker-type-model/references/dsl.md
.claude/skills/api-docker-type-model/references/types.md
.claude/skills/docker-engine-api/SKILL.md
.claude/skills/getty-git-commit-style/SKILL.md
.claude/skills/getty-perl-core/SKILL.md
.claude/skills/getty-perl-moo/SKILL.md
.claude/skills/getty-perl-release-author-getty/SKILL.md
.claude/skills/kanban-issues-karr-cli/SKILL.md

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

  my $version_info = $self->_request('GET', '/version',
    exists $opts{read_timeout} ? ( read_timeout => $opts{read_timeout} ) : (),
    exists $opts{connect_timeout} ? ( connect_timeout => $opts{connect_timeout} ) : (),
  );

  # The ApiVersion is put straight into every later request path (/v1.44/...),
  # so it has to be a JSON object carrying one of the form N.N -- nothing else
  # can be trusted there. Three ways a body fails that, each measured against a
  # fake daemon: a non-object body reached strict refs ('garbage' died with
  # "Can't use string as a HASH ref", [1] with "Not a HASH reference"); an
  # object with no ApiVersion set _version_negotiated and then sent every
  # request unversioned; and an ApiVersion copied verbatim let 'v1.44/../x'
  # become "GET /vv1.44/../x/info". One croak, naming the endpoint and the
  # shape, covers all of them.
  my $got;
  if (!defined $version_info) {
    $got = 'nothing';
  }
  elsif (ref $version_info ne 'HASH') {
    $got = ref $version_info ? 'a ' . ref($version_info) . ' reference'
      : "the non-object body '" . $version_info . "'";

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

  }

  croak __PACKAGE__ . '->negotiate_version: GET /version must answer with a '
    . 'JSON object carrying an ApiVersion of the form N.N (e.g. "1.44"); got '
    . $got
    unless ref $version_info eq 'HASH'
      && defined $version_info->{ApiVersion}
      && !ref $version_info->{ApiVersion}
      && $version_info->{ApiVersion} =~ /^\d+\.\d+$/;

  $self->_set_api_version($version_info->{ApiVersion});
  $log->debugf("Negotiated API version: %s", $version_info->{ApiVersion});
  $self->_version_negotiated(1);
}


around _request => sub {
  my ($orig, $self, $method, $path, %opts) = @_;

  # Auto-negotiate before any versioned request, but not for /version itself.
  # The triggering request's own bounds are handed to it: the negotiation is a

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

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

This attribute is set automatically by L</negotiate_version>.

=head2 tls

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
C<< API::Docker->new >> croak on every machine talking to a local socket, since
C<< tls => 1 >> on a non-C<tcp://> host is a construction error (below).

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.

C<< tls_insecure => 1 >> sets C<SSL_VERIFY_NONE> and drops the hostname check,
which leaves a connection encrypted against a passive listener and against
nothing else: whoever answers it chooses the certificate, so anyone able to
redirect the connection reads and rewrites everything on it -- registry
credentials, image contents, the commands containers are started with.

It exists for a self-signed daemon certificate whose CA is not to hand. The
better answer is nearly always L</cert_path>: a self-signed certificate is its
own CA and works as F<ca.pem> directly.

Setting it without L</tls> croaks, rather than being accepted and doing

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

C<list>, C<privileges>, C<install>, C<inspect>, C<remove>, C<enable>,
C<disable>, C<upgrade>, C<push> and C<configure>.

=head2 negotiate_version

    $docker->negotiate_version;
    $docker->negotiate_version(read_timeout => 5, connect_timeout => 2);

Automatically negotiate the highest API version supported by the Docker daemon.
This is called automatically before the first API request if L</api_version>
is not set.

After negotiation, L</api_version> will contain the negotiated version
(e.g., C<1.41>).

C<GET /version> must answer with a JSON object carrying an C<ApiVersion> of
the form C<N.N> -- the value is placed directly into the path of every later
request (C</v1.44/...>). A body that is not such an object croaks, naming the
endpoint and the shape expected: a non-object body, an object with no
C<ApiVersion>, or an C<ApiVersion> that is not two dot-separated numbers. This
replaces three earlier failures on the same path -- a non-object body dying in

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

request -- it inherits that request's own bounds instead; see
L</"What a timeout covers">.

=head1 TIMEOUTS

=head2 What a timeout covers

Two bounds, covering different halves of a request.
L<API::Docker::Role::HTTP/connect_timeout> bounds opening the connection;
L<API::Docker::Role::HTTP/read_timeout> bounds reading the answer. Both are
attributes of the client, and they are set in two places -- which are two
levels, not two spellings of one thing:

    # the rule, for this client
    my $docker = API::Docker->new(connect_timeout => 2, read_timeout => 30);

    # 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

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


=item * B<Neither of them bounds writing the request.> Sending the bytes out
is unbounded on every transport. In practice that matters for one thing: a
large C</build> context or C<< images->load >> archive being written to a
daemon that has stopped reading.

=item * B<Under TLS, C<read_timeout> is not quite an idle timer on the
plaintext.> It is C<SO_RCVTIMEO> on the socket, which bounds each blocking
receive on the underlying connection, 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. C<connect_timeout> over TLS bounds
the TCP connect and not the handshake that follows it.

=back

An expiry croaks with an L<API::Docker::Error::Timeout> carrying what did
arrive; it never returns a truncated response.
L<API::Docker::Role::HTTP/"Bounding a request that never ends"> and
L<API::Docker::Role::HTTP/"Bounding the connection itself"> have the
per-transport measurements behind all of this.

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

=head2 Socket discovery

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

An earlier pass measured every path here as a flat 404 against Podman 5.4.2
(API 1.41). That measurement is not reproducible on this machine any more --
5.4.2 is gone from it -- so whether 5.8.4 actually changed this or the
original pass only ever exercised the collection endpoint is not something
this distribution can decide from here; it is recorded as what 5.8.4 answers,
not as a change from 5.4.2. Either way, the split this section used to draw
between the two engines -- Docker's 503 "not a swarm manager" against a flat
Podman 404 -- no longer holds cleanly: most C</configs> paths on Podman answer
503 too now, just with a different body and for a different reason.

There is no Podman-side equivalent to fall back on and no socket setting that
enables it. This remains the one class in the distribution that cannot be
exercised for real against the engine the rest of it is tested on --
L<API::Docker::API::Secrets>, the same five endpoints, is served by Podman
from its own local secret store.

=head2 client

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

=head2 list

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

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


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

# It fails open on anything it does not recognise: a State it cannot read is
# not evidence that the container is stopped, and a guard that is unsure must
# not be the thing that breaks a working call.
sub _assert_container_running {
  my ($self, $id) = @_;

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

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

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

  my ($self, $id, %opts) = @_;
  croak "Container ID required" unless $id;

  # Pre-flight, and deliberately before the request is built: the call itself
  # is what destroys the exit status, so a check made afterwards could only
  # report the loss rather than prevent it. Turned off it costs nothing at all,
  # not even the round trip.
  my $require_running
    = defined $opts{require_running} ? $opts{require_running} : 1;
  # The pre-flight is a request the caller never wrote, and one that hangs is
  # exactly what a bound was set to prevent -- so it carries the same one. It
  # does that by itself here: the check runs on $self, which is the clone
  # ->using returned when there was one (karr k74).
  $self->_assert_container_running($id) if $require_running;

  my %params;
  $params{stream} = $opts{stream} ? 1 : 0;
  $params{logs}   = defined $opts{logs}   ? ($opts{logs}   ? 1 : 0) : 1;
  $params{stdout} = defined $opts{stdout} ? ($opts{stdout} ? 1 : 0) : 1;
  $params{stderr} = defined $opts{stderr} ? ($opts{stderr} ? 1 : 0) : 1;
  $params{stdin}  = $opts{stdin} ? 1 : 0 if defined $opts{stdin};

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

L<API::Docker::Role::Entity::Container/is_running> reads whichever it is
given. C<< ->status >> -- the human sentence, C<"Up 2 hours"> -- is on the
summary only.

=item * C<< ->command >> is the whole command as one string, on a summary
only. An inspect splits it into C<< ->path >> and C<< ->args >> and keeps
the original C<Cmd> ArrayRef under C<< ->config->cmd >>.

=item * C<< ->labels >> and C<< ->ports >> are top-level on a summary only.
An inspect carries the labels under C<< ->config->labels >> and the port
bindings under C<< ->network_settings->ports >>, which is a map of container
port to host bindings rather than the summary's ArrayRef of
L<API::Docker::Type::Port>.

=item * C<< ->names >> (an ArrayRef, each with a leading C</>) is the
summary's; C<< ->name >> (one string, also with the C</>) is the inspect's.

=item * C<< ->config >>, C<< ->restart_count >>, C<< ->driver >>,
C<< ->platform >>, C<< ->graph_driver >>, C<< ->exec_ids >> and the
C<*_path> fields come from an inspect only.

=item * C<< ->host_config >> and C<< ->network_settings >> exist on both and
are B<different classes>: the summary's are
L<API::Docker::Type::ContainerSummary::HostConfig> (C<NetworkMode> and
C<Annotations>, nothing else) and
L<API::Docker::Type::ContainerSummary::NetworkSettings> (C<Networks> alone),
against the full L<API::Docker::Type::HostConfig> and
L<API::Docker::Type::NetworkSettings> on an inspect.

=item * C<< ->size_rw >> and C<< ->size_root_fs >> are on both, but a
summary only carries them when C<< size => 1 >> was asked for.

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

=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
output is binary, to skip demultiplexing. Not needed for text output. The
container's own setting is C<Config.Tty> from C<< $containers->inspect($id) >>.
With C<on_frame> it is a declaration rather than a hint; see below

=item * C<on_frame> - CodeRef called with each frame as it arrives, instead of
the ArrayRef being collected and returned; see below

=back

=head2 Following the log

C<< follow => 1 >> asks the daemon to keep sending as the container writes.

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

B<Attaching to a container that has already exited loses its exit code on
Podman, and nothing reports it.> Measured before and after a single attach
against one container that exited with 4, on Podman 5.4.2 (API 1.41) and
Docker 29.7.2 (API 1.55), same machine:

    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

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

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

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

}


sub start {
  my ($self, $exec_id, %opts) = @_;
  croak "Exec ID required" unless $exec_id;
  my $body = {
    Detach => $opts{Detach} ? \1 : \0,
    Tty    => $opts{Tty}    ? \1 : \0,
  };
  # exists, not truth: an unset callback is a caller bug, and falling back to
  # the buffered path for it would answer a long-running command by waiting
  # for it in silence. Handed over as it is, the transport says so instead.
  return $self->client->stream_frames('POST', "/exec/$exec_id/start",
    body => $body,
    $opts{Tty} ? ( tty => 1 ) : (),
    %{ $self->_request_options },
    exists $opts{on_frame} ? ( on_frame => $opts{on_frame} ) : (),
  );
}

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

  $params{dockerfile} = $opts{dockerfile} if defined $opts{dockerfile};
  $params{t}          = $opts{t}          if defined $opts{t};
  $params{q}          = $opts{q} ? 1 : 0  if defined $opts{q};
  $params{nocache}    = $opts{nocache} ? 1 : 0 if defined $opts{nocache};
  $params{pull}       = $opts{pull}       if defined $opts{pull};
  $params{rm}         = defined $opts{rm} ? ($opts{rm} ? 1 : 0) : 1;
  $params{forcerm}    = $opts{forcerm} ? 1 : 0 if defined $opts{forcerm};
  $params{memory}     = $opts{memory}     if defined $opts{memory};
  $params{memswap}    = $opts{memswap}    if defined $opts{memswap};
  $params{cpushares}  = $opts{cpushares}  if defined $opts{cpushares};
  $params{cpusetcpus} = $opts{cpusetcpus} if defined $opts{cpusetcpus};
  $params{cpuperiod}  = $opts{cpuperiod}  if defined $opts{cpuperiod};
  $params{cpuquota}   = $opts{cpuquota}   if defined $opts{cpuquota};
  $params{shmsize}    = $opts{shmsize}    if defined $opts{shmsize};
  $params{networkmode} = $opts{networkmode} if defined $opts{networkmode};
  $params{platform}   = $opts{platform}   if defined $opts{platform};
  $params{target}     = $opts{target}     if defined $opts{target};

  $params{buildargs} = encode_json($opts{buildargs}) if $opts{buildargs};
  $params{labels}    = encode_json($opts{labels})    if $opts{labels};

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


  # A build's registry credentials ride in X-Registry-Config, not
  # X-Registry-Auth: the map lets `FROM private.registry/...` authenticate,
  # and a build may draw base images from several registries at once. Sent
  # only when given -- an anonymous build needs no header.
  my %headers;
  $headers{'X-Registry-Config'} =
    $self->_registry_config_header($opts{registry_config})
    if defined $opts{registry_config};

  # exists, not truth: an unset callback is a caller bug, and falling back to
  # the buffered path for it would hand a long build back as silence.
  return $self->client->_request('POST', '/build',
    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 ),
  );
}

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

=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

=item * C<cpushares> - CPU shares (relative weight)

=item * C<cpusetcpus> - CPUs to use (e.g. C<0-3>, C<0,1>)

=item * C<cpuperiod> - CPU CFS period (microseconds)

=item * C<cpuquota> - CPU CFS quota (microseconds)

=item * C<shmsize> - Size of /dev/shm in bytes

=item * C<networkmode> - Network mode during build

=item * C<platform> - Platform (e.g. C<linux/amd64>)

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

B<The return value is raw bytes, not a decoded structure.> The engine answers
with the tar stream itself, and the transport is told to hand it back
untouched (C<< raw => 1 >>), so what arrives is byte for byte what the daemon
wrote. Write it with a binary-safe file handle -- C<< path(...)->spew_raw >>,
or C<binmode> on a handle of your own. Treating it as text corrupts it, and
nothing about the value announces that it is binary.

The archive holds one tarball per layer, a config JSON per image,
C<manifest.json> and C<repositories>. Measured against Podman 5.4.2 (API
1.41): the response is chunked with
C<< Content-Type: application/octet; charset=us-ascii >>, where Docker sends
C<application/x-tar> -- the transport looks at neither, so the difference does
not reach the caller. Exporting C<alpine:3> through this method produced bytes
md5-identical to what C<curl --unix-socket> wrote for the same request, all
8705536 of them, so the chunked reader is binary-clean.

An unknown image croaks. On the same engine that is C<404 Not Found> with
C<< {"message":"failed to find image ...: image not known"} >>.

=head2 Exporting without buffering the archive

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

=item * C<on_event> - CodeRef called with each progress event as it arrives,
instead of the ArrayRef being collected and returned. The return value is then
the summary HashRef and a stream failure croaks one event in, exactly as for
L</build>; see L</"Progress as it arrives">

=back

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

A failed load croaks, but by which route depends on the engine, the same split
L</pull> and L</push> have. Docker reports it as an C<errorDetail> object
inside a 200 stream, which croaks with an L<API::Docker::Error::Stream>
carrying the events. Podman reports it in the status line instead: measured

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


sub install {
  my ($self, $remote, %opts) = @_;
  croak __PACKAGE__ . '->install remote reference required' unless $remote;

  my $privileges = $self->_privileges_body('install', $remote, %opts);

  my %params = ( remote => $remote );
  $params{name} = $opts{name} if defined $opts{name};

  # exists, not truth: an unset callback is a caller bug, and falling back to
  # the buffered path for it would hand a long pull back as silence.
  return $self->client->post('/plugins/pull', $privileges,
    params => \%params,
    $self->_auth_headers(\%opts),
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $opts{on_event} ) : ( ndjson => 1 ),
  );
}


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

  croak __PACKAGE__ . '->push plugin name required' unless $name;
  return $self->client->post("/plugins/$name/push", undef,
    $self->_auth_headers(\%opts),
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $opts{on_event} ) : ( ndjson => 1 ),
  );
}


sub configure {
  my ($self, $name, @settings) = @_;
  croak __PACKAGE__ . '->configure plugin name required' unless $name;

  # One ArrayRef or a plain list, and nothing after either: this method reads
  # no options at all. The ArrayRef form used to be where the transport bounds
  # went, because a trailing `read_timeout => 2` in the plain list would be two
  # more settings as far as this method can tell -- they now go on the resource
  # class instead (karr k74), and what is left is a form, not a split.
  if (ref $settings[0] eq 'ARRAY') {
    my $list = shift @settings;
    croak __PACKAGE__ . '->configure takes nothing after the ArrayRef of '
      . 'settings; a transport bound goes on the resource class, as '
      . '$docker->plugins->using(read_timeout => 5)->configure(...)'
      if @settings;
    @settings = @$list;
  }

  croak __PACKAGE__ . '->configure requires at least one setting, as an '
    . 'ArrayRef or a list of "KEY=value" strings' unless @settings;

  croak __PACKAGE__ . '->configure settings must be plain strings'
    if grep { ref $_ } @settings;

  return $self->client->post("/plugins/$name/set", \@settings,
    %{ $self->_request_options },
  );
}



1;

__END__

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


L</list> and L</inspect> return L<API::Docker::Type::Plugin> objects carrying
the convenience methods of L<API::Docker::Role::Entity::Plugin>, following
the C<list>/C<inspect> convention every other resource class here follows.
It is B<one> class for both, where containers and images have two: the
swagger answers C<GET /plugins> with an array of the C<Plugin> definition and
C<GET /plugins/{name}/json> with that same definition.

Field names are the swagger's own spelling in snake_case, and the nested
ones are generated classes rather than the raw HashRefs the old entity kept:
C<< $plugin->settings >> is an L<API::Docker::Type::Plugin::Settings> whose
C<< ->env >> is a list of C<KEY=value> strings, and C<< $plugin->config >> an
L<API::Docker::Type::Plugin::Config> whose C<< ->env >> is a list of
L<API::Docker::Type::PluginEnv> objects describing those same variables. The
entity's methods thread the plugin's name back through this class.

Everything else returns the decoded engine response as it came: L</privileges>
an ArrayRef of privilege HashRefs, L</install>, L</upgrade> and L</push> an
ArrayRef of progress events, and L</enable>, L</disable>, L</remove> and
L</configure> C<undef>.

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


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
plugin, and know that it reads as consent to whatever the plugin demands

=item * C<name> - Local name for the installed plugin, if it should differ
from C<remote>. A digest is not allowed here

=item * C<auth> - Registry credentials, as for L</privileges>

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

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

=head2 inspect

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

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

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

=head2 remove

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

Remove an installed plugin. A plugin that is still enabled is refused unless
C<force> is set.

Options:

=over

=item * C<force> - Disable the plugin before removing it. Removing a plugin
that containers are still using will break them

=back

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

Like L</install> this carries the privilege list in its body and the daemon
checks it against what the new version demands, so C<privileges> is required
here too -- an upgrade is where a plugin's demands can B<change>, which is
the case worth looking at.

Options:

=over

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

=item * C<accept_privileges> - Fetch the privileges for C<remote> and grant
them, in one call

=item * C<remote> - Remote reference to upgrade to. Defaults to C<$name>,
which is what you want unless the plugin was installed under a local name

=item * C<auth> - Registry credentials, as for L</privileges>

=item * C<on_event> - CodeRef called with each progress event as it arrives.

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

progress. Failure is reported by the same two routes as L</install>.

C<push> shadows the Perl builtin inside this package, which is why
L<namespace::clean> is loaded. Always call it as a method.

=head2 configure

    $plugins->configure('vieux/sshfs:latest', ['DEBUG=1']);
    $plugins->configure('vieux/sshfs:latest', 'DEBUG=1', 'sshkey.source=/tmp');

Set a plugin's user-configurable settings (C<< POST /plugins/{name}/set >>).
The plugin must be disabled. Returns C<undef>.

Settings are C<KEY=value> strings, given either as one ArrayRef or as a plain
list. They name the mutable fields of the plugin's config -- the environment
variables, mount sources, devices and args that C<< $plugin->settings >>
reports; L</inspect> is how you find out which ones a given plugin has.

The engine replaces nothing it is not told about, and rejects a key the
plugin's config does not declare as mutable.

    $plugins->configure('vieux/sshfs:latest', ['DEBUG=1']);
    $plugins->configure('vieux/sshfs:latest', 'DEBUG=1');

Both forms mean the same call, and this method takes no options in either:
anything after the ArrayRef croaks rather than being read as a setting or
quietly dropped. To bound the request, clone the resource class --
C<< $docker->plugins->using(read_timeout => 5)->configure(...) >>, see
L<API::Docker::Role::Using>.

=head1 SEE ALSO

=over

=item * L<API::Docker::Role::Entity::Plugin> - the convenience methods the
returned objects carry

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

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

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


    my $login = $system->auth(
        username      => 'me',
        password      => 'secret',
        serveraddress => 'ghcr.io',
    );

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

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

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

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

            && $err->isa('API::Docker::Error::Timeout');
        # Every complete frame reached the callback before the timeout; the
        # summary says how many.
        warn 'stopped after ' . $err->summary->{delivered} . ' frames';
    }

=head1 DESCRIPTION

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

The rest of this describes the read timeout, which is the one that has
something to hand back. A connect timeout carries no L</partial> and no
L</summary>, for the reason L</phase> gives: nothing was ever sent.

It is an B<idle> timeout, not a deadline: the clock is the time since the last
byte arrived, so a stream that keeps producing runs as long as it likes and one
that stalls is cut off. That is the distinction the endpoints this exists for

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


=head2 decoded_data

    my $text = $config->decoded_data;

The config's content: C<< $config->spec->data >> run through
L<MIME::Base64/decode_base64>. Returns nothing when the object carries no
C<Spec> or no C<Data> in it.

The result is B<raw bytes>, symmetric with what
L<API::Docker::API::Configs/create> takes -- decode the character set yourself
if the config holds text above C<U+007F>, for instance with
C<Encode::decode_utf8>.

The spec is left alone; see
L</"Decoding is offered here, not in the API class">.

=head2 version_index

    my $index = $config->version_index;

The C<< ->index >> out of C<< ->version >>, which is what the daemon wants as
the C<version> query parameter on an update. Returns nothing when the object
carries no C<Version> -- including the case where the daemon sent one the
model could not use, which leaves the attribute unset and the raw value in
C<< ->unknown_fields->{Version} >>.

It is the version as of the moment this object was fetched, which is exactly
the token's meaning: an update built on a stale entity is refused by the
daemon rather than silently overwriting whatever changed in between.

=head2 inspect

    my $fresh = $config->inspect;

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

}


sub remove {
  my ($self, %opts) = @_;
  return $self->client->plugins->remove($self->name, %opts);
}


sub configure {
  my ($self, @settings) = @_;
  return $self->client->plugins->configure($self->name, @settings);
}


sub upgrade {
  my ($self, %opts) = @_;
  return $self->client->plugins->upgrade($self->name, %opts);
}


sub push {

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

#
# Here rather than in API::Docker::API::Plugins, for the reason spelled out in
# API::Docker::Role::Entity::Container: loading this role is what puts the
# methods on the class.
#
# The clash check is not decoration. Moo composes a role into a class the
# class-wins way, so a generated accessor of the same name as a method here
# would silently keep its place and the method would be missing -- and the
# generated classes are written from a specification that grows fields
# without asking. This is the resource where the two vocabularies come
# closest: the class already declares `config`, `enabled` and `settings`
# beside this role's `configure` and `enable`. None of the seven names
# collides today; a future one says so on the first `use`.
{
  my @provided = Package::Stash->new(__PACKAGE__)->list_all_symbols('CODE');
  for my $class ('API::Docker::Type::Plugin') {
    my $fields = $class->docker_attributes;
    my @clash = sort grep { $fields->{$_} } @provided;
    croak __PACKAGE__ . ': ' . $class . ' declares ' . join(', ', @clash)
      . ' as a daemon field; the generated accessor would win over the '
      . 'method of that name and it would be missing without a word'

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


version 0.004

=head1 SYNOPSIS

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

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

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

=head1 DESCRIPTION

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

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

Every method here threads C<< ->name >> through to the method of the same
name on L<API::Docker::API::Plugins>, so the options, the return values and
the failure modes are that class's -- documented there, not repeated here.
The class does carry an C<< ->id >>, but the endpoints route on the name,
which is why this role C<requires 'name'>.

C<< ->name >> is the plugin as it is B<installed locally> --
C<vieux/sshfs:latest>, or whatever local name
L<API::Docker::API::Plugins/install> was given. The remote it came from is
C<< ->plugin_reference >> (C<docker.io/vieux/sshfs:latest>), which the engine
sets on the pull, upgrade and create paths only and omits entirely otherwise
rather than sending it as null -- so it reads as C<undef> for a plugin that
never came from a registry, and differs from the name outright for one
installed under a local one. That is exactly the case where L</upgrade>
needs C<remote> spelled out.

=head2 Two shapes of Env, one level apart

C<< $plugin->settings->env >> is a list of C<KEY=value> B<strings>, which is
what L</configure> takes. C<< $plugin->config->env >> is a list of
L<API::Docker::Type::PluginEnv> objects describing those same variables --
same field name, two shapes. The daemon flattens the one into the other when
the plugin is installed. L</configure> writes to the settings, never to the
config.

=head2 Not available on Podman

Managed plugins are a Docker feature: none of these endpoints exist on
Podman, so nothing in this role works against it. See
L<API::Docker::API::Plugins/"Not available on Podman">.

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

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


    $plugin->remove(force => 1);

Remove the plugin. An enabled plugin is refused without C<force>.

=head2 configure

    $plugin->configure(['DEBUG=1']);
    $plugin->configure('DEBUG=1', 'sshkey.source=/tmp');

Set the plugin's user-configurable settings. The plugin must be disabled
first. The settings are the C<KEY=value> strings of
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

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

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

=head2 version_index

    my $index = $secret->version_index;

The C<< ->index >> out of C<< ->version >>, which is what the daemon wants as
the C<version> query parameter on an update. Returns nothing when the object
carries no C<Version> -- including the case where the daemon sent one the
model could not use, which leaves the attribute unset and the raw value in
C<< ->unknown_fields->{Version} >>.

It is the version as of the moment this object was fetched, which is exactly
the token's meaning: an update built on a stale entity is refused by the
daemon rather than silently overwriting whatever changed in between.

=head2 inspect

    my $fresh = $secret->inspect;

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

requires 'host';
requires 'api_version';
requires 'tls';
requires 'cert_path';
requires 'tls_insecure';

# Docker stream frame types, indexed by the first byte of the frame header.
my @STREAM_TYPE = qw( stdin stdout stderr );

# A field name is an RFC 9110 token and nothing else. Anything outside this
# set -- CR, LF, a space, a colon -- is rejected rather than stripped; see
# _assert_header_name.
my $HEADER_NAME = qr/\A[0-9A-Za-z!#\$%&'*+.^_`|~-]+\z/;

# The request-target path is caller data -- a container name, an image
# reference -- spliced straight into the request line as /v$version$path, so a
# byte the line's own grammar reads rewrites the request rather than naming a
# resource: CR or LF ends the line, a space opens the HTTP-version field, and
# a ? or # opens the query or fragment. It is held to the RFC 3986 origin-form
# path character set -- unreserved, the sub-delims, and : @ % / -- and
# rejected, not sanitised, for the reason a header name is (see
# _assert_request_path). Query parameters carry the ? and everything after it
# and are assembled separately below, each element run through _uri_encode.
my $REQUEST_PATH = qr{\A[A-Za-z0-9\-._~:/\@!\$&'()*+,;=%]*\z};

# The three units a response can be cut into, one option each. A request picks
# one of them, or none and gets the buffered path; see _stream_handler.
my @STREAM_OPTION = qw( on_event on_frame on_chunk );

# What a response body has to start with to be worth handing to decode_json.

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



has _socket => (
  is      => 'lazy',
  clearer => '_clear_socket',
);

# The connect timeout and the endpoint it belongs to, on their way to
# _build__socket. It is a lazy builder and so cannot be handed an argument,
# and the value is per request rather than per client -- hence rw, with
# _reconnect as the only writer, setting it immediately before the build and
# clearing it immediately after. Unset is the whole of the old behaviour: no
# Timeout on any constructor, and the plain croak on a failure.
has _pending_connect => (
  is       => 'rw',
  init_arg => undef,
);

sub _build__socket {
  my ($self) = @_;
  my $host    = $self->host;
  my $pending = $self->_pending_connect;

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

  return $sock;
}

# Whether the connect that has just failed failed because the bound fired.
# Asked with nothing in between, because $@ and $! are the whole of the
# evidence and both are global.
#
# $@ rather than errno: IO::Socket writes 'connect: timeout' there, and only
# there, when its own select() ran out -- measured, against a host that drops
# SYNs, where $! is ETIMEDOUT, which the kernel also produces on its own after
# two minutes with no Timeout set at all.
#
# EAGAIN is the second shape and belongs to unix:// alone. Measured against a
# listener whose backlog is full: with no Timeout the connect blocks
# indefinitely (still blocked after 8s), and with one it fails at once with
# EAGAIN, because IO::Socket does the timed connect non-blocking and an
# AF_UNIX connect has no in-progress state to wait on. So on that transport
# the option does not wait, it refuses -- but a connect that failed with
# EAGAIN is still one the bound ended, and reporting it as anything else would
# name a cause the caller cannot act on.
sub _connect_expired {

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

}

# Why SO_RCVTIMEO and not select(): a bound that reads the socket cannot see
# what is already buffered above it, and would fire while the data it was
# waiting for was in hand. That was true of PerlIO's read-ahead when this was
# written (measured: after one readline of a socket holding
# "one\ntwo\nthree\n", two whole lines sit in the PerlIO buffer and select()
# says the handle is not ready), and it is true of _read_buffer now. A
# select-based bound would have to be asked only when that buffer is empty,
# which is one more invariant to keep for no gain: SO_RCVTIMEO bounds the one
# syscall in _pull for one setsockopt, and gets idle-since-the-last-byte
# semantics for free, which is the semantics these endpoints need (karr k52:
# the buffered frames arrive, and *then* the socket stalls -- a
# time-to-first-byte bound would never fire).
sub _apply_read_timeout {
  my ($self, $sock, $timeout) = @_;

  return unless $timeout;

  my $packed;
  if ($^O eq 'MSWin32' || $^O eq 'cygwin') {

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

    # Linux x86_64 against unix://, plain tcp:// and TLS.
    my $sec  = int($timeout);
    my $usec = int(($timeout - $sec) * 1_000_000 + 0.5);
    if ($usec >= 1_000_000) { $sec++; $usec -= 1_000_000 }
    $packed = pack('l!l!', $sec, $usec);
  }

  # Never a warning and never a silent pass. A caller that asked for a bound
  # and did not get one is left waiting on exactly the hang the option exists
  # to end, and would have no way to tell that from a daemon being slow.
  setsockopt($sock, SOL_SOCKET, SO_RCVTIMEO, $packed)
    or croak __PACKAGE__ . ': cannot set a read timeout of ' . $timeout
      . 's on this socket: ' . $! . '. Refusing to continue without it -- a '
      . 'bound that is not in force is worse than no bound at all, because '
      . 'the caller is relying on it';

  return;
}

# A read that did not deliver did not deliver for one of two reasons, and they
# are not the same thing: the stream ended, or the clock ran out. errno is the
# only thing that separates them -- measured, both eof() and $fh->error are
# true after a timeout just as they are at a clean end, and asking eof() costs
# a second full timeout. So $! is zeroed immediately before the read in _pull
# and captured immediately after it, with nothing in between: it is only
# meaningful after a failure, and any operation in between would overwrite it.
#
# Without this the readers would take a timeout for the end of the response and
# return a truncated body as a whole one. That is the reason karr k59 is not
# just the setsockopt: switching the option on alone would turn a hang into
# silent data loss, which is the worse of the two.
sub _timed_out {
  my ($self, $ctx, $errno) = @_;

  return 0 unless $ctx->{timeout};
  return ($errno == EAGAIN || $errno == EWOULDBLOCK) ? 1 : 0;
}

sub _croak_timeout {
  my ($self, $ctx, $partial) = @_;

  $partial = '' unless defined $partial;
  # Only ever set once a stream is past its status line, so an error body read
  # whole on the way to a >= 400 croak is still reported in bytes.
  my $summary = $ctx->{summary} ? $ctx->{summary}->() : undef;

  my $after = $summary
    ? ' after ' . $summary->{delivered} . ' unit'
      . ($summary->{delivered} == 1 ? '' : 's')
    : length($partial)
      ? ' after ' . length($partial) . ' byte'
        . (length($partial) == 1 ? '' : 's')
      : ', nothing arrived at all';

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

# the status line and the headers with <$sock>, and PerlIO reads ahead. The
# bytes past the header block were sitting in a buffer this code cannot reach
# -- there is no supported way to take them back out; ungetc is layer-
# dependent, seek does not work on a socket, and select/MSG_PEEK see the
# kernel's buffer rather than PerlIO's. So switching only the body reads to
# sysread would have silently dropped the start of every body. Either all read
# sites move together or none do.
#
# The buffer lives on the handle rather than on the client or in the context:
# it is the unconsumed bytes of *that* handle, its lifetime is the handle's,
# and a client that opens a socket per request therefore has nothing to reset.
# ${*$sock}{...} is the IO::Socket idiom for exactly this and was measured to
# work on a real socket, a lexical filehandle, a bareword glob and a tied
# handle alike.
my $RBUF = __PACKAGE__ . '/rbuf';

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

  ${*$sock}{$RBUF} = '' unless defined ${*$sock}{$RBUF};
  return \${*$sock}{$RBUF};

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

      return 'data';
    }

    # A signal is not an answer. perl's read() retried here of its own accord
    # (PerlIOUnix_read loops while errno is EINTR), so retrying keeps the
    # behaviour this replaces rather than introducing one.
    next if $errno == EINTR;

    return 'timeout' if $self->_timed_out($ctx, $errno);

    # Anything else -- a reset connection, a handle that cannot be read at
    # all -- ends the response, which is what it did before this too: every
    # reader answered a failed read with `last unless $n`. Whether the
    # response was complete when it ended is a question about its structure,
    # and is asked by the readers that know the structure.
    return 'eof';
  }
}

# The two reads every reader below is built out of. Both serve from the buffer
# and pull only when it is empty, so both hand back what has arrived rather

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


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
files on disk are for, and treating the absence of a client certificate as a
missing precondition would conflate the two. The deployment with no
certificate files is real, and is the one this role's documentation used to
recommend before there was any TLS here: a terminator -- nginx, stunnel,
Traefik -- in front of the daemon, holding a publicly trusted certificate.
There is nothing for a C<cert_path> to point at in that setup.

It is also the safe reading rather than the lax one: verification stays on
either way, so the mode reached by configuring nothing is the verifying mode.
A stock C<dockerd --tlsverify> uses a private CA that the system store does
not have, and such a connection fails with a verification error naming exactly
that -- which is the intended outcome, not a silent downgrade. Point
C<cert_path> at the directory holding its F<ca.pem> and it verifies.

=head3 Turning verification off

C<< tls_insecure => 1 >>, and the name is the whole of the warning. It sets
C<SSL_VERIFY_NONE> and switches the hostname check off, which leaves a
connection that is encrypted against a passive listener and against nothing
else: whoever answers chooses the certificate, so anyone able to redirect the
connection reads and rewrites everything on it -- registry credentials,
image contents, the commands containers are started with.

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

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


    # Give up after two seconds of silence rather than waiting forever.
    my $frames = $docker->containers->using(read_timeout => 2)->attach($id);

=head3 It is an idle timeout, not a deadline

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

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

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

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


=head3 What it does not cover

Only reading. Connecting is bounded separately by L</connect_timeout>, and
writing the request is not bounded at all -- which matters only for a large
C</build> context sent to a daemon that has stopped reading.

It is implemented with C<SO_RCVTIMEO> on the socket, which was measured to
behave the same over C<unix://>, plain C<tcp://> and TLS: the timeout fires,
the handle is not left unusable, and reading afterwards works. The C<struct
timeval> it is set with was measured on Linux; on Windows the millisecond
C<DWORD> Winsock documents is sent instead, which is reasoned rather than
measured. A platform that rejects either croaks rather than continuing without
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

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

=over

=item * C<tcp://> -- a real bound. Against a host that drops SYNs, an unbounded
connect waits for the kernel's own timeout, which on Linux is over two
minutes; C<< connect_timeout => 2 >> gave up after 2.00s. This is the case the
option exists for.

=item * C<unix://> -- a bound, but it does not wait. A connect to a Unix socket
whose listen backlog is full blocks: measured against a listener with
C<< Listen => 1 >> and nobody accepting, still blocked after 8 seconds. With a
C<connect_timeout> set it fails at once instead, with C<EAGAIN> -- because
C<IO::Socket> performs a timed connect non-blocking, and an C<AF_UNIX> connect
has no in-progress state to wait on. So the hang is gone, at the price of not
tolerating even a momentary backlog. A socket path that does not exist is
C<ENOENT> either way and is not affected.

=item * TLS -- bounds the TCP connect only. The handshake that follows it runs
on the connected socket, before L</read_timeout>'s C<SO_RCVTIMEO> is applied,
and is not covered by either.

=back

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

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

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

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

=head2 Failure in the middle of a response

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

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

The C<$path> given to L</get>, L</post>, L</put>, L</delete_request>, L</head>
and C<_request> is spliced straight into the request line as
C<< $method /v$version$path HTTP/1.1 >>, and it carries caller data: the
resource methods build it by interpolation -- C<< "/containers/$id/json" >>,
C<< "/images/$name/push" >> -- so a container name or an image reference the
user typed ends up in the request line unescaped. A byte the line's own
grammar reads therefore rewrites the request rather than naming a resource: a
CR or LF ends the line and opens a header of its own, a space starts the
HTTP-version field, and a C<?> or C<#> opens the query string or fragment.

So the path is checked against the RFC 3986 origin-form character set --
unreserved, the sub-delims, and C<:> C<@> C<%> C<< / >>, which is the set an
image reference lives in -- and a path outside it is refused with a croak
before anything reaches the wire, the same treatment and for the same reason a
header name gets. Sanitising is not on the table here: percent-encoding the
path at this layer cannot tell a separator from data, so it would either
mangle every C<< / >> and C<:> or leave the injection open. Query parameters
belong in C<params>, which is assembled separately and runs each element
through C<_uri_encode>.

=head2 post

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

rejected outright. Measured non-mutating against Docker 29.7.2 (API 1.55),
C<< POST /containers/no-such/exec >> with C<< {"Cmd":["true"],"AttachStdout":1}
>> answers C<400> C<json: cannot unmarshal number into Go struct field ... of
type bool>; Podman 5.8.4 (compat API 1.44) answers C<500> with the same Go
message. The query string is not type-checked, which is why C<1>/C<0> is right
there and wrong here.

Every resource API that forwards a caller HashRef as a JSON body therefore
normalises its own boolean keys on the way out, the same C<\1>/C<\0> encoding
L<API::Docker::API::Exec/start> already used. Which keys are boolean is the
swagger's answer and belongs to each method (the sets are declared beside the
call); this role carries only the mechanical coercion they share.

=head2 _json_bools

    $self->_json_bools(\%body, qw( Tty OpenStdin AttachStdout ));

Coerce the named keys of C<$hash> in place to JSON booleans and return the same
HashRef. A key that is absent is left alone (so an unset option sends nothing),
and a value that is already a reference -- a C<\1>/C<\0> or a
L<JSON::PP::Boolean> -- is left as it is, which keeps the coercion idempotent
and lets a caller who already passes C<< JSON->true >> through untouched. Any
other value becomes C<\1> when true and C<\0> when false, so a caller passing
C<1>/C<0> gets a real JSON boolean on the wire.

This mutates the HashRef it is given, so a caller normalising a nested
sub-object (a C<HostConfig>, say) must hand it a copy it owns rather than the
caller's own nested HashRef.

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

C<identitytoken> and C<email>, and moves it around in three shapes:

=over

=item * base64url-encoded in the C<X-Registry-Auth> request header, for
C<< POST /images/{name}/push >>, C<< POST /images/create >>,
C<< GET /distribution/{name}/json >> and the C</plugins> family

=item * as a base64url-encoded B<map> of registry hostname to AuthConfig in
the C<X-Registry-Config> request header, for C<< POST /build >> -- one build
may pull base images from several registries, so it carries a set of
credentials rather than one

=item * as the plain JSON request body of C<< POST /auth >>

=back

This role carries the conversion in both directions so every class that
speaks to a registry agrees on it, and so a caller can hand the same C<auth>
argument to any of them.

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

# response path does not come through here -- see L</from_data>.
#
# Two keys resolving to one attribute is refused rather than decided by hash
# order, and refused whether or not the two values agree: values that happen
# to match are not the same thing as an unambiguous call, and the caller
# should see the mistake instead of the luck (karr k85).
#
# Idempotent on purpose: where one generated class extends another (the
# `allOf` shape, see API::Docker::Type) the role is composed into both, so
# this modifier runs twice on the same arguments. The second pass sees every
# key already resolved and merges an empty set into unknown_fields.
around BUILDARGS => sub {
  my ($orig, $class, @args) = @_;
  my $args = $class->$orig(@args);
  my $known = $class->_docker_attr_registry;
  my $wire  = $class->_docker_wire_index;
  my $mine  = $class->_entity_attribute_index;
  my %unknown  = %{ delete($args->{unknown_fields})  || {} };
  my %rejected = %{ delete($args->{rejected_fields}) || {} };
  my (%out, %from);
  for my $key (keys %$args) {

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

# composes such a role answers _entity_attributes with their names.

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

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

caller wrote: its keys are the caller's, so the Perl spelling is read first
and the wire spelling is an alias for it.

The split is what keeps the passthrough invariant true. Resolving a Perl name
on the response path renames the engine's data -- Docker's swagger spells 114
fields with a lowercase first letter, so a lowercase key off an engine is
ordinary rather than exotic, and reading C<id> as the Perl name of C<Id> both
loses the field it really was and rewrites one we did know (karr k85).

The same split decides what happens to a value that does not fit its declared
type. L</from_data> keeps it -- unset attribute, raw value in
L</unknown_fields>, name in L</rejected_fields> -- because one divergent field
must not make the rest of a usable response unreachable. L</new> croaks,
because there the value is the caller's and a mistake worth stopping on.

A nested hashref follows whichever entry point started the construction, so
one object graph is read one way throughout.

=head2 unknown_fields

A HashRef of everything that reached this object under a name the model could
not translate, kept under the name it arrived with and handed back out by
L</TO_JSON> unchanged. Two things land here: a name the registry does not know
at all, and -- on the response path only -- a known wire name whose value did
not fit the type the swagger declares for it. L</rejected_fields> is what
tells the two apart.

This is the whole reason a caller whose engine is newer than the swagger this
model was generated from still gets their field to the daemon. Translating
what we know and B<forwarding the rest verbatim> is worth more to this
distribution than a tidy model: a field the caller set must never be dropped
because we have not heard of it.

That promise covers the value as well as the name, C<undef> included: a name
the model does not know has no declared type, so there is no zero value we
could read a null as, and inventing one would be us deciding what the engine
meant. A B<known> field's null is the opposite case and is read as unset --
see L</"A null on a known field is read as unset">.

=head2 rejected_fields

A HashRef naming the fields this object could B<not> use, mapping the wire
name the value arrived under to the Perl attribute it would have filled.

It exists so that "the engine did not send this" and "the engine sent it and
the model could not use it" are two different observations. Both leave the
typed accessor C<undef>; only the second puts the field's wire name in here,
and the value itself in L</unknown_fields> beside it:

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

first letter is lowercase -- C<BuildInfo.id> is one -- so a lowercase key off
an engine is ordinary. Reading such a key as the Perl name of a field we do
know would rename the engine's data and lose the field it really was. Use
L</new> where both spellings should be accepted; that is where a caller, not
an engine, is the author of the keys.

=head3 A value that does not fit costs one field, not the response

Where a value disagrees with the type the swagger declares -- a C<State> that
is the bare status string rather than the object C<ContainerInspectResponse>
declares -- the field is B<not> set, the response still inflates, and the raw
value is kept in L</unknown_fields> under its wire name with the name
recorded in L</rejected_fields>:

    $c->state                       # undef
    $c->rejected_fields->{State}    # 'state'
    $c->unknown_fields->{State}     # 'exited'
    $c->TO_JSON->{State}            # 'exited', byte for byte

We are not the authority on what an engine answers. Podman announces API 1.44
and Docker 1.55 on the machine this was written on, while the model is
generated from v1.51, and one divergent field making every other field of an
otherwise usable inspect unreachable is not an improvement. The typed
accessor keeps its contract either way: if it is set, it is the declared type.

This leniency is the response path's alone. L</new> croaks on a value that
does not fit, because there the value came from the caller and is a mistake
worth stopping on rather than an engine being itself.

=head3 A null on a known field is read as unset

An engine answering C<"Tags": null> is saying what an engine that omits the
field is saying, and this reads both the same way: the attribute stays
C<undef>, nothing is filed in L</unknown_fields> or L</rejected_fields>, and
L</TO_JSON> writes no key for it. The null is not carried and the key does
not come back.

That is not a convenience, it is the daemon's own resolution. Measured
2026-08-28 against Podman 5.8.4 (API 1.44) on C<POST /containers/create>,
with an image name nothing can resolve so that the body is parsed and no

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

L</from_data> on a JSON document. The argument is a UTF-8 encoded byte
string, exactly what L</to_json> produces.

=head2 TO_JSON

    my $struct = $host_config->TO_JSON;

The structure the daemon expects: registry wire names as keys, JSON booleans
for C<Bool>, nested objects serialised by their own C<TO_JSON>.

An attribute that was never set is B<omitted>, not sent as null -- Docker
tells an absent flag apart from a false one, and so does this. A known field
the engine sent as an explicit C<null> is such an attribute, so its key does
not come back either; that is measured rather than assumed, see
L</"A null on a known field is read as unset">. The contents of
L</unknown_fields> are written first and a set field wins over them -- and a
null kept there, under a name the model could not translate, does go out as
a null.

That precedence never costs a value L</from_data> preserved. A field lands in
C<unknown_fields> under a known wire name only when its typed attribute was
B<left unset> -- that is what being rejected means -- and an unset attribute
is one this loop skips, so the two do not meet. They meet only where someone
sets the attribute afterwards, or hands C<new> a C<unknown_fields> entry
beside the field of that name; in both of those the typed value is the later
and more deliberate one, and it is the one that goes out.

=head2 to_json

    my $bytes = $host_config->to_json;

L</TO_JSON> encoded as a UTF-8 byte string, canonical so two equal objects
encode to the same bytes.



( run in 1.818 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )