API-Docker

 view release on metacpan or  search on metacpan

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

blocks with one `=item * C<name> - meaning` per accepted key — mirror the method's own
`%params`/`%opts` handling, including the defaults it applies (`rm` defaults to true in
`build`, `tag` to `latest` in `pull`).

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

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

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

`README.md` carries a short synopsis that must not contradict `lib/API/Docker.pm`.

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

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

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


- **`list` and `inspect` return generated `API::Docker::Type::*` objects**
  (via `_wrap`/`_wrap_list`), everything else returns the raw daemon
  response. Which class depends on the resource: `Networks`, `Volumes`,
  `Plugins`, `Secrets` and `Configs` answer both calls with the same class
  (one swagger definition each — `API::Docker::Type::Network`, etc.), while
  `Containers` and `Images` each have two, with different fields —
  `ContainerSummary`/`ContainerInspectResponse`,
  `ImageSummary`/`ImageInspect` (see `API::Docker::API::Containers/"The two
  container shapes"`). Which resources have one class vs. two is read off
  `spec/v1.51.yaml`, not assumed — a future swagger could add a second
  definition to a resource that only has one today.
- **The seven hand-written entity classes are gone (k84)** —
  `API::Docker::{Container,Image,Network,Volume,Plugin,Secret,Config}`. The
  files still ship, but only as stubs that croak on load and on every method
  call (k92), so installing a new release overwrites the working copy an
  older one left on disk instead of leaving it to shadow the release. Never
  write code against them or treat their POD as current; the objects the
  daemon answers with are the `API::Docker::Type::*` classes above.
- **The composed `client` on an entity is a `weak_ref`**, declared by
  `API::Docker::Role::Entity` and composed into every wrapped

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

  exactly one JSON object comes back as that hashref, not as a one-element
  array. Callers check `ref` before iterating.
- **A failed build/pull/push is still HTTP 200.** `_request` croaks on status
  >= 400 only; `errorDetail` inside the event stream is the caller's job.
- **TLS is implemented, not stubbed.** `tls => 1` on a `tcp://` connection
  (`unix://` never encrypts, and refuses the combination outright) swaps in
  `IO::Socket::SSL` in place of the plain socket — same reader, same writer,
  same everything above it. `cert_path` names a directory in the `docker`
  CLI's own layout (`ca.pem` as the trust anchor, `cert.pem`+`key.pem` as
  this client's identity), defaulting from `$ENV{DOCKER_CERT_PATH}`;
  `tls_insecure => 1` turns verification off. `IO::Socket::SSL` is a
  recommended, not required, dependency, loaded only once a TLS connection is
  actually opened. Detail: `API::Docker::Role::HTTP`'s "TLS on a tcp://
  connection".
- **No connection reuse.** Each `_request` calls `_reconnect` and closes
  afterwards, streamed or not.

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

