API-Docker

 view release on metacpan or  search on metacpan

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

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

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

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

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

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

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

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

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

## Mechanics that decide whether a test is real

- **Pick the right level.** `test_docker` replaces `_request` wholesale, so anything
  below it — request line assembly, header sanitising, chunked reading, status handling,
  the NDJSON fallback — is invisible to a route-table test. Transport behavior is tested
  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.

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

          └─ _wrap / _wrap_list  → $class->from_data($data, client => ...)
                                    on an API::Docker::Type::* class, with
                                    API::Docker::Role::Entity::* composed
                                    onto that class at load time
```

**`_request($method, $path, %opts)` is the only way out of the process.** Every
resource method goes through it (usually via `get`/`post`/`put`/
`delete_request`). A new endpoint never opens its own socket.

Options: `body` (JSON-encoded), `raw_body` + `content_type` (tarballs for
`/build`), `params` (query string), `headers` (extra request headers),
`on_event`/`on_frame`/`on_chunk` (streaming callbacks, see below).

`_request` prefixes the path with `/v$api_version`. `around _request` in
`API::Docker` triggers `negotiate_version` on the first call that is not
`/version` — so a mock that replaces `_request` must strip the `/vX.YZ` prefix
itself, which `Test::API::Docker::Mock` does.

## Invariants

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

  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
  `API::Docker::Type::*` alongside the resource-specific
  `API::Docker::Role::Entity::*` role. `API::Docker->new->images->list`
  leaves every returned entity with `client => undef`, and the next
  `$image->remove` dies on an undefined invocant. The client must stay in a
  live variable — in library code, in examples, and in tests.
- **Query-string booleans are normalised to `1`/`0`** (`$opts{all} ? 1 : 0`);
  **JSON-body booleans are `\1`/`\0`** (see `Exec::start`), because the engine
  type-checks the body but not the query string.
- **A `params` value that is a hashref is JSON-encoded automatically**, but
  `filters` specifically goes through `API::Docker::Role::Filters`, which
  normalises it into the map-of-string-to-array-of-string shape the engine
  wants (wraps a bare scalar in an array, stringifies numbers, turns a JSON
  or `\1`/`\0` boolean into `'true'`/`'false'`) and croaks on anything else —
  another ref, `undef`, an empty string. Pass
  `filters => { dangling => ['true'] }` and let the role do the rest;
  encoding it by hand double-encodes it.
- **Extra headers go through `headers =>`**, which strips CR/LF. Never
  concatenate a header into the request string.
- `_uri_encode` deliberately leaves `/` and `:` raw so image names survive in
  the path (`/images/library/nginx:1.25/push`).
- `sub push` and `sub kill` shadow Perl builtins inside their packages — that
  is why `namespace::clean` is loaded; always call them as methods.

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

`API::Docker::Type::*` classes at load time
(`Moo::Role->apply_roles_to_package`), because the generated files must
match `maint/spec-to-type.pl`'s output byte for byte (`t/spec_to_type.t`
enforces it) — nothing hand-written can live in them. See
`API::Docker::Role::Entity` for why a role composed onto the class, and not
a wrapper class holding one.

Fields carry the swagger's own names in snake_case — `$container->state`,
not `$container->State` — and the model does normalise: a field declared
`Bool` comes back `1`/`0` regardless of what the daemon actually sent
(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 `=`

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

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

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


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

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

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

    ID             -> id
    NanoCpus       -> nano_cpus

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

## Serialisation, both directions

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

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

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

## Booleans

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

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

parameter took effect can be wrong without any error.

## Response shapes

- **204 No Content** is the success case for `start`, `stop`, `kill`, `pause`,
  `remove` and friends. There is no body to decode.
- **304 Not Modified** means the container was already in the requested state —
  starting a running container, stopping a stopped one. It is *not* an error,
  and a client that only special-cases `>= 400` will hand back an empty result
  here. Decide explicitly whether that is success.
- **Errors** carry `{"message": "..."}` as JSON with a 4xx/5xx status. The
  message is human text; do not parse it for control flow.
- **`/build`, `/images/create` (pull) and `/images/{name}/push` stream
  newline-delimited JSON** — one object per line: `{"stream":…}`,
  `{"status":…,"progress":…}`, `{"aux":{"ID":…}}`, `{"errorDetail":{…}}`.

**A failed build, pull or push is still HTTP 200.** The failure arrives as an
`errorDetail` object inside the stream, after the daemon has already committed
to a successful status line. Any client that treats HTTP status as the verdict
reports a broken build as a success. Scan the events.

## The multiplexed stream — the one that looks like it works

`GET /containers/{id}/logs`, `/containers/{id}/attach` and

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

emits header bytes into the caller's log output for every non-TTY container —
which is every container a program actually runs. Demultiplex by reading eight
bytes, taking the length, reading that many payload bytes, repeating. Go clients
get this from `stdcopy.StdCopy`; everyone else writes it.

`attach` and `exec/start` additionally accept `Upgrade: tcp` +
`Connection: Upgrade`, to which the daemon answers **101 Switching Protocols**
and hands over a bidirectional connection. Without those headers it answers 200
and streams the same frames one-way.

## Filters are JSON, and the shape is specific

`filters` is a query parameter holding a JSON-encoded **map of string to array
of string** — the values are arrays of *strings*, even for booleans:

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

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

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

## Registry auth

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

The header is mandatory on **every** push, anonymous included; the anonymous
form is the encoding of `{}`, which is `e30=` — three characters and one pad,
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
references keep their slashes and tags inside the path
(`/images/myrepo/app:v1/push`) — percent-encoding them breaks the reference.
Names from `GET /containers/json` arrive with a leading `/`.

`exec` is two calls: `POST /containers/{id}/exec` creates the instance and
returns an `Id`, `POST /exec/{id}/start` runs it. The exit status comes from
`GET /exec/{id}/json` afterwards (`ExitCode`), never from the start call.

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


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

## Data

- **`{ %hash }` and `\%hash` are different operations, not two styles.** `{ %h }` builds a new anonymous copy; `\%h` references the existing hash. Return a copy when the caller must not mutate your state; return the reference when sharing is the poin...
- **`Path::Tiny`** for every file operation — not `File::Spec`, not bare `open`. `path(...)->child(...)->slurp_utf8`.
- **`JSON::MaybeXS`** always — never `JSON::PP`, `JSON::XS`, `Cpanel::JSON::XS` directly. Encoders get `canonical => 1, convert_blessed => 1`.
- **Every serialiser is deterministic.** MessagePack `->canonical`, DBIC `serializer_options => { canonical => 1 }`. Same rule, every format.
- **Booleans: `JSON->true` / `JSON->false`.** `use JSON::MaybeXS;` covers codec and booleans.
- `$YAML::XS::Boolean = 'JSON::PP'` is one of YAML::XS's fixed mode names, not a module choice — leave it alone.
- **Align `=>` in multi-line hash literals** when keys are of similar length.
- **Optional pairs inline:** `$cond ? ( experimental => 1 ) : (),`

## Configuration

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

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

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

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

## Forbidden

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

## When in doubt

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

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

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

Finished work is out of `list` by default: the board's final column (`done` on
a default board) and `archived` are shown only when asked for by name
(`--status done`, `--archived`). `--sort` takes `id`, `title`, `status`,
`priority`, `created`, `updated` or `due`, and `priority` sorts most urgent
first. `-n`/`--limit` cuts after filtering **and** after sorting, so
`--sort priority -n 5` is the five most urgent open cards rather than five
arbitrary ones put in order -- that is the "what next" call, instead of pulling
the whole board and cutting it locally.

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


Idempotent — archiving an already-archived task is a no-op.

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

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

out of `pick` and out of karr-foundation's selection -- the link is the fact,
`blocked` is the decision.

### Config

```bash
karr config                                  # show all config values
karr config get KEY                          # get a single value
karr config set KEY VALUE                    # set a writable value
karr config show --defaults                  # karr's defaults, no board read
karr config --json                           # JSON output
karr config show --compact                   # key=value per line, no padding
```

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

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

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

```

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

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

