API-Docker

 view release on metacpan or  search on metacpan

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

---
name: api-docker-doc-writer
description: "Write and maintain API::Docker POD in the house format (=attr, =method, =head1 SYNOPSIS, =seealso, woven by @Author::GETTY) and keep README.md in step. Documents the surface that exists; does not change code."
model: sonnet
allowed-tools: Read, Edit, Grep, Glob
briefing:
  skills:
    - api-docker-core
    - getty-perl-release-author-getty
    - getty-perl-core
---

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

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

## What this distribution's POD looks like

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

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

You are the api-docker-engine-worker for **API::Docker**.

Your lane is the boundary between this distribution and the Docker Engine: what the
daemon accepts, what it answers, and whether this client models that faithfully.
Everything that is a Perl or packaging question — Moo structure, the socket and chunked
reader, refactoring, `cpanfile`, dist plumbing — belongs to `api-docker-worker`; hand it

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

---
name: api-docker-release-checker
description: "Audit API::Docker before release — cpanfile matches what the code actually loads, $VERSION consistent across all lib modules, Changes current, dzil build clean, POD in sync with the public method surface. Reports; does not fix or rele...
model: sonnet
allowed-tools: Read, Bash, Glob, Grep
briefing:
  skills:
    - api-docker-core
    - getty-perl-release-author-getty
    - perl-release-dist-ini
    - kanban-issues-karr-cli
---

You are the api-docker-release-checker for **API::Docker**. Conventions from the skills
above are non-negotiable — apply silently.

Audit only — you report findings; the worker fixes them and the maintainer releases.
**Never** run `dzil release` or any upload.

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

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

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

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

  behind `is_live()`, mutations behind `can_write()` / `skip_unless_write()`, with
  `register_cleanup` for every resource created.
- **Never point a live test at a daemon the suite does not own.** Write tests create and
  destroy real containers, images, networks and volumes.

## Mechanics that decide whether a test is real

- **Pick the right level.** `test_docker` replaces `_request` wholesale, so anything
  below it — request line assembly, header sanitising, chunked reading, status handling,
  the NDJSON fallback — is invisible to a route-table test. Transport behavior is tested
  either by calling the private function directly (`t/images_push_auth.t` calls
  `_build_registry_auth_header`) or by capturing `local *API::Docker::_request`. State
  which level you are on before writing the file.
- **`API::Docker::Role::HTTP` currently has no coverage beyond `use_ok`.**
  `_read_chunked`, `_read_response` and the >=400 croak path are untested; a fake socket
  (an in-memory filehandle over a canned HTTP/1.1 response) is the way in. Treat that as
  a standing gap worth a ticket, not as something to fix inside an unrelated task.
- **Route keys are matched as exact strings first, then as regexes** (`m{^$route_path$}`
  in the fallback). A key containing `.`, `?` or `+` matches more than it looks like it
  does — anchor intent by making the exact key match, or escape deliberately.
- **Assert the request, not only the response.** A route handler receives
  `($method, $clean_path, %opts)`: assert on `params`, `body` and `headers` there when
  the point of the test is what the client sends. A test that only checks the mocked
  return value proves the fixture, not the code.
- **Decode exactly what the engine would receive.** The push-auth helper used to append
  the missing base64 padding before decoding and so passed with and without the defect
  it existed to catch. Never normalise the value under test on the way into the
  assertion.
- **Live and mock must both be able to pass, or the assertion is gated.** `test_docker`
  ignores the route table entirely under `API_DOCKER_TEST_HOST` — an assertion tied to
  fixture contents runs against a real daemon's data otherwise.

New fixtures are captured from a real daemon into `t/fixtures/*.json`, never hand-rolled
— and that includes wire formats. A test for the multiplexed log stream asserts against
bytes the engine actually produced, not against a frame header written from memory; the
Engine API reference above tells you what to expect, the socket tells you what is true.
Test filenames follow the existing flat, topical naming (`t/images.t`,
`t/images_push_auth.t`), one file per resource or per defect.

A test asserts intent: it must be able to fail when the logic changes. Reproduce a bug
before fixing it and leave the regression behind. Verify with `prove -lr t/`; a single
file with `prove -lv t/NN.t`.

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

description: "Writes and maintains the generated type model of API::Docker — classes under API::Docker::Type::*, the API::Docker::Type DSL and its attribute registry, maint/spec-drift-check.pl, and the swagger under spec/. Use for anything that tur...
model: inherit
allowed-tools: Read, Edit, Write, Bash, Glob, Grep
briefing:
  skills:
    - api-docker-type-model
    - api-docker-core
    - docker-engine-api
    - getty-perl-core
    - getty-perl-moo
    - getty-perl-release-author-getty
    - getty-git-commit-style
    - kanban-issues-karr-cli
---

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

Your lane is the type model: the swagger in `spec/`, the `API::Docker::Type` DSL and its
registry, the generated classes, and the drift checker that keeps them honest. The
transport, the resource classes and the tests belong to `api-docker-worker`,
`api-docker-engine-worker` and `api-docker-test-writer` — hand those over rather than

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