`test_docker('GET /images/json' => $fixture_or_coderef, ...)` returns a client
whose `_request` dispatches against the route table (exact key first, then the

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

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

## The two rules that override everything else

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

**The generator only ever creates.** It writes a file that does not exist and
refuses to overwrite one that does. There is no `--force` and no bulk
refresh, and the refusal is enforced against relative paths, absolute paths,
`maint/../lib`, symlinks and subdirectories. When a newer spec lands, the
drift checker reports the difference and a human decides field by field. A
generated class is one that someone has since read, corrected and
documented; a re-run would throw that away silently.

## Where a change belongs

.claude/skills/getty-git-commit-style/SKILL.md  view on Meta::CPAN

This commit updates the Sybase driver distribution to use the new
[DBIO] Dist::Zilla plugin bundle instead of the previous [Author::GETTY]
bundle. Additionally, it fixes an issue where...
```

## Changelog entries

Where the repo carries a `Changes` or `CHANGELOG`, the entry belongs in the same commit
as the change it describes, and the rules above apply to it unchanged. One thing makes
it harder than a commit message: a message is written once and never seen again, while
the unreleased section stays open for weeks and is edited again every time the same
area is touched.

**One topic, one entry.** Before writing a bullet, read the unreleased section for the
topic you are about to describe. If it already has one, **rewrite that bullet** to say
where the code now stands — never append a second. Three bullets circling one option
are three chances to contradict each other, and the reader wants the released state,
not the sequence of attempts that produced it.

**Describe the destination, not the journey.** No "used to", no "previously", no
walkthrough of the mechanism, no defence of the alternative that lost. Name what is
true now and what a user does differently because of it. The reasoning has homes that
keep it — the commit body, the ticket, an ADR; a changelog is read by someone who
never saw the old behaviour.

**Reference only the tracker the repo publishes.** In a changelog a `#123` earns its
place when the reader can open it — GitHub or Gitea issues on a repo that has them. An
internal board ticket is unreadable outside the workspace, so it does not appear there
at all: name the change instead of the number. A commit message may carry one, written
in the board's own notation (karr ids are `k254`) — never as `#254`, which every
hosting platform resolves against *its* issue 254: a dead link today, someone else's
bug once the repo has that many.

Aim for one to three lines per entry, and let the count of entries fall out of the
work rather than the detail per entry. For scale: a mature Getty distribution carries
320 releases in a 348-line `Changes` — a whole history shorter than a single unreleased
section that was allowed to accrete.

## Multi-repo commits

When committing across multiple repos in a workspace, each repo gets its own
commit with its own message. Don't reference other repos in the message.

## HEREDOC usage

Always pass commit messages via HEREDOC to preserve formatting:

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


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

Getty-authored (non-exhaustive): `Langertha`, `IO::K8s`, `Kubernetes::REST`, `WWW::Crawl4AI`, `Net::Async::Crawl4AI`, `Net::Async::WebSearch`, `Catalyst::Plugin::ChainedURI`, `Locale::Simple`, `DBIO::*`, `WWW::Zitadel`, `WWW::PayPal`, `WWW::Chain`.

## Changelog (the Changes file)

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

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

## Forbidden

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

## When in doubt

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

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

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 {

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


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

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

- `version_finder` - multi-value; forwarded as the `finder` option of RewriteVersion::Transitional + BumpVersionAfterRelease (default path) and PkgVersion (task/manual_version path). Defaults to `:MainModule` when `no_cpan` is set, otherwise unset.

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

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

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

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

### Alien (prefix `alien_`)

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

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


### Inline Commands (→ =head2)
- `=attr name` → `=head2 name`
- `=method method_name` → `=head2 method_name`
- `=func func_name` → `=head2 func_name`
- `=opt` - CLI options
- `=env` - Environment variables
- `=hook` - Hooks
- `=example` - Examples

**Auto-generated sections (do NOT write manually):**
NAME, VERSION, AUTHOR, SUPPORT, CONTRIBUTING, COPYRIGHT

## Versioning Convention — CRITICAL

**The version in the repository is always the NEXT release version, not the current one.**

Before a release, the files already contain the upcoming version:
- `dist.ini` or module `$VERSION` = e.g. `1.005`
- `Changes` has `{{$NEXT}}` as the placeholder for unreleased changes
- The currently released version on CPAN is `1.004`

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


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

Read the `{{$NEXT}}` section of `Changes` once as a whole first. It was written bullet
by bullet over weeks, so this is where entries circling one topic get merged into one
and over-detailed ones get cut back (skill `getty-git-commit-style`). It is not only a
changelog: `GitHub::CreateRelease` runs with `notes_from = ChangeLog` and publishes the
section verbatim as the release notes.

Fix it in its own commit *before* `dzil release`. `Changes` sits in the release commit's
`allow_dirty` list, so an uncommitted edit stops nothing — it is folded into the release
snapshot silently, under a message naming only the version.

## Conventions

1. `copyright_year` IS used in dist.ini — GETTY has it in ALL distributions, do NOT remove it
2. No `=head1 SUPPORT/AUTHOR/COPYRIGHT` in POD
3. Use inline `=attr`/`=method` directly after code

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

```

Creates the board refs inside the current Git repository. With
`--claude-skill`, installs this skill to
`.claude/skills/kanban-issues-karr-cli/SKILL.md`.

Before it writes anything, init asks the remote whether this repository already
has a board there: `git clone` does not fetch `refs/karr/*`, so a fresh clone
looks exactly like a repository that never had one. A remote that advertises
`refs/karr/*` means the board exists and is one `karr sync` away, so init
refuses and says so rather than starting a second board beside it. Every other
answer -- no remote, an unreachable one, no answer inside the probe budget --
lets init through, because it has to work offline. Use `--new-board` only when
a clone is really meant to keep its own, independent board: the two will not
sync with each other, and the board-identity guard is what stops them.

### Create task

```bash
karr create "Title" [--status STATUS] [--priority PRIORITY] [--tags t1,t2] [--body TEXT]
karr create --title "Title" --assignee NAME --due 2026-03-15

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

### Board summary

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

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

### Multi-board dashboard

```bash
karr dashboard                                # scan the current directory

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

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
karr context --json                          # JSON output
karr context --compact                       # board_name and the four counts, key=value
```

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

### Skill management

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

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

karr sync --push
```

Use this when you want explicit control over board ref exchange with the remote
instead of relying only on the implicit pull/push behavior of mutating
commands.

`karr sync` also carries `refs/karr-foundation/*` — karr-foundation's shared
chain, run logs, question mailbox and design documents — in the same run,
after the board and never on its own. One command on purpose: a separate one
would be a second thing to remember, and a coordination namespace nobody
synced fails quietly. Mutating commands still sync the board only, so this
costs nothing outside an explicitly typed `karr sync`, and a repository
holding nothing under `refs/karr-foundation/` pushes nothing there. Deletions
in that namespace (log retention, a cleared chain) travel like board deletions
do, so a pruned run log does not come back on the next pull.

**A fresh clone fetches the board by itself.** `git clone` does not carry
`refs/karr/*`, so a new checkout holds no board while the whole board sits on
its remote. The read commands (`board`, `list`, `show`, `log`, `context`,
`metrics`, `needs`, and `config show`/`config get`) do not pull as a rule —
only mutating commands do — but where there is nothing under `refs/karr/` at
all and the remote has a board, they fetch it once and answer, with one line
on STDERR (never STDOUT) saying where it came from. Where there is no remote,
or the remote has no board, they still refuse with exit 1 rather than
rendering an empty board: that is the only place `karr init` is the answer. In
a clone whose board is on the remote, `karr init` refuses as well and points
at `karr sync`, so it can no longer start a second, empty board beside the
real one; `karr init --new-board` is the documented way through when an
independent board there really is what you want.
`KARR_NO_AUTO_FETCH=1` switches the fetch off where karr must not touch the
network.

### File view (kanban-md interop)

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

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


### Agent name

```bash
NAME=$(karr agent-name)                       # mint once, reuse everywhere
karr pick --claim "$NAME" --move in-progress
karr handoff ID --claim "$NAME" --note "Implementation complete"
```

Every `karr agent-name` call mints a **new** name and remembers it nowhere, so
`--claim "$(karr agent-name)"` written a second time claims under one name and
hands off under another — while the first claim is live the handoff is refused,
and once it has expired it silently re-stamps the card with a name nobody holds.
Capture the name once into a shell variable and pass that same variable to every
later `--claim`, `--claimed-by` and `log --agent`. If it was never captured, read
it back off the board (`karr show ID` → `Claimed:`, or `karr pick`'s own
`(claimed by NAME)`) rather than minting a fresh one.

## Stored task format

```markdown

CLAUDE.md  view on Meta::CPAN

   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
   under `lib/`, never context7.

4. **Untracked files that are not in `.gitignore` belong in the commit.**
   `.gitignore` is the source of truth. Only obvious secrets
   (`.env`, credentials) are excluded — and even then warn, don't silently
   drop them.

5. **Auto-Memory is for personal/user preferences only.** Project
   conventions belong in this `CLAUDE.md` or in a skill, never in
   auto-memory.

6. **Load the `getty-perl-core` skill before editing any Perl** in this
   workspace. It encodes Getty's house rules; the rules below are the
   TL;DR. The `api-docker-*` agents get it force-loaded — see

CLAUDE.md  view on Meta::CPAN

lib/API/Docker/Role/RegistryAuth.pm     # X-Registry-Auth / AuthConfig encoding
lib/API/Docker/Role/Filters.pm          # the `filters` query parameter, shape-normalised
lib/API/Docker/Role/Using.pm            # `using`, the resource class clone that bounds a run of calls
lib/API/Docker/API/System.pm            # /version, /info, /_ping, /auth, /events
lib/API/Docker/API/Containers.pm        # container endpoints (incl. archive, attach)
lib/API/Docker/API/Images.pm            # image endpoints (build, pull, push, tar, commit, ...)
lib/API/Docker/API/Networks.pm          # network endpoints
lib/API/Docker/API/Volumes.pm           # volume endpoints
lib/API/Docker/API/Exec.pm              # exec endpoints
lib/API/Docker/API/Distribution.pm      # /distribution registry manifest lookups
lib/API/Docker/API/Secrets.pm           # /secrets
lib/API/Docker/API/Configs.pm           # /configs
lib/API/Docker/API/Plugins.pm           # /plugins
lib/API/Docker/Type.pm                  # the DSL and attribute registry behind the generated types
lib/API/Docker/Role/Type.pm             # a generated type's own behaviour: serialisation, unknown_fields
lib/API/Docker/Type/                    # generated from spec/, one class per swagger definition -- karr k79
lib/API/Docker/Role/Entity.pm           # the client an entity delegates through
lib/API/Docker/Role/Entity/Container.pm # container operations, composed onto ContainerSummary + ContainerInspectResponse
lib/API/Docker/Role/Entity/Image.pm     # image operations, composed onto ImageSummary + ImageInspect
lib/API/Docker/Role/Entity/Network.pm   # network operations, composed onto Type::Network (one class for list and inspect)
lib/API/Docker/Role/Entity/Volume.pm    # volume operations, composed onto Type::Volume (list, inspect and create)
lib/API/Docker/Role/Entity/Plugin.pm    # plugin operations, composed onto Type::Plugin
lib/API/Docker/Role/Entity/Secret.pm    # secret operations, composed onto Type::Secret
lib/API/Docker/Role/Entity/Config.pm    # config operations, composed onto Type::Config
lib/API/Docker/Error/HTTP.pm            # croaked on a status of 400 or above
lib/API/Docker/Error/Stream.pm          # croaked on a failed build/pull/push stream
lib/API/Docker/Error/Timeout.pm         # croaked when a read_timeout or connect_timeout runs out
lib/API/Docker/Error/Truncated.pm       # croaked when the daemon closed before its announced response was complete
maint/spec-to-type.pl                   # generates lib/API/Docker/Type/*.pm from spec/ -- never overwrites
maint/spec-drift-check.pl               # diffs spec/ against the registry, and spec against spec
maint/spec-common.pl                    # the spec loader shared by the two scripts above
maint/spec-to-type-names.yaml           # inline-class naming exceptions the generator and checker share
maint/spec-to-type-prose.yaml           # hand-written POD for fields/classes the swagger describes poorly

Changes  view on Meta::CPAN

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

Changes  view on Meta::CPAN

    credentials without pulling or pushing. A rejected credential croaks.
  - New `API::Docker::API::Distribution` (`inspect`/`exists`,
    GET /distribution/{name}/json): ask a registry for a manifest without
    pulling. `exists` answers `1`/`0` and tells a registry's own 404 apart
    from an engine that serves no such route.
  - Declare a minimum Perl of 5.014 (`s///r` in `Role::HTTP`) and add the
    core modules `Errno`, `IO::Handle`, `Scalar::Util` and `Socket` to
    `cpanfile`. Stop shipping `spec/` and `maint/` in the built dist.
  - Swarm (`/swarm`, `/nodes`, `/services`, `/tasks`) is documented as a
    permanent scope decision, not a gap: Podman implements none of it and no
    consumer needs it. `secrets` and `configs` stand on their own and stay
    covered.

0.003     2026-08-27 03:37:00Z
  - t/containers.t: the registered cleanup tolerates the container the
    happy path already removed, so a live run no longer warns "Cleanup
    failed: ... no such container" on every pass. The safety net still
    warns on any other failure.
  - The `>= 400` croak now falls back to `errorDetail.message` and then to
    the flat `error` key when the JSON error body carries no `message`.
    Docker answers `{"message":...}`; Podman answers a failed push with

Changes  view on Meta::CPAN

    and one `=`. Measured against a local registry: before, all three
    tags of a test image came back 400 and nothing reached the registry;
    after, all three are there.
    The test that covered this could not have caught it. Its decode
    helper computed the missing padding and appended it before decoding,
    so the assertions passed either way. It now decodes what the engine
    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

MANIFEST  view on Meta::CPAN

t/fixtures/containers_logs_tty_json.bin
t/fixtures/exec_start_multiplexed.bin
t/fixtures/images_build_error_stream.ndjson
t/fixtures/images_build_quiet_stream.ndjson
t/fixtures/images_build_stream.ndjson
t/fixtures/images_get.tar
t/fixtures/images_list.json
t/fixtures/images_load_stream.ndjson
t/fixtures/images_pull_stream.ndjson
t/fixtures/networks_list.json
t/fixtures/secrets_list.json
t/fixtures/system_events_stream.ndjson
t/fixtures/system_info.json
t/fixtures/system_version.json
t/fixtures/volumes_list.json
t/images.t
t/images_build_prune.t
t/images_commit.t
t/images_push_auth.t
t/images_registry_auth.t
t/images_tar.t

MANIFEST  view on Meta::CPAN

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

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

  return $self->host =~ m{^tcp://} ? 1 : 0;
}


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


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


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

  # Both checks are here rather than at connect time so that a request for
  # encryption that cannot be honoured is refused before the caller has a
  # client to hand credentials to.
  croak __PACKAGE__ . '->new tls_insecure => 1 without tls => 1 does '
    . 'nothing: verification is only reachable on a connection that has TLS '
    . 'to verify. Set tls => 1 as well, or drop the option'
    if $self->tls_insecure && !$self->tls;

  return unless $self->tls;

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

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

  builder => sub { API::Docker::API::Exec->new(client => $_[0]) },
);


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


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


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

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

=item * L<API::Docker::API::Images> - Image management

=item * L<API::Docker::API::Networks> - Network management

=item * L<API::Docker::API::Volumes> - Volume management

=item * L<API::Docker::API::Exec> - Exec into containers

=item * L<API::Docker::API::Distribution> - Registry manifest lookups

=item * L<API::Docker::API::Secrets> - Swarm secrets

=item * L<API::Docker::API::Configs> - Swarm configs

=item * L<API::Docker::API::Plugins> - Managed plugins

=back

=item * B<Entity Roles> - the convenience methods of a resource, composed at
load time onto the generated L<API::Docker::Type> classes its endpoints
answer with. There is no separate wrapper object: C<< $docker->images->list >>

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


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

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

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

L<IO::Socket::SSL> is a recommended rather than a required dependency, loaded
when the first TLS connection is opened; C<< tls => 1 >> without it installed
croaks naming it. See
L<API::Docker::Role::HTTP/"TLS on a tcp:// connection"> for the whole of the

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

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


=head2 exec

Returns L<API::Docker::API::Exec> instance for executing commands in containers.

=head2 distribution

Returns L<API::Docker::API::Distribution> instance for registry manifest
lookups: C<inspect> and C<exists>.

=head2 secrets

Returns L<API::Docker::API::Secrets> instance for secret operations: C<list>,
C<create>, C<inspect>, C<update> and C<remove>.

=head2 configs

Returns L<API::Docker::API::Configs> instance for config operations: C<list>,
C<create>, C<inspect>, C<update> and C<remove>.

=head2 plugins

Returns L<API::Docker::API::Plugins> instance for managed-plugin operations:

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

=item * L<API::Docker::API::Images> - Image management

=item * L<API::Docker::API::Networks> - Network management

=item * L<API::Docker::API::Volumes> - Volume management

=item * L<API::Docker::API::Exec> - Execute commands in containers

=item * L<API::Docker::API::Distribution> - Registry manifest lookups

=item * L<API::Docker::API::Secrets> - Swarm secrets

=item * L<API::Docker::API::Configs> - Swarm configs

=item * L<API::Docker::API::Plugins> - Managed plugins

=item * L<API::Docker::Error::HTTP> - Raised for a status of 400 or above,
carries the status code

=item * L<API::Docker::Error::Stream> - Raised for a failure reported inside a
200 event stream

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

    $self->client->get("/configs/$id",
      %{ $self->_request_options },
    ));
}


sub update {
  my ($self, $id, $version, %spec) = @_;
  croak __PACKAGE__ . '->update config ID or name required'
    unless defined $id && length $id;
  croak __PACKAGE__ . '->update requires the current version as its second '
    . 'argument: the Version.Index from inspect($id), which the daemon uses '
    . 'as an optimistic-concurrency token and will not accept the update '
    . 'without'
    unless defined $version;
  croak __PACKAGE__ . '->update version must be the numeric Version.Index '
    . "from inspect(\$id), got '$version'"
    unless $version =~ /\A[0-9]+\z/;
  $spec{Data} = $self->_encode_data('update', $spec{Data})
    if defined $spec{Data};
  return $self->client->post("/configs/$id/update", \%spec,

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

class for both, where containers and images have two: the swagger answers
C<GET /configs> with an array of the C<Config> definition and
C<GET /configs/{id}> with that same definition. Field names are the
swagger's own spelling in snake_case -- C<ID> is C<< ->id >>, C<CreatedAt> is
C<< ->created_at >> -- and the nested ones are generated classes rather than
the raw HashRefs the old entity kept: C<< $config->spec >> is an
L<API::Docker::Type::ConfigSpec> and C<< $config->version >> an
L<API::Docker::Type::ObjectVersion>, whose C<< ->index >> is what
C<version_index> reaches.

Configs are L<API::Docker::API::Secrets> without the secrecy: the same five
endpoints, the same spec shape, the same mandatory C<version> on update. The
one behavioural difference is that a config's value can be read back --
L</inspect> returns it in C<< $config->spec->data >>, and
L<API::Docker::Role::Entity::Config/decoded_data> decodes it, where a secret
returns no payload at all. Which is the whole point of the split: put configuration in
a config, and anything you would mind seeing in a C<docker config inspect> in
a secret.

=head2 Data is raw bytes on the way out, base64 on the way back

The wire field C<Data> carries base64. B<L</create> and L</update> encode it
for you> -- pass them raw bytes, and do not pre-encode, or the daemon stores
your base64 text as the config's content.

Doing it here is not a convenience, it is a guard. The daemon does not
validate what it decodes: measured against Podman 5.4.2's C</secrets>, which
takes the identical field, a C<Data> of the plain text C<"hello there!"> was
accepted with B<HTTP 200> and stored three bytes of garbage -- Go's decoder
took the leading C<"hell">, stopped at the space, and said nothing. A caller
left to encode their own payload can corrupt the value and be told it worked.

The alphabet is B<standard> base64 with padding (C<+> and C</>), unwrapped,
and not the URL-safe one. The Engine API reference calls the field
"base64-url-safe-encoded" and that is measurably not what the engine takes:
four bytes sent as C<-v_--w==> were rejected B<500>, the same four as
C<+v/++w==> stored correctly.

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

the config as it stands right now, which is what L</inspect> returns:

    my $config = $docker->configs->inspect($id);
    $docker->configs->update($id, $config->version_index, %spec);

It is an optimistic-concurrency token, not a serial number to invent. If
anything else changed the config since that C<inspect>, the index has moved on
and the daemon refuses the write rather than silently overwriting that change.
Read it immediately before the update, and again before a retry.

This class makes it the second positional argument and croaks when it is
missing or not numeric, so the mistake surfaces here instead of one round trip
later. L<API::Docker::Role::Entity::Config/update> supplies it from the
entity's own C<< ->version->index >> instead, which is the same value read at
the same moment.

The Engine API reference states that only C<Labels> may actually change: the
rest of the spec must go back unchanged from what C<inspect> returned. Hence
C<< %spec = %{ $config->spec->TO_JSON } >> in the SYNOPSIS -- C<TO_JSON>
renders the spec object back into the daemon's own spelling, and the whole
spec goes back with the one key edited. Note that a spec from C<inspect>
carries C<Data> already base64-encoded, so passing it straight to L</update>
would encode it a second time; delete the key, or pass
L<API::Docker::Role::Entity::Config/decoded_data> in its place, before
sending it back.

=head2 Swarm, and Podman

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

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

-- C<GET /configs/{id}>, C<POST /configs/create>, C<DELETE /configs/{id}> and
C<POST /configs/{id}/update> -- answers B<503> instead, with a JSON body
naming the route it refuses, e.g. C<< {"cause":"Podman does not support
service: /v1.44/configs/xyz","message":"...","response":503} >>.

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

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

=head2 client

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

=head2 list

    my $configs = $configs->list;
    my $configs = $configs->list(filters => { label => ['app=web'] });

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

# check earns its round trip anyway, because the condition it tests is exactly
# the condition that does the damage. Measured on Podman 5.4.2 (API 1.41) and
# Docker 29.7.2 (API 1.55), one container per row, each exiting with status 4:
#
#   attach to an ALREADY-EXITED container      Podman: status destroyed
#   attach while RUNNING, exits under the call Podman: status intact (4)
#   either of those                            Docker: status intact (4)
#
# So "running at the moment of the call" is the whole of the condition. A
# container that is still running when attach is sent stays safe even when it
# exits a millisecond later, which is why the pre-flight answer is worth having
# despite being one round trip stale.
#
# 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

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

    data     => $error,
  );
  croak $err;
}

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

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

  # The guard has to sit on both sides of the callback split, because the same
  # body arrives either way: buffered it is the return value, streamed it goes
  # to the callback and is never returned at all. Wrapping puts the check in
  # front of the caller's callback, so no caller is handed the error object as

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

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

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

=back

=head2 restart

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

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

Reports 1/0 like L</start>, but a restart has no no-op state to report: the
engine restarts a stopped container as readily as a running one. Measured
against Podman 5.4.2 (API 1.41) it answers 204 in both cases, and the Docker
Engine API documents no 304 for this endpoint either, so 0 is not expected
here. The value is reported the same way rather than specially, so an engine
that does answer 304 is not silently read as a change.

=head2 kill

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

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

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

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

=head2 This method refuses a container that is not running

Because of the above, C<attach> asks L</inspect> whether the container is
running and B<croaks instead of attaching> when it is not:

    API::Docker::API::Containers->attach refused: container x is exited. ...

C<< require_running => 0 >> turns that off and attaches anyway; the check is
then not performed at all, so opting out costs no round trip either.

With the guard off, the hang this section exists to explain becomes reachable:
attaching to a container that has already exited never returns on rootless
Podman (measured 5.4.2, still true on 5.8.4, API 1.44). A bound on the
resource class is how to survive that call instead of blocking on it forever:

    $docker->containers->using(read_timeout => 2)
      ->attach($id, require_running => 0);

See L<API::Docker::Role::Using> and
L<API::Docker::Role::HTTP/"Bounding a request that never ends">.

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

from a client. What makes the check worth its round trip is that the window is
one round trip wide rather than unbounded, and that the condition it tests is
precisely the condition that does the damage. Measured, one container per row,
each exiting with status 4:

    attach to an ALREADY-EXITED container       Podman: status destroyed
    attach while RUNNING, exits under the call  Podman: status intact (4)
    either of those                             Docker: status intact (4)

A container that is still running when the attach is sent therefore stays
safe even when it exits a millisecond later. The damage needs the container to
be stopped B<already>, which is the common case and the one the check catches:
a caller reaching for a container it knows has finished.

Refusing costs a caller nothing that C<attach> could have given them, on
either engine. Against a container that is not running there is no combination
that is both safe and useful: on Podman every variant destroys the exit
status, C<< stream => 1 >> hangs forever on B<both> engines (measured: Docker
was still open when a 10 s probe gave up), and Docker's C<< stream => 0 >>
replay returns the same frames L</logs> returns, with none of the hazard and
with C<tail> and C<since> on top.

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

=head2 This is the one-way attach

The engine has two attach protocols behind one path. Sent with
C<Upgrade: tcp> and C<Connection: Upgrade>, C<< POST /containers/{id}/attach >>
answers B<101 Switching Protocols> and hands over a bidirectional connection:
that is what C<docker attach> uses, and it is what lets a caller type into the
container's stdin. Sent B<without> those headers -- which is what this method
does -- the engine answers B<200> and streams the container's output one way,
in exactly the frames L</logs> returns.

This method implements the second one only, because the transport here buffers
a whole response before returning it (see
L<API::Docker::Role::HTTP/"What the transport does not do">). Two consequences
a caller has to plan around:

=over

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

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

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

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

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

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

=back

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

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

Options:

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

report every one of those as a failure.

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

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

So the advice this section used to give -- test for C<read> or C<cpu_stats>
before using what comes back -- is B<wrong on Docker>: both keys are present,
both look plausible, the test passes, and the caller uses zeros as though
they were a measurement. The markers in that body are Go's zero time in
C<read> (C<0001-01-01T00:00:00Z>) and an empty C<memory_stats>, and those are
Docker's shape rather than anybody's contract.

C<num_procs> is B<not> one of them, contrary to what this section said before
it was measured: a one-shot reading taken from a container that was genuinely
B<running> on Docker 29.7.2 carries C<< num_procs => 0 >> as well. It is zero
on that engine either way and separates nothing.

The engine-independent question is not about the reading at all: ask
L</inspect> whether the container is running. Treat the zero timestamp as a
cheap Docker-specific second opinion, not as the test.

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

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

=over

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

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

=back

C<< stream => 1 >> without a callback therefore never returns on Docker for a
container that is not running. C<on_event> plus a C<< $stop->() >> is the
only way out, and there the callback has to decide for itself that a reading
is not one: that stream carries nothing to croak on.

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


So on Docker C<< stream => 1 >> has B<no> terminator tied to the container at
all, and the readings turn to zeros without anything in the stream saying so.
A caller that follows a container's stats until it stops is asking for
something this endpoint does not offer on that engine: give C<on_event> its
own stopping condition -- a reading count, a deadline, or the Go zero time in
C<read> -- and do not wait for the stream to end on its own.

B<C<read_timeout> does not bound this.> It is an idle timeout -- silence since
the last byte -- and this stream is never silent: it keeps producing a
zero-filled reading once a second, indefinitely, so the clock that C<read_timeout>
measures never runs out. C<read_timeout> bounds a daemon that goes quiet; a
Docker stats stream after container exit does the opposite -- it keeps talking,
just not truthfully. C<on_event> still has to notice the Go zero time in
C<read> and call C<< $stop->() >> itself; do not rely on C<read_timeout> to end
this case.

=head2 Why this is documented and not guarded

L</attach> refuses a container that is not running (see
L</"This method refuses a container that is not running">). This method does

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


A pre-flight L</inspect> can only answer I<is it running now>. For L</attach>
that is the whole question: the damage needs the container to be stopped
already, so the check leaves a window one round trip wide. For this method it
is the wrong question -- the hazard is the container stopping at B<any> point
in a stream that may run for hours, which the measurement above is exactly a
case of. A guard here would have returned "running, go ahead" and the caller
would have hung anyway. Its blind spot is not a round trip, it is the entire
stream.

The second difference is that nothing here is destroyed. This is a read: after
a hang or a pocketful of zeros, L</inspect> still reports the truth and the
exit status is still there. That is precisely what L</attach> takes away -- a
caller cannot check afterwards, because checking afterwards is the thing that
stops working. A guard is worth an unclosable race when the alternative is
unrecoverable, and is not worth it when the caller can simply ask again.

What can be caught for free already is: Podman reports its refusal in the body
and this method croaks on it, with no extra request and no race.

=head2 changes

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

The boolean flags (C<Init>, C<OomKillDisable>) may be given as a Perl C<1>/C<0>
or as a JSON boolean; either goes out as a real JSON C<true>/C<false>, which
the engine's body type-check requires.

=head2 get_archive

    use Path::Tiny;
    my $tar = $containers->get_archive($id, path => '/etc/hostname');
    path('hostname.tar')->spew_raw($tar);

    # and what the path was, without a second request
    my %stat;
    my $tar = $containers->get_archive($id, path => '/var/log', stat => \%stat);
    say $stat{name};

Read a path out of a container as a tar archive -- the outbound half of
C<docker cp>. Returns the raw archive bytes, never decoded and never modified.

A file comes back as a one-member archive named after its basename; a
directory comes back as the directory and everything under it, with paths
relative to its parent. The whole archive is buffered in memory.

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


    my $docker = API::Docker->new;

    # Ask a registry about an image reference without pulling it
    my $descriptor = $docker->distribution->inspect('nginx:latest');

    # With registry credentials
    my $descriptor = $docker->distribution->inspect('private/app:1.0',
        auth => {
            username => 'someone',
            password => 'secret',
        },
    );

    # The same question as a predicate: is that tag already published?
    if ($docker->distribution->exists('myrepo/app:1.0', auth => $auth)) {
        die "refusing to overwrite a released tag";
    }

=head1 DESCRIPTION

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

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

=item * C<target> - Multi-stage build target

=item * C<registry_config> - Registry credentials for the base images the build
pulls, sent as C<X-Registry-Config>. A HashRef mapping each registry hostname
to its AuthConfig --
C<< { 'registry.example:5000' => { username => 'me', password => 'secret' } } >>
-- so a C<FROM private.registry/...> can authenticate, and a build drawing from
several registries can carry all of them at once. A pre-encoded base64 string
is also accepted. Sent only when given. This is B<not> C<auth>/C<X-Registry-Auth>,
which carries a single AuthConfig; C</build> uses the map form. See
L<API::Docker::Role::RegistryAuth>

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

=back

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


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

With a callback the return value is that summary HashRef, not the events:
C<delivered> is how many went to the callback, C<stopped> is 1 when the
callback ended the stream and 0 when the daemon did. Nothing is accumulated,
so a caller that wants the C<aux> event with the image id in it must keep that
event itself as it goes by. See
L<API::Docker::Role::HTTP/"Streaming a response as it arrives">.

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

=head3 A failed build still croaks, one event earlier

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

=over

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


    my $history = $images->history('nginx:latest');

Get image history (layers). Returns ArrayRef of layer information.

=head2 push

    my $events = $images->push('myrepo/nginx', tag => 'v1');
    $images->push('myrepo/nginx', auth => {
        username      => 'me',
        password      => 'secret',
        serveraddress => 'https://index.docker.io/v1/',
    });

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

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

A failed push croaks, by one of two routes depending on the engine -- an
unauthorised push to a private registry is the common case, and it is exactly

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

=head2 Installing is two calls, and the engine enforces it

C<< POST /plugins/pull >> takes the list of privileges the plugin demands
B<in its request body>, and the daemon compares that list against the one it
computes from the plugin's own config. They must match exactly -- same
length, same names, same values -- or the install fails with
C<incorrect privileges>. A plugin runs with the host access it asked for, so
the round trip exists to make somebody look at that access before granting
it.

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

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

C<install> B<requires> C<privileges> and croaks without it, which is stricter
than the engine: the daemon's own body parser treats a missing body as an
empty privilege list rather than an error, so a blind install of a plugin
that happens to demand nothing would quietly succeed and one that demands

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


=back

Returns an ArrayRef of progress events, C<[]> when the engine sent no
progress. Failure is reported by the same two routes as L</install>.

=head2 push

    $plugins->push('myrepo/sshfs:v1', auth => {
        username      => 'me',
        password      => 'secret',
        serveraddress => 'https://index.docker.io/v1/',
    });

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

Options:

=over

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

    if $data =~ /[^\x00-\xff]/;
  return encode_base64($data, '');
}

sub list {
  my ($self, %opts) = @_;
  my %params;
  $params{filters} = $self->_normalise_filters($opts{filters})
    if defined $opts{filters};
  return $self->_wrap_list('API::Docker::Type::Secret',
    $self->client->get('/secrets',
      params => \%params,
      %{ $self->_request_options },
    ) // []);
}


sub create {
  my ($self, %spec) = @_;
  croak __PACKAGE__ . '->create Name required'
    unless defined $spec{Name} && length $spec{Name};
  croak __PACKAGE__ . '->create Data required'
    unless defined $spec{Data} && length $spec{Data};
  $spec{Data} = $self->_encode_data('create', $spec{Data});
  return $self->client->post('/secrets/create', \%spec);
}


sub inspect {
  my ($self, $id) = @_;
  croak __PACKAGE__ . '->inspect secret ID or name required'
    unless defined $id && length $id;
  return $self->_wrap('API::Docker::Type::Secret',
    $self->client->get("/secrets/$id",
      %{ $self->_request_options },
    ));
}


sub update {
  my ($self, $id, $version, %spec) = @_;
  croak __PACKAGE__ . '->update secret ID or name required'
    unless defined $id && length $id;
  croak __PACKAGE__ . '->update requires the current version as its second '
    . 'argument: the Version.Index from inspect($id), which the daemon uses '
    . 'as an optimistic-concurrency token and will not accept the update '
    . 'without'
    unless defined $version;
  croak __PACKAGE__ . '->update version must be the numeric Version.Index '
    . "from inspect(\$id), got '$version'"
    unless $version =~ /\A[0-9]+\z/;
  $spec{Data} = $self->_encode_data('update', $spec{Data})
    if defined $spec{Data};
  return $self->client->post("/secrets/$id/update", \%spec,
    params => { version => $version });
}


sub remove {
  my ($self, $id) = @_;
  croak __PACKAGE__ . '->remove secret ID or name required'
    unless defined $id && length $id;
  return $self->client->delete_request("/secrets/$id",
    %{ $self->_request_options },
  );
}



1;

__END__

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

API::Docker::API::Secrets - Docker Engine Secrets API

=head1 VERSION

version 0.004

=head1 SYNOPSIS

    my $docker = API::Docker->new;

    # List secrets
    my $secrets = $docker->secrets->list;

    # Create a secret -- Data is RAW BYTES, this class base64-encodes it
    my $created = $docker->secrets->create(
        Name   => 'my-secret',
        Data   => "hunter2\n",
        Labels => { env => 'prod' },
    );

    # Inspect a secret -- an API::Docker::Type::Secret
    my $secret = $docker->secrets->inspect($created->{ID});
    say $secret->spec->name;

    # Update: the version comes from the inspect above, and is mandatory
    my %spec = %{ $secret->spec->TO_JSON };
    $spec{Labels} = { env => 'staging' };
    $secret->update(%spec);

    # Remove
    $docker->secrets->remove($created->{ID});

=head1 DESCRIPTION

This module provides methods for managing Docker secrets (C</secrets>):
listing, creation, inspection, update and removal.

Accessed via C<< $docker->secrets >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->secrets->using(read_timeout => 5) >>.

L</list> and L</inspect> return L<API::Docker::Type::Secret> objects carrying
the convenience methods of L<API::Docker::Role::Entity::Secret>. It is B<one>
class for both, where containers and images have two: the swagger answers
C<GET /secrets> with an array of the C<Secret> definition and
C<GET /secrets/{id}> with that same definition. Field names are the
swagger's own spelling in snake_case -- C<ID> is C<< ->id >>, C<CreatedAt> is
C<< ->created_at >> -- and the nested ones are generated classes rather than
the raw HashRefs the old entity kept: C<< $secret->spec >> is an
L<API::Docker::Type::SecretSpec> and C<< $secret->version >> an
L<API::Docker::Type::ObjectVersion>, whose C<< ->index >> is what
C<version_index> reaches.

The value of a secret is write-only. L</list> and L</inspect> return the
metadata -- C<< ->id >>, C<< ->spec >>, C<< ->created_at >>,
C<< ->version >> -- and never the payload; the engine hands that out to
containers, not over this API. If you need to read the value back, this is
the wrong storage: use L<API::Docker::API::Configs>, whose entity offers a
C<decoded_data> because the daemon actually sends one.

=head2 Data is raw bytes; this class does the base64

The wire field C<Data> carries base64. B<This class encodes it for you.> Pass
L</create> raw bytes and they go out encoded; do not pre-encode, or the daemon
faithfully stores your base64 text as the secret.

That division of labour is not a matter of taste, because the daemon does not
validate what it decodes. Measured against Podman 5.4.2: a C<Data> of the
plain text C<"hello there!"> was accepted with B<HTTP 200> and stored three
bytes of garbage -- Go's decoder consumed the leading C<"hell">, stopped at
the space, and reported nothing. A caller left to encode their own payload can
therefore corrupt a secret and be told it succeeded. Doing it here removes
that failure mode from the caller entirely.

The alphabet is B<standard> base64 with padding (C<+> and C</>), not the
URL-safe one, and unwrapped. The Engine API reference calls the field
"base64-url-safe-encoded"; that is measurably not what the engine accepts. The
same four bytes sent as C<-v_--w==> were rejected with B<500>
C<"secret data must be larger than 0 and less than 512000 bytes"> -- the
URL-safe alphabet decoded to nothing -- where C<+v/++w==> was stored correctly.

C<Data> must be a byte string. A string holding characters above C<U+00FF>
croaks here rather than reaching L<MIME::Base64>, which would die with a bare
C<Wide character in subroutine entry>. Encode it first, for instance with
C<Encode::encode_utf8>.

To send an already-encoded value verbatim, bypass this class and use the
transport directly:

    $docker->post('/secrets/create', { Name => 'my-secret', Data => $b64 });

=head2 update takes the current version, and it is mandatory

C<POST /secrets/{id}/update> carries a C<version> query parameter, and the
daemon rejects the request without it. The value is the C<Version.Index> of
the secret as it stands right now, which is what L</inspect> returns:

    my $secret = $docker->secrets->inspect($id);
    $docker->secrets->update($id, $secret->version_index, %spec);

It is an optimistic-concurrency token, not a serial number to invent. If
anything else changed the secret since that C<inspect>, the index has moved on
and the daemon refuses the write instead of silently overwriting that change.
Read it immediately before the update, and read it again before a retry.

This class makes it the second positional argument and croaks when it is
missing or not numeric, so the mistake is caught here rather than one round
trip later. L<API::Docker::Role::Entity::Secret/update> supplies it from the
entity's own C<< ->version->index >> instead, which is the same value read at
the same moment.

The Engine API reference states that only C<Labels> may actually change: every
other field of the spec must be sent back unchanged from what C<inspect>
returned. Hence the C<< %spec = %{ $secret->spec->TO_JSON } >> in the
SYNOPSIS -- C<TO_JSON> renders the spec object back into the daemon's own
spelling, and the whole spec goes back with the one key edited, not just the
key you edited.

=head2 Swarm, and what Podman serves instead

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

C<GET /info>'s C<Swarm.LocalNodeState> does not tell you which of those two
you are looking at. Measured fresh against both engines with no swarm
initialized anywhere, it reports C<"inactive"> on Docker and on Podman alike
-- and Podman still serves C</secrets> with B<200> and real data in that
state, while Docker still answers B<503>. Whether C</secrets> works is a
property of the engine, not of that field.

Podman is the useful exception. Measured against Podman 5.4.2 (API 1.41) with
no swarm involved anywhere, C</secrets> is served from Podman's own local
secret store: L</list>, L</create>, L</inspect> and L</remove> all work, and
the objects carry a C<Version.Index> just as Docker's do. Two differences
worth knowing:

=over

=item * L</create> answers B<200> where Docker documents B<201>. The body is
the same C<< { ID => ... } >>, so only code inspecting the status code notices.

=item * L</update> is not implemented at all: B<501>
C<"update is not supported">, with or without a C<version> parameter.

=back

L<API::Docker::API::Configs> gets none of this -- Podman does not serve
C</configs> from a real store the way it does C</secrets>; see
L<API::Docker::API::Configs/"Swarm, and Podman"> for what it answers instead,
which is not simply "no route" on every path.

=head2 client

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

=head2 list

    my $secrets = $secrets->list;
    my $secrets = $secrets->list(filters => { label => ['env=prod'] });

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

Options:

=over

=item * C<filters> - HashRef of filters, JSON-encoded by the transport. The
Engine API accepts C<id>, C<label>, C<name> and C<names>; values are always
ArrayRefs of strings, shape-checked and normalised by
L<API::Docker::Role::Filters>.

=back

=head2 create

    my $created = $secrets->create(
        Name   => 'my-secret',
        Data   => "hunter2\n",
        Labels => { env => 'prod' },
    );

Create a secret. Returns the daemon's response, a HashRef carrying C<ID> --
not an L<API::Docker::Type::Secret>, because C<ID> is all the daemon answers
with
and an entity built from it would carry no C<Spec> and no C<Version>. Call
L</inspect> on that C<ID> for the object.

Options:

=over

=item * C<Name> - Required. The secret's name.

=item * C<Data> - Required. The secret's value as B<raw bytes>; this method
base64-encodes it. See L</"Data is raw bytes; this class does the base64">.

=item * C<Labels> - HashRef of labels.

=item * C<Driver> - HashRef naming an external secret driver, C<< { Name =>
..., Options => {...} } >>.

=item * C<Templating> - HashRef naming a templating driver, same shape.

=back

=head2 inspect

    my $secret = $secrets->inspect($id);
    my $index  = $secret->version_index;      # what update needs

Get a secret's metadata by ID or name. Returns an
L<API::Docker::Type::Secret> -- the same class L</list> returns -- with
C<< ->id >>, C<< ->spec >>, C<< ->created_at >>, C<< ->updated_at >> and
C<< ->version >>. Never the value -- see
L<API::Docker::Role::Entity::Secret/"There is no accessor for the value">.

=head2 update

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

    $secrets->update($id, $secret->version_index, %spec);
    $secret->update(%spec);                   # the same call, via the entity

Update a secret. Returns nothing on success -- the daemon answers 200 with an
empty body.

C<$version> is mandatory and is the C<Version.Index> from L</inspect>; see
L</"update takes the current version, and it is mandatory"> for why it cannot
be guessed and why the whole spec goes back.
L<API::Docker::Role::Entity::Secret/update> fills it in from the entity it
was called on. Podman does not implement this
endpoint and answers 501.

=head2 remove

    $secrets->remove($id);

Remove a secret by ID or name. The daemon answers 204 with no body, so this
returns nothing; a secret that is not there is a 404 and croaks.

=head1 SEE ALSO

=over

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

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

=item * L<API::Docker::Type::Secret> - the fields L</list> and L</inspect>
return

=item * L<API::Docker::API::Configs> - Configs, the same shape without the
secrecy

=back

=head1 SUPPORT

=head2 Issues

Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/p5-api-docker/issues>.

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

    my $events = $docker->system->events(
        since => time() - 3600,
    );

    # Disk usage
    my $df = $docker->system->df;

    # Check registry credentials before doing the work that needs them
    my $login = $docker->system->auth(
        username      => 'me',
        password      => 'secret',
        serveraddress => 'ghcr.io',
    );
    say $login->{Status};   # Login Succeeded

=head1 DESCRIPTION

This module provides access to Docker system-level operations including daemon
information, version detection, health checks, and event monitoring.

Accessed via C<< $docker->system >>, or through

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

    $summary;   # { delivered => 12, stopped => 1 }

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

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

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

=head2 df

    my $usage = $system->df;

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

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

=head2 auth

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

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

=head1 NAME

API::Docker::Error::Timeout - Read timeout while waiting for the Docker Engine

=head1 VERSION

version 0.004

=head1 SYNOPSIS

    # Stop waiting after two seconds of silence instead of hanging forever.
    my $out = '';
    eval {
        $docker->containers->attach($id,
            stream       => 1,
            stdout       => 1,
            read_timeout => 2,
            on_frame     => sub { $out .= $_[0]{data} },
        );
    };
    if (my $err = $@) {

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


=head2 phase

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

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

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

=head2 timeout

The number of seconds of silence that triggered this, i.e. the effective
C<read_timeout> of the request -- or, when L</phase> is C<'connect'>, the
C<connect_timeout> that expired. Not the total time the request took: a stream
that sent something every second for an hour and then stopped reports the same
value as one that never said anything.

=head2 partial

The response body bytes that had arrived when the timeout fired, for a request
whose body was being buffered -- the empty string when none had.

These are B<not> a body: nothing was decoded, no chunk framing was verified and
the content may stop mid-value. They are here so a caller who wants them can
have them rather than because the transport thinks they are usable.

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

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

=over

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

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

=item * C<'content-length'> - fewer bytes arrived than the C<Content-Length>
header announced, or the header arrived in full but its value is not a number.
The second is no cut response either: left as it stood it would read as C<0>
and a response that had a body would come back empty, the same body-shaped lie
a truncation is

=item * C<'chunk-header'> - the stream ended inside a chunk size line, or at a
chunk boundary with no terminating zero chunk after it, or a chunk size line
that arrived in full but is not a hexadecimal number. The last is not a cut
response: the line is complete and terminated, but C<hex> would read its
garbage as C<0> -- the terminating zero chunk -- so the body would silently
come back empty. It is caught here because the outcome is the same body-shaped
lie a truncation is, not because the connection went away

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

or method in one of those files fails the suite; there is no mode of the
generator that would put it back.

=head2 Why a role, and not a class that contains the type object

Because the daemon answers C<GET /containers/json> and
C<GET /containers/{id}/json> with two different definitions, which are two
different generated classes -- L<API::Docker::Type::ContainerSummary> and
L<API::Docker::Type::ContainerInspectResponse>. Both need the same methods.

A wrapper class holding a type object would be a second model beside the
generated one: every field access would have to be forwarded, and
C<< $container->state >> would return either the wrapper's idea of a state
or the type object's, depending on which one the caller happened to hold.
Composing a role into both generated classes leaves exactly one model.
C<< $docker->containers->list >> hands back real
L<API::Docker::Type::ContainerSummary> objects, C<TO_JSON> still produces
the daemon's own spelling, and the methods are written once.

Applying the role to each B<object> instead (C<apply_roles_to_object>) would
also work and was rejected: it reblesses every entity into a generated

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


=head1 DESCRIPTION

The convenience methods of a Docker config. This role is composed, at load
time, into L<API::Docker::Type::Config>, the generated class the daemon
answers config requests with -- the same definition for C<GET /configs> and
C<GET /configs/{id}>, so L<API::Docker::API::Configs/list> and
L<API::Docker::API::Configs/inspect> hand back one class and there is no
list-versus-inspect shape to keep apart.

A config is a secret whose value can be read back: the daemon returns it in
C<< spec->data >> as base64, where a secret returns no payload at all --
L<API::Docker::Role::Entity::Secret/"There is no accessor for the value">.
L</decoded_data> is the accessor for it.

=head2 Decoding is offered here, not in the API class

L<API::Docker::API::Configs> hands back the daemon's response with nothing
rewritten, which is the rule the whole distribution follows -- so
C<< $config->spec->data >> is the base64 string the engine sent, unchanged,
and stays that way. L</decoded_data> does not touch it either: it decodes on
demand and returns the bytes, leaving the spec verbatim for anyone who wants

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


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

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

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

=head2 remove

    $config->remove;

Remove the config. The daemon answers 204 with no body, so this returns
nothing.

=head1 SEE ALSO

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

        privileges => $privileges);

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

=head2 push

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

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

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

=head1 SEE ALSO

=over

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

package API::Docker::Role::Entity::Secret;
# ABSTRACT: Secret operations, on the generated secret type
our $VERSION = '0.004';
use Moo::Role;
with 'API::Docker::Role::Entity';
requires 'id';
use API::Docker::Type::Secret;
use Carp qw( croak );
use Package::Stash;
use namespace::clean;


sub version_index {
  my ($self) = @_;
  my $version = $self->version;
  return unless defined $version;
  return $version->index;
}


sub inspect {
  my ($self) = @_;
  return $self->client->secrets->inspect($self->id);
}


sub update {
  my ($self, %opts) = @_;
  my $version
    = exists $opts{version} ? delete $opts{version} : $self->version_index;
  return $self->client->secrets->update($self->id, $version, %opts);
}


sub remove {
  my ($self) = @_;
  return $self->client->secrets->remove($self->id);
}


# --- composition -----------------------------------------------------------
#
# Here rather than in API::Docker::API::Secrets, 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

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

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

API::Docker::Role::Entity::Secret - Secret operations, on the generated secret type

=head1 VERSION

version 0.004

=head1 SYNOPSIS

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

    say $secret->id;
    say $secret->spec->name;
    say $secret->version_index;

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

    $secret->remove;

=head1 DESCRIPTION

The convenience methods of a Docker secret. This role is composed, at load
time, into L<API::Docker::Type::Secret>, the generated class the daemon
answers secret requests with -- the same definition for C<GET /secrets> and
C<GET /secrets/{id}>, so L<API::Docker::API::Secrets/list> and
L<API::Docker::API::Secrets/inspect> hand back one class and there is no
list-versus-inspect shape to keep apart.

=head2 There is no accessor for the value

A secret carries no payload on this API at all. The daemon hands the value to
containers, never back over C</secrets>: neither C<list> nor C<inspect>
returns a C<< spec->data >>, so there is nothing here to decode. That is why
this role has no C<decoded_data>, where
L<API::Docker::Role::Entity::Config> does -- the difference is in what the
engine sends, not in what the two choose to offer. In the C<GET /secrets>
response captured from Podman 5.4.2 (API 1.41) in
F<t/fixtures/secrets_list.json>, each object carries C<ID>, C<CreatedAt>,
C<UpdatedAt>, C<Spec> and C<Version>, and the C<Spec> has C<Name>, C<Driver>
and C<Labels> but no C<Data> key whatsoever. The swagger agrees: it documents
C<SecretSpec.Data> as used to I<create> a secret and not returned by other
endpoints.

C<< $secret->spec->data >> therefore exists as an accessor -- the field is in
the definition -- and reads C<undef> on anything an engine sent back.

If you need to read a value back, a secret is the wrong storage -- put it in
a config, see L<API::Docker::API::Configs>.

=head2 The spec goes back as a whole

C<< $secret->spec >> is an L<API::Docker::Type::SecretSpec> object rather
than the raw HashRef the hand-written entity kept, so the idiom for an update
is C<< %{ $secret->spec->TO_JSON } >>: C<TO_JSON> renders the spec back into
the daemon's own spelling, which is what L</update> puts in the request body.

Why the whole spec and not the one key you changed:
L<API::Docker::API::Secrets/"update takes the current version, and it is
mandatory">.

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;

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

=head2 update

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

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

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

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

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

=head2 remove

    $secret->remove;

Remove the secret. The daemon answers 204 with no body, so this returns
nothing.

=head1 SEE ALSO

=over

=item * L<API::Docker::API::Secrets> - the operations these forward to

=item * L<API::Docker::Type::Secret> - the fields C<list> and C<inspect>
return

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

use API::Docker::Error::Stream;
use API::Docker::Error::Timeout;
use API::Docker::Error::Truncated;
use namespace::clean;


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

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

      }
      return $sock;
    }

    # Built before the connection is opened: a cert_path that names nothing,
    # or half a client certificate, is a configuration mistake and the caller
    # should hear about it as one rather than as a handshake failure.
    my %ssl = $self->_ssl_options($addr);

    $log->debugf("Connecting to TCP %s:%s over TLS (verification %s)",
      $addr, $port, $self->tls_insecure ? 'off' : 'on');
    my $sock = IO::Socket::SSL->new(
      PeerAddr => $addr,
      PeerPort => $port,
      Proto    => 'tcp',
      $timeout ? (Timeout => $timeout) : (),
      %ssl,
    );
    unless ($sock) {
      $self->_croak_connect_timeout($pending, $addr . ':' . $port . ' (TLS)')
        if $self->_connect_expired($timeout);

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

sub _ssl_options {
  my ($self, $addr) = @_;

  $self->_load_ssl;

  # SNI, sent whether or not the certificate is checked: a terminator serving
  # several names needs it to pick the right one, and that is true of an
  # unverified connection too.
  my %ssl = ( SSL_hostname => $addr );

  if ($self->tls_insecure) {
    # Everything below is off deliberately, and the attribute that got us here
    # says so in its name. Encryption without verification stops a passive
    # listener and nothing else: whoever answers the connection chooses the
    # certificate, so anyone able to redirect it reads and rewrites the
    # traffic -- credentials, image contents, container commands.
    $ssl{SSL_verify_mode}     = IO::Socket::SSL::SSL_VERIFY_NONE();
    $ssl{SSL_verifycn_scheme} = undef;
  }
  else {
    $ssl{SSL_verify_mode}     = IO::Socket::SSL::SSL_VERIFY_PEER();

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


# 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 {
  my ($self, $timeout) = @_;

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

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

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

  return $timeout > 0 ? $timeout : undef;
}

sub _read_timeout_value {
  my ($self, $timeout) = @_;
  return $self->_timeout_value('read_timeout', $timeout);
}

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

# semantics for free, which is the semantics these endpoints need (karr k52:
# the buffered frames arrive, and *then* the socket stalls -- a
# time-to-first-byte bound would never fire).
sub _apply_read_timeout {
  my ($self, $sock, $timeout) = @_;

  return unless $timeout;

  my $packed;
  if ($^O eq 'MSWin32' || $^O eq 'cygwin') {
    # Winsock takes a DWORD of milliseconds here rather than a struct timeval,
    # and reads a zero as "wait forever" -- so a sub-millisecond request is
    # rounded up instead of becoming the hang it asked to avoid. Reasoned from
    # the Winsock documentation and NOT measured: there is no Windows here.
    # What makes that safe to ship is the croak below -- a shape the platform
    # rejects is reported rather than ignored.
    my $ms = int($timeout * 1000 + 0.5);
    $ms = 1 if $ms < 1;
    $packed = pack('L', $ms);
  }
  else {
    # struct timeval: two native longs, seconds then microseconds. Measured on
    # 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) = @_;

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

# ---------------------------------------------------------------------------
# Reading, in one buffer regime
#
# Every byte of a response is taken off the handle by _pull and by nothing
# else, and every reader below is served out of the buffer _pull fills. That
# is not an optimisation, it is the only shape that works (karr k60).
#
# What forced it: perl's read() is fread-shaped. It loops until it has the
# LENGTH it was asked for or the stream ends -- it does not return what has
# arrived. Measured on an AF_UNIX socketpair whose peer writes 6 bytes, waits
# half a second, writes 6 more and closes: read($sock, $buf, 65536) came back
# with 12 after 0.90s, having waited for the close, while sysread came back
# with 6 in 0.00s. On the endpoints with neither a Content-Length nor chunked
# encoding -- attach, logs(follow), exec/start, all
# application/vnd.docker.raw-stream -- the reader asks for $READ_SIZE, so
# read() delivered nothing to an on_frame/on_chunk callback until 64K had
# piled up or the daemon hung up. On a stream that never ends it would deliver
# nothing at all. The POD promised those callbacks the bytes as they arrive,
# and that promise was not kept.
#
# Why it could not be fixed at the one site that had the bug: _read_head read

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


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

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

  # Looped so a 1xx informational response is read whole and passed by: it is a
  # complete head -- a status line and an optional field section closed by the
  # blank line -- with no body of its own, sent before the real response
  # (RFC 9110 section 15.2). Without this the reader took the 1xx status as the
  # response and then read the real response as its body. A 100 Continue is the
  # one an HTTP/1.1 client is most likely to be sent; 102 and 103 have the same
  # framing.
  while (1) {
    my $status_line = $self->_read_line($sock, $ctx);
    # A daemon that closed without answering at all, which is the one shape here
    # that was never silent and is left saying exactly what it always said.
    croak "No response from Docker daemon" unless defined $status_line;
    $self->_assert_status_line($ctx, $status_line);
    $status_line =~ s/\r?\n$//;

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

}

# The two ways the head ends early, and neither of them has a byte count to
# compare either (karr k73).
#
# karr k64 left the head out on the grounds that nothing in a status line or a
# header block announces its own length, so there was no announcement to hold
# a short one against. True, and beside the point: an announcement is not what
# is being checked here, any more than it is in _assert_chunk_header one level
# down. HTTP/1.1 frames the head by terminating every line, and the field
# section by an empty line that is mandatory even when there are no fields at
# all (RFC 9112 section 2.1), so "the stream ended where the terminator
# belongs" is a complete test on its own. It is the same question that reader
# already asks about a chunk header, asked of the head.
#
# What it was worth. A head cut short is not just a bogus status: the response
# is then read with whichever headers happened to arrive, and a cut landing
# before Content-Length or Transfer-Encoding leaves neither -- which is
# exactly the close-delimited branch of _read_body, where an EOF is the
# legitimate end and nothing looks wrong. Measured over a socketpair whose
# peer writes "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nX-Half"
# and closes: status 200, one header, an empty body, no complaint. The cuts

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

sub _assert_status_line {
  my ($self, $ctx, $line) = @_;

  # Only the unterminated half: a status line that never started at all is the
  # croak above, which says something better than this could.
  $self->_croak_truncated($ctx, phase => 'status-line',
    detail => 'the stream ended inside the status line, after '
      . length($line) . ' byte' . (length($line) == 1 ? '' : 's') . ' of one')
    unless $line =~ /\n\z/;

  # Terminated, and now: is it an HTTP status line at all? RFC 9112 section 4:
  # HTTP-version SP status-code SP [ reason-phrase ], with status-code exactly
  # three digits. A line that arrived whole but is not this shape -- a proxy's
  # plain-text banner, an ICY greeting, an HTML error page -- would otherwise
  # be split on whitespace in _read_head and its second word run through the
  # >= 400 comparison as the status. It is refused here rather than silently
  # misread, the same way a non-hexadecimal chunk size is one level down.
  my $stripped = $line =~ s/\r?\n\z//r;
  $self->_croak_truncated($ctx, phase => 'status-line',
    detail => "the status line '" . $stripped
      . "' is not a well-formed HTTP status line")
    unless $stripped =~ m{\AHTTP/[0-9]+\.[0-9]+ [0-9]{3}(?: .*)?\z};

  return;
}

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


  $self->_croak_truncated($ctx, phase => 'chunk-header',
    detail => 'the stream ended inside a chunk header, after '
      . length($line) . ' byte' . (length($line) == 1 ? '' : 's') . ' of one')
    unless $line =~ /\n\z/;

  # The line arrived whole and terminated, and its size is not a hexadecimal
  # number. Left to hex() that warns once and reads as 0 -- the terminating
  # zero chunk -- so a 200 whose framing is corrupt used to come back as an
  # empty body, the same body-shaped lie a truncation is. A chunk size is hex
  # digits, optionally followed by a ';' extension (RFC 9112 section 7.1.1),
  # which is read past and discarded; anything else is refused here rather than
  # silently misread by the caller. The returned size is parsed from the same
  # match, so hex() is called on nothing but hex digits and never has cause to
  # warn on a legal extension either.
  my ($size) = $line =~ /^([0-9A-Fa-f]+)(?:;.*)?\r?\n\z/;
  $self->_croak_truncated($ctx, phase => 'chunk-header',
    detail => "the chunk size line '" . ($line =~ s/\r?\n\z//r)
      . "' is not a hexadecimal number")
    unless defined $size;

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

sub _assert_chunk_terminator {
  my ($self, $ctx, $line) = @_;

  $self->_croak_truncated($ctx, phase => 'chunk-terminator',
    detail => 'the stream ended before the CRLF that terminates a chunk')
    unless defined $line && $line =~ /\n\z/;

  return;
}

# The declared body length, validated to be the digits RFC 9110 section 8.6
# requires before it is compared against or counted down (karr k113). The
# sibling of _assert_chunk_header's hex check: a Content-Length that is not a
# number -- 'abc', an empty value, a duplicated '11, 11', a leading space --
# left as it stood is run through `$len > 0`, which warns once ("isn't
# numeric") and reads as 0, so the body is taken to be empty and a response
# that had one comes back blank. Refused here rather than silently misread, so
# hex()'s sibling warning is never reached either.
sub _assert_content_length {
  my ($self, $ctx, $value) = @_;

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


=head1 SYNOPSIS

    package MyDockerClient;
    use Moo;

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

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

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

=head1 DESCRIPTION

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

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

the endpoints that never close are usable at all (L</"Streaming a response as
it arrives">)

=item * Request/response logging via L<Log::Any>

=item * Automatic connection management

=back

Consuming classes must provide C<host>, C<api_version>, C<tls>, C<cert_path>
and C<tls_insecure> attributes. The last three are read only by the C<tcp://>
branch of the socket builder, and only when TLS is asked for, but the contract
is stated once rather than probed for at connect time.

A C<unix://> connection is a local socket with no wire to protect and is never
encrypted; it ignores all three attributes, and L<API::Docker> refuses the
combination at construction rather than letting a request for an encrypted
transport be answered with an unencrypted one. A C<tcp://> connection is
B<plaintext unless C<< tls => 1 >>>, which is the whole of the difference --
see L</"TLS on a tcp:// connection">.

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


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

daemon that has nothing more to send and does not hang up leaves the client
blocked with no way out. That is not hypothetical: attaching to a container
that has B<already exited> answers, delivers the buffered frames and then
holds the connection open indefinitely on rootless Podman (karr k52), and
C</containers/{id}/stats> opened on a running container does not end when that
container exits on Docker -- it degrades into zero-filled readings and keeps
going (karr k59).

L</read_timeout> bounds that:

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

=head3 It is an idle timeout, not a deadline

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

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

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

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

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

=head3 What happens when it expires

The request croaks, on every path, with an L<API::Docker::Error::Timeout>. It
never returns a truncated response: a short body satisfies every return shape
this role promises and would be indistinguishable from a complete one. The
exception carries what did arrive -- C<< ->partial >> for a buffered request,
C<< ->summary >> for a streamed one -- so collecting what there is and then
stopping is an C<eval>:

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.

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.

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


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


# Constructor-side name resolution for `new`, and one of the two places
# unknown fields are collected. A key is taken as a Perl attribute name
# first and as a wire name second, because `new` assembles a REQUEST out of
# what a caller wrote and both spellings are the caller's to choose. The
# 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);

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

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:

    my $c = API::Docker::Type::ContainerInspectResponse->from_data({
      Id => 'x', State => 'exited' });     # the swagger says State is an object

    $c->state                       # undef
    $c->rejected_fields->{State}    # 'state'  -- sent, and refused
    $c->unknown_fields->{State}     # 'exited' -- kept as it arrived
    $c->TO_JSON->{State}            # 'exited' -- and written back out

Only L</from_data> fills it. L</new> is strict and croaks instead, so an
object a caller built has an empty one.

=head2 new

    my $hc = API::Docker::Type::HostConfig->new(privileged => 1);
    my $hc = API::Docker::Type::HostConfig->new(Privileged => 1);   # the same

Builds an object from data a B<caller> wrote, which is what a request payload
is. Keys are matched against the Perl attribute names first and the registry's
wire names second, so either spelling reaches the attribute; anything else is
kept in L</unknown_fields>, exactly as on the response path.

Two keys that resolve to one attribute -- C<privileged> and C<Privileged>
together -- are B<refused>. Which one won was decided by hash order and
nothing else, measured at nine zeroes and eleven ones over twenty
constructions of the same arguments. They are refused even where the two
values agree: values that happen to match are not the same thing as an
unambiguous call, and the caller should be shown the mistake rather than the
luck.

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

L</unknown_fields> under the name it arrived with.

Pairs after the hashref are attributes that did not come from the engine --
the C<client> a composed entity role declares in C<_entity_attributes> is the
one this distribution has. They are kept apart from C<$hashref> on purpose:
what the daemon sent and what we are adding are two different things, and a
daemon that one day sends a key of that name should not be able to overwrite
ours.

A decoded response is a map of wire names and nothing else, so that is the
only name space this reads. The Perl spelling is B<not> a second chance here,
and deliberately so: Docker's swagger gives 114 fields a wire name whose
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

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

    # or in passing
    $docker->images->using(connect_timeout => 2, read_timeout => 60)
      ->pull(fromImage => 'alpine');

=head1 DESCRIPTION

L<API::Docker::Role::HTTP/read_timeout> and
L<API::Docker::Role::HTTP/connect_timeout> are attributes of the client, so
they are set once and hold for every request it makes. That is the right
level for a rule and the wrong one for an exception: a client bounded at 30
seconds is no help to the one call that must give up after 2, and a client
bounded at 2 cannot pull an image.

C<using> is the exception. It returns a B<clone of the resource class>
carrying the options, and every request made through that clone is given
them:

    $docker->containers->using(read_timeout => 5)->list;

Only the two transport bounds may be carried. Everything else a request needs
-- query parameters, the body, a streaming callback -- is an argument of the

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

=head2 What has no clone of its own

B<The entity classes.> C<< $container->logs >> and its neighbours are
one-line delegations to the resource class, and they hold the container's own
daemon fields -- every one of them, verbatim -- rather than a call surface, so
a clone would have to copy a record whose shape is the daemon's. The bound
belongs where the request is built:

    $docker->containers->using(read_timeout => 5)->logs($container->id);

B<The client.> C<< $docker->using(...) >> would be a second client sharing
one connection state and one negotiated API version with the first. The
client already takes both bounds as constructor arguments, which is the level
it works at.

=head2 using

    my $bounded = $docker->containers->using(read_timeout => 5);

Returns a clone of the resource class that hands C<read_timeout> and
C<connect_timeout> to every request made through it. Takes those two options

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

# first method call, because that is the earliest point at which the caller
# can be told -- and because @Author::GETTY generates no compile-all author
# test that a dying module would fail. Measured on 2026-08-28: the bundle
# generates exactly xt/author/pod-syntax.t, which parses POD without loading
# anything, and xt/release/changes_has_content.t, which only reads Changes.
my $REFUSED =
  __PACKAGE__ . ' was removed in API::Docker 0.004 and this file is a stub'
  . ' with nothing in it: it ships only so that installing this'
  . ' release overwrites the working copy an earlier one left on'
  . ' disk. You have not hit a fault in the distribution. The'
  . ' secrets the daemon answers with are'
  . ' API::Docker::Type::Secret (secrets->list and'
  . ' secrets->inspect), with the field names the swagger\'s own in'
  . ' snake_case, and inspect, update, remove and version_index'
  . ' are unchanged on them, composed in from'
  . ' API::Docker::Role::Entity::Secret. This stub refuses';

# The croak below is what a caller normally hits. AUTOLOAD is for the one who
# swallowed it -- eval { require API::Docker::Secret } and then called a
# method anyway; the answer has to be the same one, not a bare "Can't locate
# object method". DESTROY is defined so it does not reach AUTOLOAD.
sub AUTOLOAD { croak $REFUSED }
sub DESTROY  { }

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


This class was never released. It existed between 0.003 and 0.004 in
development only, so the copy this stub overwrites is one a local install
put on disk -- which is the only reason the file is in the distribution at
all.

What to reach for instead:

=over

=item * L<API::Docker::Type::Secret> -- what C<< secrets->list >> and C<< secrets->inspect >> return

=item * L<API::Docker::Role::Entity::Secret> -- inspect, update, remove and version_index,
unchanged, composed into the above at load time

=back

Where this class mirrored the daemon's CamelCase verbatim, the generated
classes carry the swagger's own names in snake_case.
L<API::Docker::API::Secrets> documents the shape each method returns.



( run in 1.928 second using v1.01-cache-2.11-cpan-d01c6094234 )