one-line payload. A document goes in on stdin instead — with no content
argument at all, `karr set-refs REF < file` stores the file verbatim and
`karr get-refs REF > file` gives it back unchanged.

### Activity log

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

### Flow metrics

```bash
karr metrics                                 # throughput, lead/cycle time, efficiency, aging
karr metrics --since 2026-01-01              # only count tasks completed after this date
karr metrics --compact                       # one line plus one per aging item
karr metrics --json                          # JSON output
```

Every figure comes from the `created`/`started`/`completed` stamps on the
cards, not from the activity log. Cards whose stamps cannot carry a
measurement — an unreadable date, a `started` that precedes the card's own
`created`, or a `completed` that precedes that `started` — are left out of the
averages that need them and counted in `unusable_timestamps` (cards, not
stamps), so a low sample count is visible rather than silent.

Lead time is the deliberate exception: a `completed` that precedes its own

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

| `copyright_year` | Override year |
| `[@Bundle]` | Plugin bundle |
| `[Plugin]` | Individual plugin |

## Plugin Loading Order

1. `[GatherDir]` - Collects files
2. `[PruneCruft]` - Removes unwanted files
3. `[Prereqs]` / `cpanfile` - Dependencies
4. `[Version plugins]` - PkgVersion, AutoVersion, etc.
5. `[Meta plugins]` - MetaJSON, MetaYAML
6. `[Test plugins]` - Tests
7. `[Release plugins]` - UploadToCPAN, etc.
8. `[VCS plugins]` - Git::Commit, Git::Tag, etc.

## Key Questions

1. Which plugin bundle is used?
2. Are there custom plugins configured?
3. Are prereqs in dist.ini or cpanfile?
4. What release mechanism is configured?

Changes  view on Meta::CPAN

    callers are unaffected.
  - `tls => 1` now speaks real TLS over `tcp://`. `cert_path` names the
    `docker` CLI's `ca.pem`/`cert.pem`/`key.pem` layout, `tls_insecure`
    turns verification off, and `tls` defaults from
    `$ENV{DOCKER_TLS_VERIFY}`. `IO::Socket::SSL` is a recommended
    dependency, loaded on the first TLS connection.
  - New `response => \%h` option fills `status`, `reason` and `headers`,
    including for a request that croaked. New `head` verb beside
    `get`/`post`/`put`/`delete_request`, which never waits for a body.
  - `negotiate_version` croaks, naming `GET /version` and the expected
    shape, when the body is not a JSON object carrying an `ApiVersion` of the
    form `N.N`.
  - New `API::Docker::Role::Filters`, consumed by all eight resource classes
    and applied at every `filters` call site: a bare value, a boolean or a
    mis-shaped filter is normalised to the engine's JSON map-of-arrays
    instead of silently matching nothing.
  - JSON request bodies send booleans as real `true`/`false`; a caller may
    pass `1`/`0` or a JSON boolean interchangeably.
  - `_uri_encode` UTF-8-encodes a decoded character string before
    percent-escaping, so a name or tag typed as characters (`ü`, `中`) goes
    out as valid UTF-8.
  - A request path outside the RFC 3986 origin-form character set is refused
    before it reaches the daemon, closing a request-line injection through a
    container name or image reference.
  - An ArrayRef query parameter expands into one repeated `k=v` pair per
    element (`names=a&names=b`), which some endpoints require.
  - A bare JSON scalar body (`null`, `true`, a number, a quoted string) is
    decoded rather than handed back as raw bytes; `raw` and `ndjson` return
    `''` and `[]` for a zero-byte body instead of `undef`.
  - Registry credentials reach `images->pull` (`auth`, sent as
    `X-Registry-Auth`) and `images->build` (`registry_config`, sent as
    `X-Registry-Config`), sent only when given. An already-base64 auth value
    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

Changes  view on Meta::CPAN

    permanent scope decision, not a gap: Podman implements none of it and no
    consumer needs it. `secrets` and `configs` stand on their own and stay
    covered.

0.003     2026-08-27 03:37:00Z
  - t/containers.t: the registered cleanup tolerates the container the
    happy path already removed, so a live run no longer warns "Cleanup
    failed: ... no such container" on every pass. The safety net still
    warns on any other failure.
  - The `>= 400` croak now falls back to `errorDetail.message` and then to
    the flat `error` key when the JSON error body carries no `message`.
    Docker answers `{"message":...}`; Podman answers a failed push with
    the stream shape instead -- `{"errorDetail":{"message":...},"error":
    ...}` and no `message` at all -- so the whole JSON object used to be
    the croak text and the reason had to be dug out of it by eye. A body
    that is not an object still surfaces verbatim.
  - `images->build`, `->pull` and `->push` now croak when the engine
    reports a failure inside the event stream, instead of returning the
    stream and leaving the check to the caller. A failed build, pull or
    push is answered with HTTP 200 -- the status line is committed
    before the operation is attempted -- and the failure arrives as an
    `errorDetail` object among the progress events, so nothing about the
    response says the operation broke. Measured against the rootless
    Podman socket (5.4.2, API 1.41): a Dockerfile of `FROM alpine:3` /

Changes  view on Meta::CPAN

    the word "error" in payload text.
    Which of the three actually takes that route depends on the engine,
    and Podman is not Docker here -- measured on the same socket, all
    three cases: only `build` answers 200 with the failure in the
    stream. A pull of a missing repository answers `403 Forbidden` with
    `{"message":"denied: requested access to the resource is denied"}`,
    a missing tag answers `404 Not Found` with `{"message":"manifest
    unknown: manifest unknown"}`, and a push to an unreachable registry
    answers `500 Internal Server Error` with an `errorDetail` body and
    no `message` key at all. The first two never reach the stream; the
    third has its whole JSON body used as the croak text, because the
    >= 400 path looks for `message`. So on Podman the new check fires
    for `build` and the pre-existing status check catches the other two.
    All three are loud either way, but catching
    `API::Docker::Error::Stream` specifically is not a reliable way to
    catch a failed pull or push -- inspect $@ as a string, which both
    routes satisfy. The POD on each method says which engine does what.
    `system->events` is explicitly exempt and never croaks on stream
    content: it is a feed, so an object in it records something that
    happened on the engine rather than the outcome of this call. The
    check is on by default for the transport's `ndjson` option and