---
name: api-docker-worker
description: "Default API::Docker worker — the Perl side of this distribution: Moo classes and roles, the socket and HTTP/1.1 transport internals (chunked reading, header assembly, status handling), entity wrappers, refactoring, cpanfile and dist p...
model: inherit
allowed-tools: Read, Edit, Write, Bash, Glob, Grep
briefing:
  skills:
    - api-docker-core
    - getty-perl-core
    - getty-perl-moo
    - getty-perl-release-author-getty
    - perl-release-dist-ini
    - getty-git-commit-style
    - kanban-issues-karr-cli
---

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

Implement, refactor, debug, and test the Perl side of this distribution. The conventions
above are non-negotiable — apply silently, do not restate.

**Where your lane ends.** You own how this distribution is built: `Role::HTTP`'s socket
handling and chunked reader, Moo composition, the entity roles composed onto the
generated types, `cpanfile`, dist
plumbing. You do not own what the Docker Engine accepts or answers. If the task turns on
daemon semantics — a wire format, a query-parameter meaning, a response shape, registry
auth, API version gating — stop and hand it to `api-docker-engine-worker`, which is
briefed with the Engine API reference and you are not. Guessing at daemon behavior from
the existing Perl is exactly how a wrong assumption gets cemented; the current
`containers->logs` is the proof.

Coordinate via `karr`: pick tickets from the local board, record drift you find as new
tickets rather than expanding scope mid-change.

## Repo-specific notes — beyond the briefed skills

**This distribution has a consumer in the same workspace.**

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

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

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

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

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

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

  until it is `git add`-ed — while `prove -lr t/` stays green the whole time, because it
  reads `lib/` and `t/` directly rather than through the gathered file list. A `dzil
  build` failure that looks like a missing module, or a passing `prove` next to a
  failing `dzil test`, is this before anything else: check `git status` for an untracked
  file first.

## Perl specifics — reference, don't restate

Module loading, `$VERSION`, cpanfile pinning and house style: skills `getty-perl-core`,
`getty-perl-moo`. `[@Author::GETTY]`, POD weaving, `{{$NEXT}}`: skill
`getty-perl-release-author-getty`. dist.ini mechanics: `perl-release-dist-ini`. Commit
messages: `getty-git-commit-style`. Architecture and transport invariants:
`api-docker-core`. What the daemon itself does — wire formats, response shapes, filters,
registry auth: `docker-engine-api` (briefed only into the engine-worker and the
test-writer). Don't duplicate any of it here.

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

(measured on `Plugin.enabled`), and `TO_JSON` writes it back out as a JSON
boolean rather than the Perl truth value.

## X-Registry-Auth — padded base64url, always sent

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

## Transport behavior that's easy to get wrong

- **Buffers the whole response by default; streaming needs a callback.**
  Every request sends `Connection: close`. With none of `on_event`,
  `on_frame`, `on_chunk` given, `_request` reads the whole response before
  parsing, so `/build`, `/images/create`, `/push`, `/events`,
  `/containers/*/stats` and `logs(follow)` block until the daemon closes the
  connection — an unbounded `events` or `stats` call without one of those

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


Fixtures in `t/fixtures/*.json` are captured from a real daemon, so drift stays
detectable — do not hand-roll them. That was not always true until karr k101
(and its follow-up): all eight are now real captures -- see the header of
`t/type_fixture_passthrough.t` for which engine and API version backs each
one, including `containers_list.json`, `container_inspect.json` and
`volumes_list.json`, captured from a disposable container and volume created
and removed for the purpose once an earlier pass found neither engine
reachable from the recapturing machine holding one to read.

**A test helper that repairs its input cannot see the defect.** The push-auth
test used to compute and append the missing base64 padding before decoding, so
it passed both with and against the bug it existed to catch. Decode exactly
what the engine would receive.

Canonical run: `prove -lr t/` (recursive — plain `prove -l t/` would silently
skip a future subdirectory). Single file: `prove -lv t/images.t`.

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

null, because with no declared type there is no zero value to read it as, and
so does a null under a key the caller chose. Three shapes, three outcomes, on
purpose; the reasoning lives in `API::Docker::Role::Type`'s POD and
`t/type_fixture_passthrough.t` holds all three against the fixtures.

**`since` is documentation, never a check.** It records which API version
introduced a field, derived by diffing the specs in `spec/` against each
other — the swagger itself carries no per-field version. Nothing is
validated, warned about or dropped at runtime. Podman serves fields its
announced version does not promise and refuses ones it does; we are not the
authority on what an engine can do.

## Where the values come from

`spec/` holds the swagger verbatim as Docker publishes it, so a `curl | diff`
still checks out. Generate against the newest version present and keep the
older ones for the diff that produces `since`.

    https://docs.docker.com/reference/api/engine/version/v1.51.yaml