Changes  view on Meta::CPAN

    engine. Instead the body is walked as frames and is only treated as
    framed when the walk consumes it exactly; the one way to fool it,
    and the `tty => 1` option that overrides it, are documented on
    `API::Docker::Role::HTTP::stream_frames`.
    `exec->start` also gained POD saying where the exit status actually
    comes from -- `exec->inspect($id)->{ExitCode}`, a separate call --
    which the method's documentation never mentioned.
  - `images->build`, `->pull` and `->push` now always return an ArrayRef
    of events. `_request` used to try `decode_json` on the whole body
    first and only fall back to line-by-line parsing, so a stream that
    carried exactly one JSON object came back as a HashRef while a
    multi-event stream came back as an ArrayRef, and every caller had to
    check `ref` before iterating. Measured on Podman: `POST /build?q=1`
    emits exactly one object, which is the case that used to change
    shape. The ordinary single-JSON-object endpoints (`/version`,
    `/containers/{id}/json`, ...) are untouched and still return a
    HashRef -- the streaming behaviour is now requested explicitly with
    the new `ndjson => 1` transport option rather than guessed from the
    body. The option is named for the format and not `stream`, which is
    already a query parameter of `/events` and
    `/containers/{id}/stats`.
    `system->events` takes the same option. It was reaching an ArrayRef
    only through the implicit fallback that has now gone, so without it
    the endpoint would have quietly started returning an undecoded
    string. Measured on Podman for one container create/init/start/
    died/remove cycle: five newline-delimited objects, and the body is
    not valid JSON as a whole. Its POD now also says to always pass
    `until`, since the transport buffers the whole response and an
    unbounded event stream therefore never returns.
    Note for anyone scanning these events: a failed build is still HTTP
    200 with the failure carried as an `errorDetail` object inside the
    stream, confirmed on Podman for a Dockerfile whose `RUN` exits 7.
    A failed *pull* differs there -- Podman answers 404 with a plain
    `{"message":...}` body where Docker streams `errorDetail` on a 200 --
    so `pull` can croak as well as report an error event.
  - Bring the cpanfile in line with what the code loads. `URI` was
    required and is used nowhere in `lib/` or `t/`, so every consumer

Changes  view on Meta::CPAN


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

MANIFEST  view on Meta::CPAN

lib/API/Docker/Role/Entity.pm
lib/API/Docker/Role/Entity/Config.pm
lib/API/Docker/Role/Entity/Container.pm
lib/API/Docker/Role/Entity/Image.pm
lib/API/Docker/Role/Entity/Network.pm
lib/API/Docker/Role/Entity/Plugin.pm
lib/API/Docker/Role/Entity/Secret.pm
lib/API/Docker/Role/Entity/Volume.pm
lib/API/Docker/Role/Filters.pm
lib/API/Docker/Role/HTTP.pm
lib/API/Docker/Role/JSONBody.pm
lib/API/Docker/Role/RegistryAuth.pm
lib/API/Docker/Role/Type.pm
lib/API/Docker/Role/Using.pm
lib/API/Docker/Secret.pm
lib/API/Docker/Type.pm
lib/API/Docker/Type/Address.pm
lib/API/Docker/Type/AuthConfig.pm
lib/API/Docker/Type/BuildCache.pm
lib/API/Docker/Type/BuildInfo.pm
lib/API/Docker/Type/ClusterInfo.pm

META.json  view on Meta::CPAN

         "recommends" : {
            "IO::Socket::SSL" : "0"
         },
         "requires" : {
            "Carp" : "0",
            "Errno" : "0",
            "IO::Handle" : "0",
            "IO::Socket::INET" : "0",
            "IO::Socket::UNIX" : "0",
            "Import::Into" : "0",
            "JSON::MaybeXS" : "0",
            "Log::Any" : "0",
            "MIME::Base64" : "0",
            "Module::Runtime" : "0",
            "Moo" : "0",
            "Package::Stash" : "0",
            "Path::Tiny" : "0",
            "Scalar::Util" : "0",
            "Socket" : "0",
            "Types::Standard" : "0",
            "namespace::clean" : "0",

META.json  view on Meta::CPAN

         "version" : "0.004"
      },
      "API::Docker::Role::Filters" : {
         "file" : "lib/API/Docker/Role/Filters.pm",
         "version" : "0.004"
      },
      "API::Docker::Role::HTTP" : {
         "file" : "lib/API/Docker/Role/HTTP.pm",
         "version" : "0.004"
      },
      "API::Docker::Role::JSONBody" : {
         "file" : "lib/API/Docker/Role/JSONBody.pm",
         "version" : "0.004"
      },
      "API::Docker::Role::RegistryAuth" : {
         "file" : "lib/API/Docker/Role/RegistryAuth.pm",
         "version" : "0.004"
      },
      "API::Docker::Role::Type" : {
         "file" : "lib/API/Docker/Role/Type.pm",
         "version" : "0.004"
      },