Parse it with `YAML::XS`, not `YAML::PP`: Docker's `example:` blocks are

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

```

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

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

## Registry auth

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

The header is mandatory on **every** push, anonymous included; the anonymous
form is the encoding of `{}`, which is `e30=` — three characters and one pad,
the shortest case and the one that proves padding matters. Payload keys:
`username`, `password`, `serveraddress`, or `identitytoken`.

`/build` uses a different header for the same job: `X-Registry-Config`,
base64url of a map from registry hostname to auth object, because a build may
pull from several registries.

## Bodies and paths

`POST /build` is the odd one: the request body is the **tar build context**
(`Content-Type: application/x-tar`), and every option — `t`, `dockerfile`,
`buildargs`, `target`, `platform` — rides in the query string. `buildargs` and
`labels` are themselves JSON-encoded strings inside that query.

Container endpoints accept a name or any unambiguous ID prefix. Image

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

- **Section long files with a figlet banner** as a comment block. Pick from `standard`, `slant`, `small`, `banner`. Where figlet is unavailable or the file is short, a `#### <Name>` rule does the job.
- **Commented-out debug lines stay** (`#use DDP; p($res);`). They mark where debugging was needed before — deleting them as dead code removes a warning sign, and sometimes the precaution it guards.

## cpanfile

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

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

---
name: getty-perl-release-author-getty
description: Load when a dist.ini contains [@Author::GETTY] — bundle options, POD conventions (=attr/=method/=opt), next-version semantics, the dzil release workflow.
user-invocable: false
allowed-tools: Read, Grep
model: sonnet
---

When working with `[@Author::GETTY]` plugin bundle:

## Required Metadata

```ini
name = Distribution-Name
author = Name <email>
license = Perl_5
copyright_holder = Copyright Owner
```

## The LICENSE file

The bundle expects `LICENSE` to be a **committed file in the repository**. It
removes `[License]` from `@Basic` and adds `[LicenseFile]`, which aborts the
build when that file is missing or no longer matches the distribution's
`license`, `copyright_holder` and `copyright_year`:

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

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

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

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

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

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

---
name: perl-release-dist-ini
description: "Load when reading, editing or debugging any dist.ini — Perl distributions and plugin bundles (Author::GETTY, Author::ETHER, ...), version config, plugins, prereqs."
user-invocable: false
allowed-tools: Read, Grep
model: sonnet
---

Generic Dist::Zilla dist.ini reference — applies to any distribution regardless of author bundle.
For `[@Author::GETTY]`-specific conventions (next-version semantics, POD commands, bundle options), the getty-perl-release-author-getty skill applies additionally.

When analyzing dist.ini:

## Section Detection

```ini
[@Author::PLUGIN_BUNDLE]   # Plugin bundle
[Some::Plugin]             # Individual plugin
```

## Common dist.ini Sections

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

## Plugin Loading Order

1. `[GatherDir]` - Collects files
2. `[PruneCruft]` - Removes unwanted files

CLAUDE.md  view on Meta::CPAN

# CLAUDE.md

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

## The 12 Rules

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

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

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

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