META.json  view on Meta::CPAN

            "class" : "Dist::Zilla::Plugin::LicenseFile",
            "name" : "@Author::GETTY/LicenseFile",
            "version" : "0.002"
         },
         {
            "class" : "Dist::Zilla::Plugin::MetaConfig",
            "name" : "@Author::GETTY/MetaConfig",
            "version" : "6.037"
         },
         {
            "class" : "Dist::Zilla::Plugin::MetaJSON",
            "name" : "@Author::GETTY/MetaJSON",
            "version" : "6.037"
         },
         {
            "class" : "Dist::Zilla::Plugin::PodSyntaxTests",
            "name" : "@Author::GETTY/PodSyntaxTests",
            "version" : "6.037"
         },
         {
            "class" : "Dist::Zilla::Plugin::Test::ChangesHasContent",
            "name" : "@Author::GETTY/Test::ChangesHasContent",

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

    version: '0.004'
  API::Docker::Role::Entity::Volume:
    file: lib/API/Docker/Role/Entity/Volume.pm
    version: '0.004'
  API::Docker::Role::Filters:
    file: lib/API/Docker/Role/Filters.pm
    version: '0.004'
  API::Docker::Role::HTTP:
    file: lib/API/Docker/Role/HTTP.pm
    version: '0.004'
  API::Docker::Role::JSONBody:
    file: lib/API/Docker/Role/JSONBody.pm
    version: '0.004'
  API::Docker::Role::RegistryAuth:
    file: lib/API/Docker/Role/RegistryAuth.pm
    version: '0.004'
  API::Docker::Role::Type:
    file: lib/API/Docker/Role/Type.pm
    version: '0.004'
  API::Docker::Role::Using:
    file: lib/API/Docker/Role/Using.pm
    version: '0.004'

META.yml  view on Meta::CPAN

    version: '0.004'
recommends:
  IO::Socket::SSL: '0'
requires:
  Carp: '0'
  Errno: '0'
  IO::Handle: '0'
  IO::Socket::INET: '0'
  IO::Socket::UNIX: '0'
  Import::Into: '0'
  JSON::MaybeXS: '0'
  Log::Any: '0'
  MIME::Base64: '0'
  Module::Runtime: '0'
  Moo: '0'
  Package::Stash: '0'
  Path::Tiny: '0'
  Scalar::Util: '0'
  Socket: '0'
  Types::Standard: '0'
  namespace::clean: '0'

META.yml  view on Meta::CPAN

      version: '6.037'
    -
      class: Dist::Zilla::Plugin::LicenseFile
      name: '@Author::GETTY/LicenseFile'
      version: '0.002'
    -
      class: Dist::Zilla::Plugin::MetaConfig
      name: '@Author::GETTY/MetaConfig'
      version: '6.037'
    -
      class: Dist::Zilla::Plugin::MetaJSON
      name: '@Author::GETTY/MetaJSON'
      version: '6.037'
    -
      class: Dist::Zilla::Plugin::PodSyntaxTests
      name: '@Author::GETTY/PodSyntaxTests'
      version: '6.037'
    -
      class: Dist::Zilla::Plugin::Test::ChangesHasContent
      name: '@Author::GETTY/Test::ChangesHasContent'
      version: '0.011'
    -

Makefile.PL  view on Meta::CPAN

  "LICENSE" => "perl",
  "MIN_PERL_VERSION" => "5.014",
  "NAME" => "API::Docker",
  "PREREQ_PM" => {
    "Carp" => 0,
    "Errno" => 0,
    "IO::Handle" => 0,
    "IO::Socket::INET" => 0,
    "IO::Socket::UNIX" => 0,
    "Import::Into" => 0,
    "JSON::MaybeXS" => 0,
    "Log::Any" => 0,
    "MIME::Base64" => 0,
    "Module::Runtime" => 0,
    "Moo" => 0,
    "Package::Stash" => 0,
    "Path::Tiny" => 0,
    "Scalar::Util" => 0,
    "Socket" => 0,
    "Types::Standard" => 0,
    "namespace::clean" => 0,

Makefile.PL  view on Meta::CPAN



my %FallbackPrereqs = (
  "Carp" => 0,
  "Errno" => 0,
  "Exporter" => 0,
  "IO::Handle" => 0,
  "IO::Socket::INET" => 0,
  "IO::Socket::UNIX" => 0,
  "Import::Into" => 0,
  "JSON::MaybeXS" => 0,
  "Log::Any" => 0,
  "MIME::Base64" => 0,
  "Module::Runtime" => 0,
  "Moo" => 0,
  "Package::Stash" => 0,
  "Path::Tiny" => 0,
  "Scalar::Util" => 0,
  "Socket" => 0,
  "Test::More" => 0,
  "Types::Standard" => 0,

cpanfile  view on Meta::CPAN

requires 'perl', '5.014';

requires 'Carp';
requires 'Errno';
requires 'Import::Into';
requires 'IO::Handle';
requires 'IO::Socket::INET';
requires 'IO::Socket::UNIX';
requires 'JSON::MaybeXS';
requires 'Log::Any';
requires 'MIME::Base64';
requires 'Module::Runtime';
requires 'Moo';
requires 'namespace::clean';
requires 'overload';
requires 'Package::Stash';
requires 'Path::Tiny';
requires 'Scalar::Util';
requires 'Socket';

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

  return if $self->_version_negotiated;
  return if defined $self->api_version;

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

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

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

  elsif (!defined $version_info->{ApiVersion}) {
    $got = 'an object with no ApiVersion field';
  }
  else {
    my $v = $version_info->{ApiVersion};
    $got = 'an ApiVersion of '
      . (ref $v ? 'a ' . ref($v) . ' reference' : "'" . $v . "'");
  }

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

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

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

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

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

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

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

Options:

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

        Name   => 'my-config',
        Data   => "listen 8080;\n",
        Labels => { app => 'web' },
    );

    # Inspect a config -- an API::Docker::Type::Config; spec->data stays base64
    my $config = $docker->configs->inspect($created->{ID});
    my $text   = $config->decoded_data;

    # Update: the version comes from the inspect above, and is mandatory
    my %spec = %{ $config->spec->TO_JSON };
    delete $spec{Data};                       # already base64 -- see below
    $spec{Labels} = { app => 'web', tier => 'edge' };
    $config->update(%spec);

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

=head1 DESCRIPTION

This module provides methods for managing Docker configs (C</configs>):

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

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
has never run it is the ordinary case, not an edge one.

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

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

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


List configs. Returns an ArrayRef of L<API::Docker::Type::Config> objects,
each carrying the methods of L<API::Docker::Role::Entity::Config>. Each
carries a C<< ->spec->data >> that is still base64;
L<API::Docker::Role::Entity::Config/decoded_data> is the decode.

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 = $configs->create(
        Name   => 'my-config',

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

package API::Docker::API::Containers;
# ABSTRACT: Docker Engine Containers API
our $VERSION = '0.004';
use Moo;
with 'API::Docker::Role::Filters', 'API::Docker::Role::Using',
  'API::Docker::Role::JSONBody';
use API::Docker::Error::HTTP;
use API::Docker::Role::Entity::Container;
use API::Docker::Type::ContainerInspectResponse;
use API::Docker::Type::ContainerSummary;
use Carp qw( croak shortmess );
use JSON::MaybeXS qw( decode_json );
use MIME::Base64 qw( decode_base64 );
use Scalar::Util qw( blessed );
use namespace::clean;


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

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

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


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

sub create {
  my ($self, %config) = @_;

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

  return $result;
}


sub changes {
  my ($self, $id) = @_;
  croak "Container ID required" unless $id;
  my $result = $self->client->get("/containers/$id/changes",
    %{ $self->_request_options },
  );
  # A container with nothing changed answers with a JSON null, which the
  # transport decodes to undef. Normalised here so the return is always
  # something a caller can iterate.
  return ref $result eq 'ARRAY' ? $result : [];
}


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

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

sub update {
  my ($self, $id, %config) = @_;
  croak "Container ID required" unless $id;
  $self->_json_bools(\%config, @UPDATE_BOOLS);
  return $self->client->post("/containers/$id/update", \%config);
}


# The engine reports what a path is in a response header rather than a body,
# so both GET and HEAD carry it and only HEAD has nothing else to say. The
# header is base64-encoded JSON; handing the caller the base64 would make
# every one of them write this.
sub _decode_path_stat {
  my ($self, $response) = @_;

  my $header = $response->{headers}{'x-docker-container-path-stat'};
  return undef unless defined $header && length $header;

  # Docker encodes this one with Go's base64.StdEncoding -- unlike
  # X-Registry-Auth, which is URLEncoding. Decoded tolerantly rather than
  # strictly: translating the two URL-safe characters first costs nothing and

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

        Env   => ['FOO=bar'],
    );

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

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

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

Boolean flags may be given as a Perl C<1>/C<0> or as a JSON boolean; either
goes out as a real JSON C<true>/C<false>, which the engine's body type-check
requires. This applies to the top-level flags (C<Tty>, C<OpenStdin>,
C<AttachStdin>, C<AttachStdout>, C<AttachStderr>, C<StdinOnce>,
C<NetworkDisabled>, C<ArgsEscaped>) and to the C<HostConfig> flags
(C<Privileged>, C<PublishAllPorts>, C<ReadonlyRootfs>, C<AutoRemove>, C<Init>,
C<OomKillDisable>).

=head2 inspect

    my $container = $containers->inspect($id);

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

=item * C<0> - B<modified>. The path exists in both and its contents or
metadata changed

=item * C<1> - B<added>. The path exists only in the container

=item * C<2> - B<deleted>. The path existed in the image and is gone

=back

A container with nothing changed comes back as an empty ArrayRef; the engine
answers that case with a JSON C<null> rather than an empty list.

Measured against Podman 5.4.2 (API 1.41): the endpoint is served, but an
unknown container is answered with B<500> and
C<< {"cause":"layer not known","message":"<id> not found: layer not known"} >>
rather than the 404 every other container endpoint gives -- so a caller
distinguishing "no such container" from a real failure cannot do it on the
status code alone on that engine.

=head2 export

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


Rename a container.

=head2 update

    $containers->update($id, Memory => 314572800);

Update container resource limits and configuration.

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;

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

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

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

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

=over

=item * C<name> - The path's basename. For a symlink the two engines
disagree: Docker reports the requested path's own basename, Podman the
resolved target's

=item * C<size> - Size in bytes

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

package API::Docker::API::Exec;
# ABSTRACT: Docker Engine Exec API
our $VERSION = '0.004';
use Moo;
with 'API::Docker::Role::Using', 'API::Docker::Role::JSONBody';
use Carp qw( croak );
use namespace::clean;


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


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

sub create {
  my ($self, $container_id, %config) = @_;
  croak "Container ID required" unless $container_id;
  croak "Cmd required" unless $config{Cmd};
  $self->_json_bools(\%config, @EXEC_CONFIG_BOOLS);
  return $self->client->post("/containers/$container_id/exec", \%config);

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

    );

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

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

Common config keys: C<AttachStdin>, C<AttachStdout>, C<AttachStderr>, C<Tty>,
C<Env>, C<User>, C<WorkingDir>.

The boolean flags (C<AttachStdin>, C<AttachStdout>, C<AttachStderr>, C<Tty>,
C<Privileged>) may be given as a Perl C<1>/C<0> or as a JSON boolean; either
goes out as a real JSON C<true>/C<false>, which the engine's body type-check
requires. Passing C<1> where the daemon wants a boolean would otherwise be
rejected.

=head2 start

    my $frames = $exec->start($exec_id, Detach => 0);

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

Start an exec instance. Returns an ArrayRef of frames in the same shape as

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

package API::Docker::API::Images;
# ABSTRACT: Docker Engine Images API
our $VERSION = '0.004';
use Moo;
with 'API::Docker::Role::Filters', 'API::Docker::Role::RegistryAuth',
  'API::Docker::Role::Using';
use API::Docker::Role::Entity::Image;
use API::Docker::Type::ImageInspect;
use API::Docker::Type::ImageSummary;
use Carp qw( croak );
use JSON::MaybeXS qw( encode_json );
use namespace::clean;


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


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

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

There is no C<< ->virtual_size >>: the swagger dropped C<VirtualSize> from
both definitions after v1.44, and engines that still send it -- the Podman on
this machine does -- have it kept verbatim in
C<< ->unknown_fields->{VirtualSize} >>, where C<TO_JSON> writes it back
unchanged. F<t/type_fixture_passthrough.t> pins that.

=head2 client

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

=head2 list

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

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

        buildargs => { APP_VERSION => '1.0' },
        nocache   => 1,
    );

Build an image from a tar archive containing a Dockerfile and build context.

The C<context> parameter is required and must contain the raw bytes of a tar
archive (or a scalar reference to one).

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

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

The engine answers a failed build with HTTP 200 and reports the failure as an
C<errorDetail> object inside the stream, so nothing about the response status
says the build broke. This method used to return that stream like any other

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

as a fallback, so defaulting it onto an already-qualified name breaks the pull:
measured against Docker 29.7.2 (API 1.55) C<< pull(fromImage => 'nginx:1.25')
>> would silently fetch C<nginx:latest> and report success, and against Podman
5.8.4 (compat API 1.44) the same request answers C<500 invalid reference
format> for C<nginx:1.25:latest>. A digest reference breaks the same way on
both. So C<tag> is sent only if given explicitly, or defaulted to C<latest>
when the name carries neither a C<:tag> (in the segment after the last C</>)
nor an C<@digest>. A registry C<host:port/> prefix is not mistaken for a tag.

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

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

=over

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

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

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

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

behind C<docker image save>. Together with L</load> it is the only way in or
out of a daemon that does not go through a registry.

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

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

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

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


    for my $event (@$events) {
        print $event->{stream} if defined $event->{stream};
    }

Import a tar archive produced by L</get> or L</get_all> -- the endpoint behind
C<docker image load>. The archive is the request body; pass it as raw bytes or
as a scalar reference to them, the way L</build> takes its context.

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.
The last of them names what was imported:

    my ($loaded) = grep { ($_->{stream} // '') =~ /^Loaded image/ } @$events;

Options:

=over

=item * C<quiet> - Suppress the per-layer progress detail in the response
stream

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

=back

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

A failed load croaks, but by which route depends on the engine, the same split
L</pull> and L</push> have. Docker reports it as an C<errorDetail> object

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

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

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

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

Options:

=over

=item * C<keep_storage> - Bytes of cache to keep. Sent as the engine's
C<keep-storage>, which is also accepted as the option name; the underscore
form exists because the hyphenated one has to be quoted in a Perl hash

=item * C<all> - Remove all cache, not just the dangling entries

=item * C<filters> - HashRef of filters, e.g. C<< { until => ['24h'] } >>;
values are ArrayRefs of strings, shape-checked and normalised by
L<API::Docker::Role::Filters>, and passed to the transport unencoded because
it JSON-encodes a HashRef params value itself

=back

=head1 SEE ALSO

=over

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

=item * L<API::Docker::Role::Entity::Image> - the convenience methods the

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

package API::Docker::API::Networks;
# ABSTRACT: Docker Engine Networks API
our $VERSION = '0.004';
use Moo;
with 'API::Docker::Role::Filters', 'API::Docker::Role::Using',
  'API::Docker::Role::JSONBody';
use API::Docker::Role::Entity::Network;
use API::Docker::Type::Network;
use Carp qw( croak );
use namespace::clean;


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

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

  my ($self, $id) = @_;
  croak "Network ID required" unless $id;
  my $result = $self->client->get("/networks/$id",
    %{ $self->_request_options },
  );
  return $self->_wrap('API::Docker::Type::Network', $result);
}


# The NetworkCreateRequest booleans of spec/v1.51.yaml. The engine rejects a
# number for any of them, so 1/0 is normalised to a JSON boolean on the way
# out; a caller may still pass 1/0 or a JSON boolean and it goes out correctly.
my @NETWORK_CREATE_BOOLS = qw(
  Attachable ConfigOnly EnableIPv4 EnableIPv6 Ingress Internal
);

sub create {
  my ($self, %config) = @_;
  croak "Network name required" unless $config{Name};
  $self->_json_bools(\%config, @NETWORK_CREATE_BOOLS);
  my $result = $self->client->post('/networks/create', \%config);
  return $result;

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

=head2 create

    my $result = $networks->create(
        Name   => 'my-network',
        Driver => 'bridge',
    );

Create a network. Returns hashref with C<Id> and C<Warning>.

Boolean flags (C<Internal>, C<Attachable>, C<Ingress>, C<ConfigOnly>,
C<EnableIPv4>, C<EnableIPv6>) 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 remove

    $networks->remove($id);

Remove a network.

=head2 connect

    $networks->connect($network_id, Container => $container_id);

Connect a container to a network.

=head2 disconnect

    $networks->disconnect($network_id, Container => $container_id, Force => 1);

Disconnect a container from a network. Optional C<Force> parameter, given as a
Perl C<1>/C<0> or a JSON boolean; it goes out as a real JSON C<true>/C<false>,
which the engine's body type-check requires.

=head2 prune

    my $result = $networks->prune;
    my $result = $networks->prune(filters => { until => ['24h'] });

Delete unused networks. Returns hashref with C<NetworksDeleted>.

Options:

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

    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';
  return $result;
}


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

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

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


List installed plugins. Returns an ArrayRef of L<API::Docker::Type::Plugin>
objects, each carrying the methods of L<API::Docker::Role::Entity::Plugin>.
An engine with no plugins installed answers C<[]>, never C<null>, so this is
an empty ArrayRef rather than C<undef>.

Options:

=over

=item * C<filters> - HashRef of filters, JSON-encoded by the transport. Values
are ArrayRefs of strings even for booleans -- L<API::Docker::Role::Filters>
shape-checks and normalises that, but not the names, which the daemon
validates itself

=back

The accepted filter names are C<enabled> and C<capability>. B<It is C<enabled>,
not C<enable>> -- the published Engine API reference says C<enable>, and the
daemon validates plugin filter names against its own list, so the documented
spelling is refused outright rather than silently matching nothing. C<enabled>

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

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.

=head2 Progress as it arrives

Without a callback the whole stream is read before anything is parsed, so
pulling a plugin is silence until it is done. Pass C<on_event> and the events
are handed over as the daemon sends them:

    my $summary = $plugins->install('vieux/sshfs:latest',
        privileges => $privileges,

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

        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.

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

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

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

    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',

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


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

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


=head2 events

    my $events = $system->events(
        since   => 1234567890,
        until   => 1234567900,
        filters => { type => ['container'] },
    );

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

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

B<Bound the window with C<until>, or pass C<on_event>.> Without a callback the

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.

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



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

# The hook API::Docker::Role::Type's BUILDARGS reads. Its constructor sorts
# every key into "a field of this definition" or "a field the daemon sent
# that the model has not heard of", and `client` is neither: without this it
# would land in unknown_fields and TO_JSON would try to send the client
# object back to the engine. Anything an entity role adds as an attribute of
# its own belongs in this list.
sub _entity_attributes { return ('client') }


1;

__END__

=pod

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

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
subclass, which costs something per object and makes C<ref> report a name no
documentation mentions.

=head2 What this role contributes

The half every entity shares: the client the methods delegate through. The

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

=head1 SYNOPSIS

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

    say $config->id;
    say $config->spec->name;
    say $config->spec->data;        # still base64, as the daemon sent it
    say $config->decoded_data;      # the bytes

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

    $config->remove;

=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

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

to compare it against the wire or hand it back.

That is the whole reason the accessor belongs on the entity rather than on
the API class: an entity may offer a derived view of a response, an API
method may not silently replace one.

=head2 The spec goes back as a whole

C<< $config->spec >> is an L<API::Docker::Type::ConfigSpec> object rather
than the raw HashRef the hand-written entity kept, so the idiom for an update
is C<< %{ $config->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.
Mind the C<Data> it brings with it -- see L</update>.

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 decoded_data

    my $text = $config->decoded_data;

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


=head2 inspect

    my $fresh = $config->inspect;

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

=head2 update

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

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

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

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

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

=head2 remove

    $config->remove;

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

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


=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

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

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

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


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

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

package API::Docker::Role::Filters;
# ABSTRACT: The filters query parameter, normalised into the one shape the engine reads
our $VERSION = '0.004';
use Moo::Role;
use Carp qw( croak );
use namespace::clean;


# The boolean classes JSON::MaybeXS hands back across its backends. Named
# rather than duck-typed: a blessed object that merely overloads bool is not
# a claim of being a JSON boolean.
my %BOOLEAN_CLASS = map { $_ => 1 } qw(
  JSON::PP::Boolean
  Types::Serialiser::Boolean
);

sub _normalise_filters {
  my ($self, $filters) = @_;

  croak __PACKAGE__ . '->_normalise_filters filters must be a HashRef of '
    . 'filter name to value, e.g. { dangling => [\'true\'] }'
    unless ref $filters eq 'HASH';

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

  }

  return \%normalised;
}

sub _normalise_filter_value {
  my ($self, $name, $value) = @_;

  my $where = __PACKAGE__ . '->_normalise_filters filter \'' . $name . '\' ';

  croak $where . 'has an undefined value; the engine reads a JSON null into '
    . 'a string as the empty string and rejects it there'
    unless defined $value;

  my $ref = ref $value;
  return $value ? 'true' : 'false' if $BOOLEAN_CLASS{$ref};

  if ($ref eq 'SCALAR') {
    croak $where . 'is a ScalarRef to something other than 1 or 0; \\1 and '
      . '\\0 are read as the booleans \'true\' and \'false\''
      unless $$value eq '1' || $$value eq '0';

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


  croak $where . 'has a ' . $ref . ' reference as a value; filter values are '
    . 'strings, or an ArrayRef of them' if $ref;

  croak $where . 'has an empty value; the engine rejects it. A Perl boolean '
    . 'stringifies to \'\' when false -- the engine wants the string '
    . '\'false\''
    unless length $value;

  # Stringify a copy: a scalar carrying a number would otherwise be
  # JSON-encoded as one, and the engine's filter values are strings.
  return "$value";
}


1;

__END__

=pod

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

      my ($self, %opts) = @_;
      my %params;
      $params{filters} = $self->_normalise_filters($opts{filters})
        if defined $opts{filters};
      return $self->client->get('/whatever', params => \%params);
    }

=head1 DESCRIPTION

Every C<list> and C<prune> endpoint of the Engine API takes a C<filters>
query parameter, and every one of them wants the same thing: a JSON B<map of
string to array of string>.

    filters => { dangling => ['true'] }        correct
    filters => { dangling => 'true'   }        wrong -- not an array
    filters => { dangling => 1        }        wrong -- not an array
    filters => { dangling => [1]      }        wrong -- a number, not a string
    filters => { dangling => [\1]     }        wrong -- a JSON boolean

The transport JSON-encodes a HashRef C<params> value on its own, so the
I<encoding> was never the problem. The I<shape> is, and it is the thing
clients get wrong, because Perl has no notion of "array of string" and a
HashRef literal will happily hold whatever the caller typed.

This role normalises that shape in one place, so the twelve methods that
accept C<filters> agree on it and document it by pointing here.

=head2 What it does

=over

=item * A value that is not an ArrayRef is wrapped into a one-element one, so
C<< { dangling => 'true' } >> means what it looks like it means.

=item * Each element is stringified, so C<< { stars => [3] } >> reaches the
wire as C<"3"> rather than as the number C<3>.

=item * A JSON boolean object (C<< JSON->true >>, C<< JSON->false >>) and the
ScalarRef form this distribution uses for JSON request bodies (C<\1>, C<\0>)
become the strings C<'true'> and C<'false'>.

=item * Anything else -- another ref, C<undef>, an empty string -- croaks.

=back

The result is a fresh HashRef; the caller's is never modified.

=head2 Why the boolean rewrite is bound to the type and not to the value

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

exited with status 0, C<< { stars => [0] } >> for images with no stars, and
C<< { label => [1] } >> for a label whose value is C<1> -- rewriting any of
those to C<'false'>/C<'true'> would silently ask a different question.

Binding it to the filter I<name> instead would need a table of which names
are boolean, per endpoint, kept in step with the daemon -- see
L</"What it deliberately does not do">.

So the rewrite is bound to the value's B<type>: a plain Perl C<1> carries no
claim of being a boolean and becomes the string C<"1">, while
C<< JSON->true >> and C<\1> carry exactly that claim, and are also the two
forms C<encode_json> would otherwise turn into a JSON C<true> -- which the
daemon rejects outright.

That leaves one form this role cannot recognise: perl 5.36's core booleans,
where C<< !!1 >> and C<< $x == $y >> produce a boolean the JSON encoder also
writes as C<true>. Stringified, those are C<"1"> and C<""> -- and C<"1"> is a
value the daemon reads as true, so only the false one needs help. It is the
reason an empty string croaks here rather than travelling on.

=head2 What it deliberately does not do

It does not check filter B<names>. Doing so would need one accepted-name
table per endpoint, and the daemon already has them: measured against Podman
5.x (API 1.41), an unknown name is refused with HTTP 500 by
C</containers/json> (C<bogusname is an invalid filter>), C</images/json>

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

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

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

=head1 METHODS

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

use IO::Socket::UNIX;
use IO::Socket::INET;
# For the sysread method on a plain filehandle: _pull calls it as a method so
# that IO::Socket::SSL's own gets picked up rather than the builtin. See _pull.
use IO::Handle;
use Socket qw( SOL_SOCKET SO_RCVTIMEO );
# How a read that delivered nothing says it ran out of time rather than out of
# stream (EAGAIN/EWOULDBLOCK), and how it says it was interrupted rather than
# either (EINTR). See _pull and _timed_out.
use Errno qw( EAGAIN EWOULDBLOCK EINTR );
use JSON::MaybeXS qw( encode_json decode_json );
use Scalar::Util qw( looks_like_number );
use Path::Tiny;
use Carp qw( croak shortmess );
use Log::Any qw( $log );
use API::Docker::Error::HTTP;
use API::Docker::Error::Stream;
use API::Docker::Error::Timeout;
use API::Docker::Error::Truncated;
use namespace::clean;

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

# rejected, not sanitised, for the reason a header name is (see
# _assert_request_path). Query parameters carry the ? and everything after it
# and are assembled separately below, each element run through _uri_encode.
my $REQUEST_PATH = qr{\A[A-Za-z0-9\-._~:/\@!\$&'()*+,;=%]*\z};

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

# What a response body has to start with to be worth handing to decode_json.
# An object or an array is not the whole of JSON: the engine answers several
# endpoints with a bare JSON scalar, and a `null` used to come back as the
# four-character string 'null'. See _request.
my $JSON_BODY = qr/\A\s*(?:[\[\{"]|-?[0-9]|true|false|null)/;

# How much is asked for per sysread. Strictly an upper bound -- sysread
# returns what has arrived rather than filling to it (see _pull), so on a live
# feed a call typically comes back with one burst, and asking for 64K costs
# nothing but the size of the buffer it lands in.
my $READ_SIZE = 64 * 1024;

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

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


  if ($status_code >= 400) {
    my $error_msg = $body;
    my $data;
    if ($body && $body =~ /^\s*[\{\[]/) {
      eval {
        $data = decode_json($body);
        # Docker answers with {"message":...}. Podman answers a failed push
        # with the stream shape instead -- {"errorDetail":{"message":...},
        # "error":...} and no message key at all -- so without these two
        # fallbacks the whole JSON object became the croak text (karr k13).
        my $detail = ref $data->{errorDetail} eq 'HASH'
          ? $data->{errorDetail}{message} : undef;
        $error_msg = $data->{message} // $detail // $data->{error} // $body;
      };
    }

    # Carp hands a reference straight back rather than decorating it, so this
    # croak is a die with an object -- hence the location captured by hand,
    # which names the same frame a croak of a plain string would have named.
    # message . location is byte for byte what the string croak produced,

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

  # Zero bytes is a different answer in each shape a request can ask for, so
  # the two options that promise one are answered before the empty-body check
  # rather than after it. `raw` promises the response bytes and a body of no
  # bytes is '', which a caller can take length() of; `ndjson` promises an
  # ArrayRef of events even for a stream carrying a single object, so a stream
  # that carried none is []. Returning undef for both broke each promise
  # exactly where the engine legitimately says nothing.
  $body = '' unless defined $body;

  # The framed endpoints (logs, attach, exec/start) carry arbitrary bytes
  # that must not be mistaken for JSON -- a TTY container printing a JSON
  # line would otherwise come back decoded.
  return $body if $opts{raw};

  # Streaming endpoints (/build, /images/create, /images/*/push) always
  # return an ArrayRef of events, even when the stream carried exactly one
  # object.  See _decode_stream.
  if ($opts{ndjson}) {
    my $events = $self->_decode_stream($body);
    # A failed build, pull or push is HTTP 200 with the failure buried in the
    # stream, so the status line above cannot catch it.  Opt out for a stream

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

    $self->_assert_no_stream_error($endpoint, $events)
      if $opts{croak_on_error} // 1;
    return $events;
  }

  # Nothing was asked of the body's shape and there is no body, so there is
  # nothing to hand back. 204 says so in the status line and is taken at its
  # word even if bytes follow it.
  return undef if $status_code == 204 || $body eq '';

  # A body that is JSON is decoded, whichever JSON value it is. The guard was
  # `{` or `[` alone, which returned a body that is a bare JSON scalar as its
  # own bytes: `null` came back as the four-character string 'null'. The
  # engine sends exactly that where a Go nil slice or pointer is the whole
  # response -- GET /plugins/privileges for a plugin that demands nothing,
  # GET /containers/{id}/changes for a container that changed nothing -- and
  # the string is neither the ArrayRef those endpoints document nor anything
  # a caller can iterate.
  #
  # The eval decides, not the pattern: a plain-text body that happens to
  # start with one of these characters fails to decode and is returned as
  # itself. So must the eval's success, not its result -- decode_json('null')
  # is a successful decode to undef.
  if ($body =~ $JSON_BODY) {
    my $decoded;
    return $decoded if eval { $decoded = decode_json($body); 1 };
  }

  return $body;
}

sub _decode_stream {
  my ($self, $body) = @_;

  # Newline-delimited JSON: one object per line.  A literal newline cannot
  # occur inside a JSON string, so splitting on lines is safe.
  my @events;
  for my $line (split /\r?\n/, $body) {
    next unless $line =~ /\S/;
    my $event = eval { decode_json($line) };
    push @events, $event if defined $event;
  }

  # Fall back to the whole body for a stream that is not newline-framed
  # (a single pretty-printed object), so nothing is silently dropped.
  unless (@events) {

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

# body] shape, plus a fifth element: the summary of what the callback was
# handed. The body it returns is empty -- that is the point, nothing is kept --
# except on the two paths that fall back to reading whole, which return undef
# as the summary instead so _request treats them exactly as before.
sub _read_streaming_response {
  my ($self, $sock, $method, $handler, $ctx) = @_;
  $ctx ||= {};

  my ($status_code, $status_text, $headers) = @{ $self->_read_head($sock, $ctx) };

  # Neither of these is a stream. A >= 400 body is a short JSON object naming
  # the failure and _request has to croak with it, so it is read whole and the
  # callback never sees it; a HEAD response has no body at all.
  if ($status_code >= 400 || (defined $method && uc($method) eq 'HEAD')) {
    return [$status_code, $status_text, $headers,
      $self->_read_body($sock, $headers, $method, $ctx), undef];
  }

  # Set only here, past the two branches above, so a timeout while reading an
  # error body is still reported in bytes rather than in units nothing
  # delivered. From this point on an expiry carries the callback's own summary

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

  # worth complaining about. One the caller stopped has bytes in the carry
  # buffer by construction, and treating those as truncation would turn every
  # early stop into an error.
  $handler->{finish}->() unless $handler->{stopped}->();

  return [$status_code, $status_text, $headers, '', $handler->{summary}->()];
}

# One unit per call, and the unit is whichever of the three the caller asked
# for. The engine's streaming endpoints do not share one: /events and the
# build/pull/push progress streams are newline-delimited JSON, logs and
# exec/start are 8-byte-framed, and an image export is bytes with no structure
# above them at all. Forcing one unit on all three would mean handing two of
# them back undecoded and calling it streaming.
#
# The three decoders differ only in how they cut the byte stream up; the carry
# buffer, the delivery and the stop handling below are common to all of them.
sub _stream_handler {
  my ($self, $endpoint, $option, $cb, $croak_on_error) = @_;

  my $carry     = '';

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

      # Checked per event rather than over the finished list, so a failed
      # build croaks at the event that reports it instead of when the daemon
      # eventually closes. The Error::Stream then carries that one event: a
      # callback stream keeps no history, having been given all of it already.
      $self->_assert_no_stream_error($endpoint, [$event]) if $croak_on_error;
      return $deliver->($event);
    };
    $feed = sub {
      my ($bytes) = @_;
      $carry .= $bytes;
      # A JSON string cannot contain a literal newline, so a newline in the
      # buffer always ends an event -- and everything after the last one is
      # an event still arriving, which stays in the carry for the next read.
      while ((my $idx = index($carry, "\n")) >= 0) {
        my $line = substr($carry, 0, $idx, '');
        substr($carry, 0, 1, '');
        return 0 unless $emit_line->($line);
      }
      return 1;
    };
    $finish = sub {

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


=over

=item * Unix socket transport (C<unix://...>)

=item * TCP socket transport (C<tcp://host:port>), in the clear or over TLS
with client certificates (L</"TLS on a tcp:// connection">)

=item * HTTP/1.1 chunked transfer encoding

=item * Automatic JSON encoding/decoding

=item * Newline-delimited JSON event streams (C<< ndjson => 1 >>), including
the failures the engine reports inside an HTTP 200 body

=item * Demultiplexing of the Docker stream format (L</stream_frames>)

=item * Incremental delivery of a response through a per-request callback, so
the endpoints that never close are usable at all (L</"Streaming a response as
it arrives">)

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

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

Per request it is an option of L</get>, L</post>, L</put>, L</delete_request>
and L</head>. A resource class carries it through
L<API::Docker::Role::Using/using>. See L</"Bounding the connection itself">
for what it does on each transport, which is not the same thing on all
three.

=head2 get

    my $data = $client->get($path, %opts);

Perform HTTP GET request. Returns decoded JSON or raw response body.

Options:

=over

=item * C<params> - HashRef of query parameters; a HashRef value is JSON-encoded

=item * C<headers> - HashRef of extra HTTP headers, e.g.
C<< { 'X-Registry-Auth' => $b64 } >>

=item * C<ndjson> - Parse the body as newline-delimited JSON and always
return an ArrayRef of events, even for a stream carrying a single object.
Named for the format rather than C<stream>, which is already a query
parameter of C</events> and C</containers/{id}/stats>. An C<errorDetail>
event in such a stream croaks; see L</"Failure inside a 200 response">

=item * C<croak_on_error> - Default true, and only consulted with
C<< ndjson => 1 >>. Set it false for a stream whose objects are engine data
rather than the outcome of one operation -- C</events> is the only such
endpoint here

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


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

=head3 One unit per call, and three units to choose from

The engine's streaming endpoints do not share a natural unit, so there is an
option per unit and a request picks one:

=over

=item * C<on_event> - one decoded HashRef per newline-delimited JSON object.
For C</events> and the C</build>, C</images/create>, C</images/*/push>
progress streams

=item * C<on_frame> - one C<< { stream => ..., data => ... } >> HashRef per
demultiplexed frame of the Docker stream format. For
C<< /containers/{id}/logs >> and C<< /exec/{id}/start >>; normally reached
through L</stream_frames> rather than directly

=item * C<on_chunk> - the response bytes as they arrive, undecoded and
unbuffered. For an image export, and for anything with no structure this role

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


Every read now goes through one C<sysread> and a buffer this role keeps
itself, which is also why the status line and the headers are read the same
way: PerlIO's read-ahead put the first bytes of the body somewhere the body
reader could not get at them, so the header reads had to move too or those
bytes would have been dropped.

=head3 What is not streamed

A response with status >= 400 is read whole and croaked with as always: it is
a short JSON object naming a failure, not a stream, and the callback never
sees it. C<response> is still filled. A C<HEAD> response has no body, so a
callback on one is never called and C<undef> comes back as usual.

With C<on_event>, C<croak_on_error> works as it does for C<ndjson> -- except
that the check runs per event, so a failed build croaks at the event that
reports it instead of when the daemon eventually closes. The
L<API::Docker::Error::Stream> then carries that one event in C<< ->events >>
rather than the whole stream: the callback was handed the rest as it arrived,
and none of it was kept.

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

header name gets. Sanitising is not on the table here: percent-encoding the
path at this layer cannot tell a separator from data, so it would either
mangle every C<< / >> and C<:> or leave the injection open. Query parameters
belong in C<params>, which is assembled separately and runs each element
through C<_uri_encode>.

=head2 post

    my $data = $client->post($path, $body, %opts);

Perform HTTP POST request. C<$body> is automatically JSON-encoded if provided.

Options: C<params>, C<headers>, C<ndjson>, C<croak_on_error>, C<raw>,
C<response> and the C<on_event>/C<on_frame>/C<on_chunk> callbacks as for
L</get>, plus C<raw_body> and C<content_type> for sending a non-JSON payload
such as a build context tarball.

=head2 put

    my $data = $client->put($path, $body, %opts);

Perform HTTP PUT request. C<$body> is automatically JSON-encoded if provided.

Options: C<params>, C<headers>, C<ndjson>, C<croak_on_error>, C<raw>,
C<response> and the C<on_event>/C<on_frame>/C<on_chunk> callbacks as for
L</get>, plus C<raw_body> and C<content_type> for sending a non-JSON payload
-- C<< containers->put_archive >> uses both to send a tar stream.

=head2 delete_request

    my $data = $client->delete_request($path, %opts);

Perform HTTP DELETE request.

Options: C<params> (hashref of query parameters).

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


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

C<stream> is C<stdout>, C<stderr> or C<stdin> for a multiplexed stream, and
C<raw> for an unframed one. It is always a plain string, so callers never need
a defined-check. Joining the payloads gives the plain text:

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

The response body is never JSON-decoded, so a container printing JSON lines is
returned verbatim.

Options are those of C<_request> (C<params>, C<body>, C<headers>), plus:

=over

=item * C<tty> - Skip demultiplexing and return the body as a single C<raw>
frame. Set it when the container or exec instance was created with a TTY and
its output is binary; see L</"Detecting a framed stream"> for why.



( run in 1.872 second using v1.01-cache-2.11-cpan-364913b4093 )