CLAUDE.md  view on Meta::CPAN

   TL;DR. The `api-docker-*` agents get it force-loaded — see
   [Delegation](#delegation).

7. **`use Module;` to load modules.** Only use `require` when there's a
   real runtime reason (lazy plugin loading, optional deps), not just to
   defer cost.

8. **`->instance` for `MooX::Singleton` / `MooseX::Singleton` classes.**
   `->new` for everything else.

9. **Never copy `$VERSION` from a Getty-authored repo into a cpanfile.**
   The repo version is the *next* unreleased version. Check
   `cpanm --info` for the actual released version when pinning.

10. **Pin every Getty-authored dependency** to its latest released CPAN
    version in `cpanfile`.

11. **The version in `lib/API/Docker.pm` is the NEXT release.** What's
    currently on CPAN is the previous tag. `dzil release` bumps the
    version automatically — never bump it by hand before a release. The
    same literal is repeated in every `lib/**/*.pm` file and must
    stay in sync -- 45 of them as of 0.004, and the generated
    `API::Docker::Type::*` classes make that number move. Count them, do
    not trust a number written down here.

CLAUDE.md  view on Meta::CPAN

the source of truth, not this file.

## Layout

```
lib/API/Docker.pm                       # main client, version negotiation
lib/API/Docker/Role/HTTP.pm             # HTTP/1.1 transport (unix:// + tcp://, TLS)
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

CLAUDE.md  view on Meta::CPAN

`ndjson` body.

## Delegation

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

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

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

`api-docker-type-writer` is briefed with `api-docker-type-model`, which carries the

Changes  view on Meta::CPAN

    percent-escaping, so a name or tag typed as characters (`ü`, `中`) goes
    out as valid UTF-8.
  - A request path outside the RFC 3986 origin-form character set is refused
    before it reaches the daemon, closing a request-line injection through a
    container name or image reference.
  - An ArrayRef query parameter expands into one repeated `k=v` pair per
    element (`names=a&names=b`), which some endpoints require.
  - A bare JSON scalar body (`null`, `true`, a number, a quoted string) is
    decoded rather than handed back as raw bytes; `raw` and `ndjson` return
    `''` and `[]` for a zero-byte body instead of `undef`.
  - Registry credentials reach `images->pull` (`auth`, sent as
    `X-Registry-Auth`) and `images->build` (`registry_config`, sent as
    `X-Registry-Config`), sent only when given. An already-base64 auth value
    in the standard alphabet is respelled URL-safe, which the engine
    requires.
  - `images->pull` no longer appends a default `tag` onto a reference that
    already carries a `:tag` or `@digest`.
  - New `images->get`, `->get_all` and `->load`: the image tar roundtrip in
    and out of a daemon without a registry. New `images->commit`
    (POST /commit) and `images->build_prune` (POST /build/prune, the
    BuildKit cache, a different store from the dangling images
    `images->prune` deletes).
  - New container endpoints: `get_archive`, `put_archive`, `stat_archive`

Changes  view on Meta::CPAN

    a failure inside a 200 response, instead of handing the error object back
    as a reading.
  - New `API::Docker::API::Plugins` (`$docker->plugins`): `list`,
    `privileges`, `install`, `inspect`, `remove`, `enable`, `disable`,
    `upgrade`, `push` and `configure`. Needs a real Docker daemon; Podman
    serves no `/plugins`.
  - New `API::Docker::API::Secrets` and `API::Docker::API::Configs`: `list`,
    `create`, `inspect`, `update` and `remove`. `Data` is base64-encoded for
    the caller; `update` takes the current `Version.Index` as a mandatory
    concurrency token.
  - New `API::Docker::API::System::auth` (POST /auth): check registry
    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

Changes  view on Meta::CPAN

    sibling) were undeclared and are now required, as is `Exporter`
    under `on test` for the mock helper. Nothing else in the tree loads
    an undeclared module: `SOCK_STREAM` in the HTTP role comes from
    IO::Socket, which IO::Socket::UNIX and IO::Socket::INET both
    re-export, and `Path::Tiny` appears in `lib/` only inside the
    API::Docker::API::Images SYNOPSIS, so it stays a test dependency.
  - Fix every image push failing with a 400. The X-Registry-Auth header
    was encoded as base64url with the padding stripped; the engine
    decodes it with Go's `base64.URLEncoding`, which requires padding
    and answers `failed to parse "X-Registry-Auth" header ... unexpected
    EOF` without it. That hit authenticated and anonymous pushes alike
    -- the anonymous payload is `{}`, which encodes to three characters
    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

Changes  view on Meta::CPAN

    (`currentContext`, `~/.docker/contexts/meta/*/meta.json`) are never
    consulted, unlike the `docker` CLI, docker-java or Testcontainers.

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

0.001     2026-04-29 00:40:43Z
    - Initial release as API::Docker
    - Docker Engine API client with Unix socket and TCP support
    - Auto-negotiate API version from daemon
    - Container, Image, Network, Volume, System, and Exec APIs
    - Pure Perl implementation with minimal dependencies (no LWP)
    - HTTP/1.1 transport with chunked transfer encoding support

LICENSE  view on Meta::CPAN

 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

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

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

  To protect your rights, we need to make restrictions that forbid

LICENSE  view on Meta::CPAN


  For example, if you distribute copies of a such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the
source code.  And you must tell them their rights.

  We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

  Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software.  If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

  The precise terms and conditions for copying, distribution and
modification follow.

                    GNU GENERAL PUBLIC LICENSE
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

  0. This License Agreement applies to any program or other work which
contains a notice placed by the copyright holder saying it may be
distributed under the terms of this General Public License.  The

LICENSE  view on Meta::CPAN


Each version is given a distinguishing version number.  If the Program
specifies a version number of the license which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation.  If the Program does not specify a version number of
the license, you may choose any version ever published by the Free Software
Foundation.

  8. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission.  For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this.  Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

                            NO WARRANTY

  9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN

LICENSE  view on Meta::CPAN

possible use to humanity, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.

  To do so, attach the following notices to the program.  It is safest to
attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) 19yy  <name of author>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 1, or (at your option)
    any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, see <https://www.gnu.org/licenses/>.


Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

    Gnomovision version 69, Copyright (C) 19xx name of author
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the
appropriate parts of the General Public License.  Of course, the
commands you use may be called something other than `show w' and `show
c'; they could even be mouse-clicks or menu items--whatever suits your
program.

MANIFEST  view on Meta::CPAN

.claude/rules/api-docker-rules.md
.claude/settings.json
.claude/skills/api-docker-core/SKILL.md
.claude/skills/api-docker-type-model/SKILL.md
.claude/skills/api-docker-type-model/references/dsl.md
.claude/skills/api-docker-type-model/references/types.md
.claude/skills/docker-engine-api/SKILL.md
.claude/skills/getty-git-commit-style/SKILL.md
.claude/skills/getty-perl-core/SKILL.md
.claude/skills/getty-perl-moo/SKILL.md
.claude/skills/getty-perl-release-author-getty/SKILL.md
.claude/skills/kanban-issues-karr-cli/SKILL.md
.claude/skills/perl-release-dist-ini/SKILL.md
.gitignore
CLAUDE.md
Changes
LICENSE
MANIFEST
META.json
META.yml
Makefile.PL

MANIFEST  view on Meta::CPAN

lib/API/Docker/Type/TaskSpec/Resources.pm
lib/API/Docker/Type/TaskSpec/RestartPolicy.pm
lib/API/Docker/Type/TaskStatus.pm
lib/API/Docker/Type/ThrottleDevice.pm
lib/API/Docker/Type/Topology.pm
lib/API/Docker/Type/Volume.pm
lib/API/Docker/Type/Volume/UsageData.pm
lib/API/Docker/Type/VolumeCreateOptions.pm
lib/API/Docker/Type/VolumeListResponse.pm
lib/API/Docker/Volume.pm
t/author-pod-syntax.t
t/basic.t
t/connect_timeout.t
t/containers.t
t/containers_endpoints.t
t/containers_stats_error.t
t/dist_source.t
t/distribution.t
t/entity_container.t
t/entity_roles.t
t/exec.t

MANIFEST  view on Meta::CPAN

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
t/json_body_booleans.t
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
t/tls.t
t/tls_read.t
t/transport_shape.t
t/truncated_response.t
t/type.t
t/type_fixture_passthrough.t
t/type_model.t
t/using.t
t/version.t

META.json  view on Meta::CPAN

{
   "abstract" : "Perl client for the Docker Engine API",
   "author" : [
      "Torsten Raudssus <getty@cpan.org>"
   ],
   "dynamic_config" : 0,
   "generated_by" : "Dist::Zilla version 6.037, CPAN::Meta::Converter version 2.150010",
   "license" : [
      "perl_5"
   ],
   "meta-spec" : {
      "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
      "version" : 2

META.json  view on Meta::CPAN

         }
      ],
      "zilla" : {
         "class" : "Dist::Zilla::Dist::Builder",
         "config" : {
            "is_trial" : 0
         },
         "version" : "6.037"
      }
   },
   "x_authority" : "cpan:GETTY",
   "x_generated_by_perl" : "v5.40.1",
   "x_serialization_backend" : "Cpanel::JSON::XS version 4.43",
   "x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later"
}

META.yml  view on Meta::CPAN

---
abstract: 'Perl client for the Docker Engine API'
author:
  - 'Torsten Raudssus <getty@cpan.org>'
build_requires:
  Exporter: '0'
  Path::Tiny: '0'
  Test::More: '0'
configure_requires:
  ExtUtils::MakeMaker: '0'
dynamic_config: 0
generated_by: 'Dist::Zilla version 6.037, CPAN::Meta::Converter version 2.150010'
license: perl

META.yml  view on Meta::CPAN

      version: '6.037'
    -
      class: Dist::Zilla::Plugin::FinderCode
      name: '@Author::GETTY/MetaProvides::Package/AUTOVIV/:InstallModulesPM'
      version: '6.037'
  zilla:
    class: Dist::Zilla::Dist::Builder
    config:
      is_trial: 0
    version: '6.037'
x_authority: cpan:GETTY
x_generated_by_perl: v5.40.1
x_serialization_backend: 'YAML::Tiny version 1.76'
x_spdx_expression: 'Artistic-1.0-Perl OR GPL-1.0-or-later'

dist.ini  view on Meta::CPAN

name    = API-Docker
author  = Torsten Raudssus <getty@cpan.org>
license = Perl_5
copyright_holder = Torsten Raudssus <torsten@raudssus.de> L<https://raudssus.de/>
copyright_year   = 2026

[@Author::GETTY]

; spec/ (raw swagger) and maint/ (generator + drift scripts) are
; maintainer-only inputs — nothing under lib/ needs them at runtime.
[PruneFiles]
match = ^spec/

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

L<API::Docker::Role::HTTP/"TLS on a tcp:// connection"> for the whole of the
policy.

=head2 cert_path

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

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

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

=head2 tls_insecure

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

  required => 1,
  weak_ref => 1,
);


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

sub inspect {
  my ($self, $name, %opts) = @_;
  croak __PACKAGE__ . '->inspect requires an image reference' unless $name;

  return $self->client->get("/distribution/$name/json",
    $self->_auth_headers(\%opts),
    %{ $self->_request_options },
    (exists $opts{response} ? (response => $opts{response}) : ()));
}


# The engine's own "I have no such route" 404 versus the registry's "I do not
# have that reference" 404. Measured on Podman 5.4.2 (API 1.41), which has no
# route: 'Path /v1.41/distribution/nginx:latest/json is not supported'. That
# '1.41' is the negotiated API version echoed back from the request path, not
# a fixed string -- it moves with negotiation, which is why the regex below

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


=head1 SYNOPSIS

    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

This module provides access to the Docker distribution endpoint
(C<GET /distribution/{name}/json>), which asks a I<registry> for the manifest
descriptor of an image reference without pulling the image.

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

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

C<inspect> convention the other resource classes follow, because there is no
C<API::Docker::Distribution> entity class to wrap it in.

=head2 client

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

=head2 inspect

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

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

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

B<A missing reference croaks.> This method is the endpoint, so it inherits
the transport's rule that any status at or above 400 is an error, and the

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


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

Options:

=over

=item * C<auth> - Registry credentials, in any shape
L<API::Docker::API::Images/push> accepts them: a HashRef of C<username> /
C<password> / C<serveraddress> / C<identitytoken>, or a pre-encoded base64
string. Sent as C<X-Registry-Auth>. Unlike C<push>, which always sends the
header, it is omitted entirely without this option -- the lookup is then
anonymous, which is what a public image needs

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

=back

=head2 exists

    if ($distribution->exists('myrepo/app:1.0', auth => $auth)) { ... }

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

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

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

  $params{networkmode} = $opts{networkmode} if defined $opts{networkmode};
  $params{platform}   = $opts{platform}   if defined $opts{platform};
  $params{target}     = $opts{target}     if defined $opts{target};

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

  my $raw = ref $context eq 'SCALAR' ? $$context : $context;

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

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

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

  return 1 if $ref =~ /\@/;
  my ($last_segment) = $ref =~ m{([^/]*)\z};
  return $last_segment =~ /:/ ? 1 : 0;
}

# Only when credentials were given: an anonymous pull needs no header, and the
# engine reads X-Registry-Auth off /images/create only to reach a private
# registry. This is the plugins/distribution policy, not push's always-send --
# push must send even the anonymous {} because the engine rejects a push with
# no header at all.
sub _auth_headers {
  my ($self, $opts) = @_;
  return () unless defined $opts->{auth};
  return (headers => { 'X-Registry-Auth' => $self->_registry_auth_header($opts->{auth}) });
}

sub pull {
  my ($self, %opts) = @_;
  croak "fromImage required" unless $opts{fromImage};
  my %params;
  $params{fromImage} = $opts{fromImage};
  if (defined $opts{tag}) {
    $params{tag} = $opts{tag};
  }
  elsif (!$self->_reference_has_tag_or_digest($opts{fromImage})) {
    $params{tag} = 'latest';
  }
  return $self->client->post('/images/create', undef,
    params => \%params,
    $self->_auth_headers(\%opts),
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $opts{on_event} ) : ( ndjson => 1 ),
  );
}


sub inspect {
  my ($self, $name) = @_;
  croak "Image name required" unless $name;
  my $result = $self->client->get("/images/$name/json",

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

  );
}


sub push {
  my ($self, $name, %opts) = @_;
  croak "Image name required" unless $name;
  my %params;
  $params{tag} = $opts{tag} if defined $opts{tag};

  my $auth_header = $self->_registry_auth_header($opts{auth});

  return $self->client->post(
    "/images/$name/push",
    undef,
    params  => \%params,
    headers => { 'X-Registry-Auth' => $auth_header },
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $opts{on_event} ) : ( ndjson => 1 ),
  );
}


sub tag {
  my ($self, $name, %opts) = @_;
  croak "Image name required" unless $name;
  my %params;

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


sub commit {
  my ($self, %opts) = @_;
  croak "container required" unless $opts{container};

  my %params;
  $params{container} = $opts{container};
  $params{repo}      = $opts{repo}    if defined $opts{repo};
  $params{tag}       = $opts{tag}     if defined $opts{tag};
  $params{comment}   = $opts{comment} if defined $opts{comment};
  $params{author}    = $opts{author}  if defined $opts{author};
  $params{pause}     = $opts{pause} ? 1 : 0 if defined $opts{pause};

  # `changes` is a repeated query parameter on the wire, but the engine parses
  # each value as a Dockerfile snippet and a snippet may span lines, so one
  # newline-joined value carries a list just as well. Measured against Podman
  # 5.4.2: changes=LABEL%20a%3Db%0AEXPOSE%208080 and two separate changes=
  # pairs produce the same image. The joined form is used because it fits the
  # transport's one-value-per-key params encoder.
  if (defined $opts{changes}) {
    $params{changes} = ref $opts{changes} eq 'ARRAY'

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

L<API::Docker::Type::ImageConfig> the image runs containers with --
C<< ->cmd >>, C<< ->env >>, C<< ->entrypoint >>, C<< ->exposed_ports >> and
the rest.

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

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

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

=back

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

=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

=head2 Progress as it arrives

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


Options:

=over

=item * C<fromImage> - Image name to pull (required)

=item * C<tag> - Tag to pull. Defaulted to C<latest> only when C<fromImage>
carries no tag or digest of its own; see above

=item * C<auth> - Registry credentials for pulling from a private registry,
sent as C<X-Registry-Auth>. A HashRef of the usual keys (C<username>,
C<password>, C<serveraddress>, or C<identitytoken>) or a pre-encoded base64
string, exactly as L</push> takes it. Unlike C<push>, the header is sent
B<only> when C<auth> is given -- an anonymous pull carries none, which the
engine reads as the anonymous case. See L<API::Docker::Role::RegistryAuth>

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

=back

=head2 inspect

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


=head2 history

    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
the one that must not be reported as a success.

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

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

The Docker Engine requires an C<X-Registry-Auth> header on every push,
even for anonymous attempts; the header is always sent. Pass C<auth> as
a hashref of credentials (typical keys: C<username>, C<password>,
C<serveraddress>, or C<identitytoken>), or as a pre-encoded base64 string.
Without C<auth> the header carries an empty JSON object.

Options:

=over

=item * C<tag> - Tag to push

=item * C<auth> - Registry credentials, as above

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

=back

=head2 tag

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

=over

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

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

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

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

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

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

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

=item * C<config> - HashRef of container configuration to override on the new
image (C<Cmd>, C<Env>, C<Labels>, C<ExposedPorts>, ...), sent as the request
body. Measured against Podman 5.4.2: C<Cmd> replaces the container's, C<Env>

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

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

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

sub _privileges_body {
  my ($self, $method, $remote, %opts) = @_;

  return $self->privileges($remote, %opts) if $opts{accept_privileges};

  my $privileges = $opts{privileges};
  croak __PACKAGE__ . '->' . $method . ' requires privileges: fetch them with '
    . '->privileges(' . $remote . ') and pass them back as privileges => '

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

  return $self->_wrap_list('API::Docker::Type::Plugin', $result // []);
}


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

  my $result = $self->client->get('/plugins/privileges',
    params => { remote => $remote },
    $self->_auth_headers(\%opts),
    %{ $self->_request_options },
  );

  # A plugin that demands nothing answers a bare `null`: computePrivileges
  # builds its result with `var privileges types.PluginPrivileges` and
  # appends only what the config asks for, so a nil Go slice reaches the
  # wire. The transport decodes that to undef, which no caller can iterate
  # and which accept_privileges => 1 would post straight back to
  # /plugins/pull as a JSON null. Normalised to the empty list it means.
  return [] unless ref $result eq 'ARRAY';

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


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

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

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


sub inspect {
  my ($self, $name) = @_;
  croak __PACKAGE__ . '->inspect plugin name required' unless $name;
  return $self->_wrap('API::Docker::Type::Plugin',

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


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

  my $remote = $opts{remote} // $name;
  my $privileges = $self->_privileges_body('upgrade', $remote, %opts);

  return $self->client->post("/plugins/$name/upgrade", $privileges,
    params => { remote => $remote },
    $self->_auth_headers(\%opts),
    %{ $self->_request_options },
    exists $opts{on_event} ? ( on_event => $opts{on_event} ) : ( ndjson => 1 ),
  );
}


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


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

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

A plugin that demands nothing answers with an empty ArrayRef.

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

Options:

=over

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

=back

=head2 install

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

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

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

=item * C<accept_privileges> - Fetch the privileges and grant them, in one
call. A blanket grant: use it where the call site is allowed to trust the
plugin, and know that it reads as consent to whatever the plugin demands

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

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

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

=back

Returns an ArrayRef of progress events, one per object in the engine's
newline-delimited JSON stream, C<[]> when the engine sent no progress
at all.

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


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

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

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

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

=item * C<on_event> - CodeRef called with each progress event as it arrives.
The return value is then the summary HashRef; see
L</"Progress as it arrives">

=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

=item * C<auth> - Registry credentials; HashRef of C<username> / C<password> /
C<serveraddress> / C<identitytoken>, or a pre-encoded base64 string. Sent as
C<X-Registry-Auth>

=item * C<on_event> - CodeRef called with each progress event as it arrives --
layer by layer, rather than the whole upload in one silence. The return value
is then the summary HashRef; see L</"Progress as it arrives">

=back

Unlike L<API::Docker::API::Images/push>, which sends C<X-Registry-Auth> on
every call because the engine rejects an image push without it, this sends
the header only when C<auth> is given: the plugin router decodes the header
and discards a decoding failure, so an anonymous push needs no header. The
Engine API reference documents no header on this endpoint at all; the daemon
reads it.

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

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

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



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


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

  # Two spellings of the same AuthConfig. The credential keys and the name
  # `auth` cannot collide -- the engine's AuthConfig has no `auth` field --
  # so both are accepted, but not at once: which one wins would be a silent
  # choice about someone's credentials.
  my @flat = grep { defined $opts{$_} }
    qw( username password email serveraddress identitytoken );
  croak __PACKAGE__ . '->auth takes either auth => $config or the credential '
    . 'keys themselves, not both (' . join(', ', @flat) . ' given beside auth)'
    if defined $opts{auth} && @flat;

  my $config = defined $opts{auth}
    ? $self->_registry_auth_config($opts{auth})
    : { map { $_ => $opts{$_} } @flat };

  # Stricter than the engine, deliberately. An empty AuthConfig is a valid
  # body -- Podman answers it 500 'getting username and password: cannot
  # prompt for username without stdin' -- but a credential check with no
  # credentials in it is a caller bug, and answering it with the engine's
  # message would hide that.
  croak __PACKAGE__ . '->auth requires credentials: pass username/password, '
    . 'identitytoken, or auth => $config' unless keys %$config;

  return $self->client->post('/auth', $config,
    %{ $self->_request_options },
    (exists $opts{response} ? (response => $opts{response}) : ()));
}



1;

__END__

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


    # Monitor events
    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.

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

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

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

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

    my %res;
    eval { $docker->system->auth(auth => $auth, response => \%res); 1 }
      or do {
        die "registry rejected the credentials" if $res{status} == 401;
        die "could not reach the registry: $@";
      };

Options -- the AuthConfig keys the engine defines, all optional
individually, but at least one is required:

=over

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


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

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

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

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

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

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

=back

Passing neither C<auth> nor any credential key croaks before the request is
made.

=head3 What Podman answers

Measured against the rootless Podman socket (5.4.2, API 1.41): the endpoint
exists, but a failed check is B<500 Internal Server Error>, not Docker's 401,
and the message is the registry's own text wrapped by Podman --
C<< {"message":"login attempt to 127.0.0.1:1 failed with status: ..."} >>.
An empty AuthConfig answers
C<< {"message":"login attempt to  failed with status: getting username and

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


# A removed class, kept as a stub on purpose (karr k92). A module that
# disappears from a distribution does not disappear from the disks it was
# installed on: the old file stays behind and keeps loading, so deleting it
# here would leave a working API::Docker::Config shadowing this release for
# everyone who ever installed the last one. Shipping a file overwrites it;
# shipping nothing does not.
#
# It refuses instead of working, and it refuses at load rather than at the
# 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'
  . ' configs the daemon answers with are'
  . ' API::Docker::Type::Config (configs->list and'
  . ' configs->inspect), with the field names the swagger\'s own in'
  . ' snake_case, and inspect, update, remove, version_index and'

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


# A removed class, kept as a stub on purpose (karr k92). A module that
# disappears from a distribution does not disappear from the disks it was
# installed on: the old file stays behind and keeps loading, so deleting it
# here would leave a working API::Docker::Container shadowing this release for
# everyone who ever installed the last one. Shipping a file overwrites it;
# shipping nothing does not.
#
# It refuses instead of working, and it refuses at load rather than at the
# 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'
  . ' containers the daemon answers with are'
  . ' API::Docker::Type::ContainerSummary (containers->list) and'
  . ' API::Docker::Type::ContainerInspectResponse'
  . ' (containers->inspect), with the field names the swagger\'s'

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


# A removed class, kept as a stub on purpose (karr k92). A module that
# disappears from a distribution does not disappear from the disks it was
# installed on: the old file stays behind and keeps loading, so deleting it
# here would leave a working API::Docker::Image shadowing this release for
# everyone who ever installed the last one. Shipping a file overwrites it;
# shipping nothing does not.
#
# It refuses instead of working, and it refuses at load rather than at the
# 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'
  . ' images the daemon answers with are'
  . ' API::Docker::Type::ImageSummary (images->list) and'
  . ' API::Docker::Type::ImageInspect (images->inspect), with the'
  . ' field names the swagger\'s own in snake_case, and inspect,'

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


# A removed class, kept as a stub on purpose (karr k92). A module that
# disappears from a distribution does not disappear from the disks it was
# installed on: the old file stays behind and keeps loading, so deleting it
# here would leave a working API::Docker::Network shadowing this release for
# everyone who ever installed the last one. Shipping a file overwrites it;
# shipping nothing does not.
#
# It refuses instead of working, and it refuses at load rather than at the
# 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'
  . ' networks the daemon answers with are'
  . ' API::Docker::Type::Network (networks->list and'
  . ' networks->inspect), with the field names the swagger\'s own'
  . ' in snake_case, and inspect, remove, connect and disconnect'

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


# A removed class, kept as a stub on purpose (karr k92). A module that
# disappears from a distribution does not disappear from the disks it was
# installed on: the old file stays behind and keeps loading, so deleting it
# here would leave a working API::Docker::Plugin shadowing this release for
# everyone who ever installed the last one. Shipping a file overwrites it;
# shipping nothing does not.
#
# It refuses instead of working, and it refuses at load rather than at the
# 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'
  . ' plugins the daemon answers with are'
  . ' API::Docker::Type::Plugin (plugins->list and'
  . ' plugins->inspect), with the field names the swagger\'s own in'
  . ' snake_case, and inspect, enable, disable, remove, configure,'



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